Merge remote-tracking branch 'goog/ub-deskclock-huddle'
Bug: 35488649
Test: lunch aosp_bullhead-userdebug && make -j24
Change-Id: I353f634cd877dd4ef6002d51b8a03954c894f265
diff --git a/Android.mk b/Android.mk
index 9cd0d9f..f757e7f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -2,7 +2,6 @@
include $(CLEAR_VARS)
LOCAL_RESOURCE_DIR := packages/apps/DeskClock/res
-LOCAL_RESOURCE_DIR += frameworks/opt/datetimepicker/res
ifeq ($(TARGET_BUILD_APPS),)
LOCAL_RESOURCE_DIR += frameworks/support/design/res
@@ -36,8 +35,7 @@
LOCAL_PROGUARD_FLAG_FILES += ../../../frameworks/support/v7/preference/proguard-rules.pro
LOCAL_PROGUARD_FLAG_FILES += ../../../frameworks/support/v7/recyclerview/proguard-rules.pro
-LOCAL_STATIC_JAVA_LIBRARIES := android-opt-datetimepicker
-LOCAL_STATIC_JAVA_LIBRARIES += android-support-design
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-design
LOCAL_STATIC_JAVA_LIBRARIES += android-support-percent
LOCAL_STATIC_JAVA_LIBRARIES += android-support-transition
LOCAL_STATIC_JAVA_LIBRARIES += android-support-v13
@@ -56,6 +54,5 @@
LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.gridlayout
LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.preference
LOCAL_AAPT_FLAGS += --extra-packages android.support.v7.recyclerview
-LOCAL_AAPT_FLAGS += --extra-packages com.android.datetimepicker
include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9da916f..100712e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -6,7 +6,7 @@
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
+ 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,
@@ -48,7 +48,8 @@
android:icon="@mipmap/ic_launcher_alarmclock"
android:label="@string/app_label"
android:requiredForAllUsers="true"
- android:supportsRtl="true">
+ android:supportsRtl="true"
+ android:theme="@style/Theme.DeskClock">
<!-- ============================================================== -->
<!-- Main app components. -->
@@ -58,7 +59,6 @@
android:name=".DeskClock"
android:label="@string/app_label"
android:launchMode="singleTask"
- android:theme="@style/DeskClockTheme"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -69,12 +69,18 @@
</activity>
<activity
+ android:name=".ringtone.RingtonePickerActivity"
+ android:excludeFromRecents="true"
+ android:taskAffinity=""
+ android:theme="@style/Theme.DeskClock.RingtonePicker" />
+
+ <activity
android:name=".worldclock.CitySelectionActivity"
android:excludeFromRecents="true"
android:label="@string/cities_activity_title"
android:parentActivityName=".DeskClock"
android:taskAffinity=""
- android:theme="@style/CitiesTheme" />
+ android:theme="@style/Theme.DeskClock.CitySelection" />
<activity
android:name=".settings.SettingsActivity"
@@ -82,7 +88,7 @@
android:label="@string/settings"
android:parentActivityName=".DeskClock"
android:taskAffinity=""
- android:theme="@style/SettingsTheme" />
+ android:theme="@style/Theme.DeskClock.Settings" />
<activity
android:name=".HandleShortcuts"
@@ -136,7 +142,6 @@
android:resizeableActivity="false"
android:showOnLockScreen="true"
android:taskAffinity=""
- android:theme="@style/AlarmAlertFullScreenTheme"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
@@ -183,8 +188,7 @@
android:launchMode="singleInstance"
android:resizeableActivity="false"
android:showOnLockScreen="true"
- android:taskAffinity=""
- android:theme="@style/ExpiredTimersActivityTheme" />
+ android:taskAffinity="" />
<!-- Legacy broadcast receiver that honors old scheduled timers across app upgrade. -->
<receiver
@@ -218,15 +222,14 @@
android:name=".ScreensaverActivity"
android:excludeFromRecents="true"
android:resizeableActivity="false"
- android:taskAffinity=""
- android:theme="@style/ScreensaverActivityTheme" />
+ android:taskAffinity="" />
<activity
android:name=".settings.ScreensaverSettingsActivity"
android:excludeFromRecents="true"
android:label="@string/screensaver_settings"
android:taskAffinity=""
- android:theme="@style/SettingsTheme" />
+ android:theme="@style/Theme.DeskClock.Settings" />
<service
android:name=".Screensaver"
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/anim-v22/caret_toclose_animation_interpolator_0.xml
similarity index 84%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/anim-v22/caret_toclose_animation_interpolator_0.xml
index 114006d..bce86f7 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/anim-v22/caret_toclose_animation_interpolator_0.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.0001,0.0 0.0,1.0 1.0,1.0" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/anim-v22/caret_toopen_animation_interpolator_0.xml
similarity index 84%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/anim-v22/caret_toopen_animation_interpolator_0.xml
index 114006d..bce86f7 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/anim-v22/caret_toopen_animation_interpolator_0.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,6 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.0001,0.0 0.0,1.0 1.0,1.0" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/anim-v22/ic_ringtone_active_animation_interpolator.xml
similarity index 90%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/anim-v22/ic_ringtone_active_animation_interpolator.xml
index 114006d..c636b14 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/anim-v22/ic_ringtone_active_animation_interpolator.xml
@@ -16,4 +16,4 @@
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.66666667,1.0 1.0,1.0" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/anim-v24/ic_pause_play_animation_interpolator_0.xml
similarity index 88%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/anim-v24/ic_pause_play_animation_interpolator_0.xml
index 114006d..f64181e 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/anim-v24/ic_pause_play_animation_interpolator_0.xml
@@ -16,4 +16,4 @@
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.409107671822,0.0 0.199295043945,0.999999938222 1.0,1.0" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/anim-v24/ic_pause_play_animation_interpolator_1.xml
similarity index 90%
rename from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
rename to res/anim-v24/ic_pause_play_animation_interpolator_1.xml
index 114006d..4de192d 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/anim-v24/ic_pause_play_animation_interpolator_1.xml
@@ -16,4 +16,4 @@
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.409107671822,0.0 0.0,1.0 1.0,1.0" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/anim-v24/ic_play_pause_animation_interpolator_0.xml
similarity index 89%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/anim-v24/ic_play_pause_animation_interpolator_0.xml
index 114006d..d683106 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/anim-v24/ic_play_pause_animation_interpolator_0.xml
@@ -16,4 +16,4 @@
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.402967439735,0.0 0.193154348289,1.0 1.0,1.0" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/anim-v24/ic_play_pause_animation_interpolator_1.xml
similarity index 88%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/anim-v24/ic_play_pause_animation_interpolator_1.xml
index 114006d..d645fe5 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/anim-v24/ic_play_pause_animation_interpolator_1.xml
@@ -16,4 +16,4 @@
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.402967439735,0.0 0.193154348289,0.999999985538 1.0,1.0" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_1.xml b/res/anim-v24/ic_stop_play_animation_interpolator_0.xml
similarity index 91%
rename from res/interpolator-v22/ic_lap_animation_interpolator_1.xml
rename to res/anim-v24/ic_stop_play_animation_interpolator_0.xml
index d3728c4..03cc0b0 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_1.xml
+++ b/res/anim-v24/ic_stop_play_animation_interpolator_0.xml
@@ -16,4 +16,4 @@
<pathInterpolator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.4,0.0 0.4,1.0 1.0,1.0" />
+ android:pathData="M 0.0,0.0 c 0.6,0.0 0.2,1.0 1.0,1.0" />
diff --git a/res/interpolator/ic_clock_animation_interpolator_0.xml b/res/anim/ic_clock_animation_interpolator_0.xml
similarity index 100%
rename from res/interpolator/ic_clock_animation_interpolator_0.xml
rename to res/anim/ic_clock_animation_interpolator_0.xml
diff --git a/res/interpolator/ic_clock_animation_interpolator_1.xml b/res/anim/ic_clock_animation_interpolator_1.xml
similarity index 100%
rename from res/interpolator/ic_clock_animation_interpolator_1.xml
rename to res/anim/ic_clock_animation_interpolator_1.xml
diff --git a/res/interpolator/ic_stopwatch_button_translatex_interpolator.xml b/res/anim/ic_stopwatch_button_translatex_interpolator.xml
similarity index 100%
rename from res/interpolator/ic_stopwatch_button_translatex_interpolator.xml
rename to res/anim/ic_stopwatch_button_translatex_interpolator.xml
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v21/day_button_checked_alpha_animation.xml
similarity index 74%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v21/day_button_checked_alpha_animation.xml
index 114006d..5cc7d70 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v21/day_button_checked_alpha_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="250"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
\ No newline at end of file
diff --git a/res/animator-v21/day_button_checked_animation.xml b/res/animator-v21/day_button_checked_animation.xml
new file mode 100644
index 0000000..29cee41
--- /dev/null
+++ b/res/animator-v21/day_button_checked_animation.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:duration="250"
+ android:propertyName="scaleX"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+ <objectAnimator
+ android:duration="250"
+ android:propertyName="scaleY"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+</set>
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v21/day_button_unchecked_alpha_animation.xml
similarity index 71%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v21/day_button_unchecked_alpha_animation.xml
index 114006d..65e667c 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v21/day_button_unchecked_alpha_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="300"
+ android:propertyName="alpha"
+ android:startOffset="133"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
\ No newline at end of file
diff --git a/res/animator-v21/day_button_unchecked_animation.xml b/res/animator-v21/day_button_unchecked_animation.xml
new file mode 100644
index 0000000..949b24c
--- /dev/null
+++ b/res/animator-v21/day_button_unchecked_animation.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together">
+ <objectAnimator
+ android:duration="300"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="scaleX"
+ android:valueFrom="0.875"
+ android:valueTo="1.5"
+ android:valueType="floatType" />
+ <objectAnimator
+ android:duration="300"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="scaleY"
+ android:valueFrom="0.875"
+ android:valueTo="1.5"
+ android:valueType="floatType" />
+</set>
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/caret_collapse_base_animation.xml
similarity index 67%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/caret_collapse_base_animation.xml
index 114006d..45d0684 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/caret_collapse_base_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="300"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:pathData="M 12.0,15.0 c 0.0,-1.0 0.0,-5.33333 0.0,-6.0"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/caret_collapse_l_animation.xml
similarity index 68%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/caret_collapse_l_animation.xml
index 114006d..a352f3c 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/caret_collapse_l_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="300"
+ android:interpolator="@anim/caret_toclose_animation_interpolator_0"
+ android:propertyName="rotation"
+ android:valueFrom="45.0"
+ android:valueTo="-45.0"
+ android:valueType="floatType" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/caret_collapse_r_animation.xml
similarity index 68%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/caret_collapse_r_animation.xml
index 114006d..e6292e2 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/caret_collapse_r_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="300"
+ android:interpolator="@anim/caret_toclose_animation_interpolator_0"
+ android:propertyName="rotation"
+ android:valueFrom="-45.0"
+ android:valueTo="45.0"
+ android:valueType="floatType" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/caret_expand_base_animation.xml
similarity index 67%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/caret_expand_base_animation.xml
index 114006d..78753fe 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/caret_expand_base_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,10 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="300"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:pathData="M 12.0,9.0 c 0.0,0.66667 0.0,5.0 0.0,6.0"
+ android:propertyXName="translateX"
+ android:propertyYName="translateY" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/caret_expand_l_animation.xml
similarity index 68%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/caret_expand_l_animation.xml
index 114006d..c6eb694 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/caret_expand_l_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="300"
+ android:interpolator="@anim/caret_toopen_animation_interpolator_0"
+ android:propertyName="rotation"
+ android:valueFrom="-45.0"
+ android:valueTo="45.0"
+ android:valueType="floatType" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/caret_expand_r_animation.xml
similarity index 68%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/caret_expand_r_animation.xml
index 114006d..d5273fd 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/caret_expand_r_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,7 +14,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="300"
+ android:interpolator="@anim/caret_toopen_animation_interpolator_0"
+ android:propertyName="rotation"
+ android:valueFrom="45.0"
+ android:valueTo="-45.0"
+ android:valueType="floatType" />
\ No newline at end of file
diff --git a/res/animator-v22/ic_clock_hour_hand_animation.xml b/res/animator-v22/ic_clock_hour_hand_animation.xml
index 7f09360..97927ab 100644
--- a/res/animator-v22/ic_clock_hour_hand_animation.xml
+++ b/res/animator-v22/ic_clock_hour_hand_animation.xml
@@ -25,7 +25,7 @@
android:valueFrom="117.75"
android:valueTo="116.68351"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_1" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_1" />
<objectAnimator
android:duration="41"
android:propertyName="translateX"
@@ -95,7 +95,7 @@
android:valueFrom="114.26022"
android:valueTo="114.25"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_0" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_0" />
</set>
<set
android:ordering="sequentially" >
@@ -105,7 +105,7 @@
android:valueFrom="128.25"
android:valueTo="128.05357"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_1" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_1" />
<objectAnimator
android:duration="41"
android:propertyName="translateY"
@@ -175,7 +175,7 @@
android:valueFrom="124.41091"
android:valueTo="124.0"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_0" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_0" />
</set>
<objectAnimator
android:duration="833"
diff --git a/res/animator-v22/ic_clock_minute_hand_animation.xml b/res/animator-v22/ic_clock_minute_hand_animation.xml
index bebb394..2b58723 100644
--- a/res/animator-v22/ic_clock_minute_hand_animation.xml
+++ b/res/animator-v22/ic_clock_minute_hand_animation.xml
@@ -25,7 +25,7 @@
android:valueFrom="114.25"
android:valueTo="115.55177"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_1" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_1" />
<objectAnimator
android:duration="41"
android:propertyName="translateX"
@@ -81,7 +81,7 @@
android:valueFrom="117.74111"
android:valueTo="117.75"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_0" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_0" />
</set>
<set
android:ordering="sequentially" >
@@ -91,7 +91,7 @@
android:valueFrom="124.0"
android:valueTo="124.26329"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_1" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_1" />
<objectAnimator
android:duration="41"
android:propertyName="translateY"
@@ -147,7 +147,7 @@
android:valueFrom="127.62279"
android:valueTo="128.25"
android:valueType="floatType"
- android:interpolator="@interpolator/ic_clock_animation_interpolator_0" />
+ android:interpolator="@anim/ic_clock_animation_interpolator_0" />
</set>
<objectAnimator
android:duration="833"
diff --git a/res/animator-v22/ic_lap_ic_refresh_white_outlines_animation.xml b/res/animator-v22/ic_lap_ic_refresh_white_outlines_animation.xml
deleted file mode 100644
index cc7b104..0000000
--- a/res/animator-v22/ic_lap_ic_refresh_white_outlines_animation.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <objectAnimator
- android:duration="500"
- android:propertyName="rotation"
- android:valueFrom="-270.0"
- android:valueTo="90.0"
- android:valueType="floatType"
- android:interpolator="@interpolator/ic_lap_animation_interpolator_1" />
-</set>
diff --git a/res/animator-v22/ic_lap_path_1_animation.xml b/res/animator-v22/ic_lap_path_1_animation.xml
deleted file mode 100644
index 96e4708..0000000
--- a/res/animator-v22/ic_lap_path_1_animation.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <set
- android:ordering="sequentially" >
- <objectAnimator
- android:duration="200"
- android:propertyName="pathData"
- android:valueFrom="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 4.48883056641,-4.48062133789 4.4888458252,-4.48063659668 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -11.2804718018,11.2843780518 -11.2804718018,11.284362793 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 3.94947814941,-3.95066833496 3.94371032715,-3.94970703125 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueTo="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 -0.0254516601562,0.0207214355469 -0.0254364013672,0.0207061767578 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -2.81059265137,2.87217712402 -2.81059265137,2.87216186523 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 -0.00611877441406,-0.0398101806641 -0.0118865966797,-0.0388488769532 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueType="pathType"
- android:interpolator="@interpolator/ic_lap_animation_interpolator_0" />
- <objectAnimator
- android:duration="300"
- android:propertyName="pathData"
- android:valueFrom="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 -0.0254516601562,0.0207214355469 -0.0254364013672,0.0207061767578 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -2.81059265137,2.87217712402 -2.81059265137,2.87216186523 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 -0.00611877441406,-0.0398101806641 -0.0118865966797,-0.0388488769532 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueTo="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 4.48883056641,-4.48062133789 4.4888458252,-4.48063659668 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -11.2804718018,11.2843780518 -11.2804718018,11.284362793 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 3.94947814941,-3.95066833496 3.94371032715,-3.94970703125 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueType="pathType"
- android:interpolator="@interpolator/ic_lap_animation_interpolator_2" />
- </set>
-</set>
diff --git a/res/animator-v22/ic_lap_reset_ic_refresh_white_outlines_0_animation.xml b/res/animator-v22/ic_lap_reset_ic_refresh_white_outlines_0_animation.xml
deleted file mode 100644
index e4a35d5..0000000
--- a/res/animator-v22/ic_lap_reset_ic_refresh_white_outlines_0_animation.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <objectAnimator
- android:duration="366"
- android:propertyName="rotation"
- android:valueFrom="-270.0"
- android:valueTo="0.0"
- android:valueType="floatType"
- android:interpolator="@interpolator/ic_lap_reset_animation_interpolator_0" />
-</set>
diff --git a/res/animator-v22/ic_lap_reset_path_2_animation.xml b/res/animator-v22/ic_lap_reset_path_2_animation.xml
deleted file mode 100644
index f52d343..0000000
--- a/res/animator-v22/ic_lap_reset_path_2_animation.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <objectAnimator
- android:duration="366"
- android:propertyName="pathData"
- android:valueFrom="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 4.48883056641,-4.48062133789 4.4888458252,-4.48063659668 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -11.2804718018,11.2843780518 -11.2804718018,11.284362793 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 3.94947814941,-3.95066833496 3.94371032715,-3.94970703125 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueTo="M 3.0,-18.0 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -6.0,0.0 -6.0,0.0 c 0.0,0.0 7.80000305176,7.80000305176 7.80000305176,7.80000305176 c 0.0,0.0 0.0999908447266,0.300003051758 0.0999908447266,0.300003051758 c 0.0,0.0 8.10000610352,-8.10000610352 8.10000610352,-8.10000610352 c 0.0,0.0 -6.0,0.0 -6.0,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.19044494629,0.0 -6.11320495605,-1.07075500488 -8.43977355957,-2.82905578613 c -0.0890655517578,-0.0673065185547 -0.668563842773,-0.534942626953 -0.862991333008,-0.707794189453 c -0.204864501953,-0.18212890625 -0.404037475586,-0.36994934082 -0.597229003906,-0.563140869141 c 0.0,0.0 -2.80000305176,2.79998779297 -2.80000305176,2.79998779297 c 0.247512817383,0.255249023438 0.502792358398,0.502716064453 0.765487670898,0.742080688477 c 0.103759765625,0.0945434570312 0.547332763672,0.478912353516 0.665634155273,0.576156616211 c 3.04972839355,2.50672912598 6.97491455078,3.98176574707 11.2688751221,3.98176574707 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueType="pathType"
- android:interpolator="@interpolator/ic_lap_reset_animation_interpolator_0" />
-</set>
diff --git a/res/animator-v22/ic_reset_lap_ic_refresh_white_outlines_1_animation.xml b/res/animator-v22/ic_reset_lap_ic_refresh_white_outlines_1_animation.xml
deleted file mode 100644
index 088d105..0000000
--- a/res/animator-v22/ic_reset_lap_ic_refresh_white_outlines_1_animation.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <objectAnimator
- android:duration="366"
- android:propertyName="rotation"
- android:valueFrom="0.0"
- android:valueTo="-270.0"
- android:valueType="floatType"
- android:interpolator="@interpolator/ic_reset_lap_animation_interpolator_1" />
-</set>
diff --git a/res/animator-v22/ic_reset_lap_path_3_animation.xml b/res/animator-v22/ic_reset_lap_path_3_animation.xml
deleted file mode 100644
index b7aa262..0000000
--- a/res/animator-v22/ic_reset_lap_path_3_animation.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<set
- xmlns:android="http://schemas.android.com/apk/res/android" >
- <objectAnimator
- android:duration="366"
- android:propertyName="pathData"
- android:valueFrom="M 3.0,-18.0 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -6.0,0.0 -6.0,0.0 c 0.0,0.0 7.80000305176,7.80000305176 7.80000305176,7.80000305176 c 0.0,0.0 0.0999908447266,0.300003051758 0.0999908447266,0.300003051758 c 0.0,0.0 8.10000610352,-8.10000610352 8.10000610352,-8.10000610352 c 0.0,0.0 -6.0,0.0 -6.0,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.19044494629,0.0 -6.11320495605,-1.07075500488 -8.43977355957,-2.82905578613 c -0.0890655517578,-0.0673065185547 -0.668563842773,-0.534942626953 -0.862991333008,-0.707794189453 c -0.204864501953,-0.18212890625 -0.404037475586,-0.36994934082 -0.597229003906,-0.563140869141 c 0.0,0.0 -2.80000305176,2.79998779297 -2.80000305176,2.79998779297 c 0.247512817383,0.255249023438 0.502792358398,0.502716064453 0.765487670898,0.742080688477 c 0.103759765625,0.0945434570312 0.547332763672,0.478912353516 0.665634155273,0.576156616211 c 3.04972839355,2.50672912598 6.97491455078,3.98176574707 11.2688751221,3.98176574707 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueTo="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 4.48883056641,-4.48062133789 4.4888458252,-4.48063659668 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -11.2804718018,11.2843780518 -11.2804718018,11.284362793 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 3.94947814941,-3.95066833496 3.94371032715,-3.94970703125 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:valueType="pathType"
- android:interpolator="@interpolator/ic_reset_lap_animation_interpolator_0" />
-</set>
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/ic_ringtone_active_outlines_0_animation.xml
similarity index 69%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/ic_ringtone_active_outlines_0_animation.xml
index 114006d..f03f39f 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/ic_ringtone_active_outlines_0_animation.xml
@@ -14,6 +14,13 @@
limitations under the License.
-->
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="140"
+ android:interpolator="@anim/ic_ringtone_active_animation_interpolator"
+ android:propertyName="rotation"
+ android:repeatCount="-1"
+ android:repeatMode="reverse"
+ android:valueFrom="12.0"
+ android:valueTo="-12.0"
+ android:valueType="floatType" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/ic_ringtone_active_outlines_1_animation.xml
similarity index 69%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/ic_ringtone_active_outlines_1_animation.xml
index 114006d..68bada8 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/ic_ringtone_active_outlines_1_animation.xml
@@ -14,6 +14,13 @@
limitations under the License.
-->
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="140"
+ android:interpolator="@anim/ic_ringtone_active_animation_interpolator"
+ android:propertyName="rotation"
+ android:repeatCount="-1"
+ android:repeatMode="reverse"
+ android:valueFrom="-8.0"
+ android:valueTo="8.0"
+ android:valueType="floatType" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v22/ic_ringtone_active_outlines_2_animation.xml
similarity index 70%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v22/ic_ringtone_active_outlines_2_animation.xml
index 114006d..3d57f35 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v22/ic_ringtone_active_outlines_2_animation.xml
@@ -14,6 +14,13 @@
limitations under the License.
-->
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="140"
+ android:interpolator="@anim/ic_ringtone_active_animation_interpolator"
+ android:propertyName="translateX"
+ android:repeatCount="-1"
+ android:repeatMode="reverse"
+ android:valueFrom="-3"
+ android:valueTo="3"
+ android:valueType="floatType" />
\ No newline at end of file
diff --git a/res/animator-v22/ic_stopwatch_animation_button.xml b/res/animator-v22/ic_stopwatch_animation_button.xml
index 27e00cf..874ea42 100644
--- a/res/animator-v22/ic_stopwatch_animation_button.xml
+++ b/res/animator-v22/ic_stopwatch_animation_button.xml
@@ -15,11 +15,10 @@
limitations under the License.
-->
-<set xmlns:android="http://schemas.android.com/apk/res/android" >
- <objectAnimator
- android:duration="267"
- android:propertyXName="translateX"
- android:propertyYName="translateY"
- android:pathData="M 12,12 c 0,0.16667 0,1 0,1 l 0,-1 "
- android:interpolator="@interpolator/ic_stopwatch_button_translatex_interpolator" />
-</set>
+<objectAnimator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="267"
+ android:interpolator="@anim/ic_stopwatch_button_translatex_interpolator"
+ android:pathData="M 12,12 c 0,0.16667 0,1 0,1 l 0,-1 "
+ android:propertyXName="translateX"
+ android:propertyYName="translateY" />
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v24/ic_pause_play_path_0_animation.xml
similarity index 74%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v24/ic_pause_play_path_0_animation.xml
index 114006d..6218db2 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v24/ic_pause_play_path_0_animation.xml
@@ -14,6 +14,11 @@
limitations under the License.
-->
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="250"
+ android:interpolator="@anim/ic_pause_play_animation_interpolator_0"
+ android:propertyName="rotation"
+ android:valueFrom="0.0"
+ android:valueTo="90.0"
+ android:valueType="floatType" />
diff --git a/res/animator-v24/ic_pause_play_path_1_animation.xml b/res/animator-v24/ic_pause_play_path_1_animation.xml
new file mode 100644
index 0000000..793a987
--- /dev/null
+++ b/res/animator-v24/ic_pause_play_path_1_animation.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<objectAnimator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:interpolator="@anim/ic_pause_play_animation_interpolator_1"
+ android:propertyName="pathData"
+ android:valueFrom="M -3.9375,-14.0625 c 0.0,0.0 -8.0625,0.0 -8.0625,0.0 c 0.0,0.0 0.0,28.125 0.0,28.125 c 0.0,0.0 8.0625,0.0 8.0625,0.0 c 0.0,0.0 0.0,-28.125 0.0,-28.125 Z"
+ android:valueTo="M 0.0,-14.0625 c 0.0,0.0 0.0,0.0 0.0,0.0 c 0.0,0.0 -14.0625,22.0625 -14.0625,22.0625 c 0.0,0.0 14.0625,0.0 14.0625,0.0 c 0.0,0.0 0.0,-22.0625 0.0,-22.0625 Z"
+ android:valueType="pathType" />
diff --git a/res/animator-v24/ic_pause_play_path_2_animation.xml b/res/animator-v24/ic_pause_play_path_2_animation.xml
new file mode 100644
index 0000000..a6d777c
--- /dev/null
+++ b/res/animator-v24/ic_pause_play_path_2_animation.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<objectAnimator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:interpolator="@anim/ic_pause_play_animation_interpolator_1"
+ android:propertyName="pathData"
+ android:valueFrom="M -3.9375,-14.0625 c 0.0,0.0 -8.0625,0.0 -8.0625,0.0 c 0.0,0.0 0.0,28.125 0.0,28.125 c 0.0,0.0 8.0625,0.0 8.0625,0.0 c 0.0,0.0 0.0,-28.125 0.0,-28.125 Z"
+ android:valueTo="M -15.9375,-14.0625 c 0.0,0.0 0.0,0.0 0.0,0.0 c 0.0,0.0 0.0,22.0625 0.0,22.0625 c 0.0,0.0 14.0625,0.0 14.0625,0.0 c 0.0,0.0 -14.0625,-22.0625 -14.0625,-22.0625 Z"
+ android:valueType="pathType" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v24/ic_play_pause_path_0_animation.xml
similarity index 74%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v24/ic_play_pause_path_0_animation.xml
index 114006d..e7b5251 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v24/ic_play_pause_path_0_animation.xml
@@ -14,6 +14,11 @@
limitations under the License.
-->
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="250"
+ android:interpolator="@anim/ic_play_pause_animation_interpolator_1"
+ android:propertyName="rotation"
+ android:valueFrom="90.0"
+ android:valueTo="180.0"
+ android:valueType="floatType" />
diff --git a/res/animator-v24/ic_play_pause_path_1_animation.xml b/res/animator-v24/ic_play_pause_path_1_animation.xml
new file mode 100644
index 0000000..69dab8d
--- /dev/null
+++ b/res/animator-v24/ic_play_pause_path_1_animation.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<objectAnimator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:interpolator="@anim/ic_play_pause_animation_interpolator_0"
+ android:propertyName="pathData"
+ android:valueFrom="M 0.0,-14.0625 c 0.0,0.0 0.0,0.0 0.0,0.0 c 0.0,0.0 -14.0625,22.0625 -14.0625,22.0625 c 0.0,0.0 14.0625,0.0 14.0625,0.0 c 0.0,0.0 0.0,-22.0625 0.0,-22.0625 Z"
+ android:valueTo="M -3.9375,-14.0625 c 0.0,0.0 -8.0625,0.0 -8.0625,0.0 c 0.0,0.0 0.0,28.125 0.0,28.125 c 0.0,0.0 8.0625,0.0 8.0625,0.0 c 0.0,0.0 0.0,-28.125 0.0,-28.125 Z"
+ android:valueType="pathType" />
diff --git a/res/animator-v24/ic_play_pause_path_2_animation.xml b/res/animator-v24/ic_play_pause_path_2_animation.xml
new file mode 100644
index 0000000..9ba8b1b
--- /dev/null
+++ b/res/animator-v24/ic_play_pause_path_2_animation.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<objectAnimator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="250"
+ android:interpolator="@anim/ic_play_pause_animation_interpolator_0"
+ android:propertyName="pathData"
+ android:valueFrom="M -15.9375,-14.0625 c 0.0,0.0 0.0,0.0 0.0,0.0 c 0.0,0.0 0.0,22.0625 0.0,22.0625 c 0.0,0.0 14.0625,0.0 14.0625,0.0 c 0.0,0.0 -14.0625,-22.0625 -14.0625,-22.0625 Z"
+ android:valueTo="M -3.9375,-14.0625 c 0.0,0.0 -8.0625,0.0 -8.0625,0.0 c 0.0,0.0 0.0,28.125 0.0,28.125 c 0.0,0.0 8.0625,0.0 8.0625,0.0 c 0.0,0.0 0.0,-28.125 0.0,-28.125 Z"
+ android:valueType="pathType" />
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/animator-v24/ic_stop_play_ic_stop_play_path_0_animation.xml
similarity index 74%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/animator-v24/ic_stop_play_ic_stop_play_path_0_animation.xml
index 114006d..48d6174 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/animator-v24/ic_stop_play_ic_stop_play_path_0_animation.xml
@@ -14,6 +14,11 @@
limitations under the License.
-->
-<pathInterpolator
+<objectAnimator
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:duration="366"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="rotation"
+ android:valueFrom="0.0"
+ android:valueTo="90.0"
+ android:valueType="floatType" />
diff --git a/res/animator-v24/ic_stop_play_path_1_animation.xml b/res/animator-v24/ic_stop_play_path_1_animation.xml
new file mode 100644
index 0000000..0b938ff
--- /dev/null
+++ b/res/animator-v24/ic_stop_play_path_1_animation.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<objectAnimator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="366"
+ android:interpolator="@anim/ic_stop_play_animation_interpolator_0"
+ android:propertyName="pathData"
+ android:valueFrom="M -6.0,-6.0 c 0.0,0.0 12.0,0.0 12.0,0.0 c 0.0,0.0 0.0,12.0 0.0,12.0 c 0.0,0.0 -12.0,0.0 -12.0,0.0 c 0.0,0.0 0.0,-12.0 0.0,-12.0 Z"
+ android:valueTo="M 0.0,-7.001953125 c 0.0,0.0 7.001953125,11.0009765625 7.001953125,11.0009765625 c 0.0,0.0 0.0,0.0009765625 0.0,0.0009765625 c 0.0,0.0 -14.001953125,0.0 -14.001953125,0.0 c 0.0,0.0 7.0,-11.001953125 7.0,-11.001953125 Z"
+ android:valueType="pathType" />
diff --git a/res/color/backspace_tint_color.xml b/res/color/backspace_tint_color.xml
index a18f3db..f01ba25 100644
--- a/res/color/backspace_tint_color.xml
+++ b/res/color/backspace_tint_color.xml
@@ -15,6 +15,6 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_enabled="true" android:color="@color/clock_white" />
+ <item android:state_enabled="true" android:color="@color/white" />
<item android:color="@color/white_63p" />
</selector>
\ No newline at end of file
diff --git a/res/color/bg_day_tint_color.xml b/res/color/bg_day_tint_color.xml
deleted file mode 100644
index 0368475..0000000
--- a/res/color/bg_day_tint_color.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2014 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
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_checked="true" android:color="@color/clock_white" />
- <item android:color="@android:color/transparent" />
-</selector>
diff --git a/res/color/tab_tint_color.xml b/res/color/tab_tint_color.xml
index 6b777db..912a9fe 100644
--- a/res/color/tab_tint_color.xml
+++ b/res/color/tab_tint_color.xml
@@ -15,7 +15,7 @@
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_focused="true" android:color="@color/clock_white" />
- <item android:state_selected="true" android:color="@color/clock_white" />
+ <item android:state_focused="true" android:color="@color/white" />
+ <item android:state_selected="true" android:color="@color/white" />
<item android:color="@color/white_63p" />
</selector>
\ No newline at end of file
diff --git a/res/drawable-hdpi/bg_gray_circle.png b/res/drawable-hdpi/bg_gray_circle.png
deleted file mode 100644
index 5ee0d68..0000000
--- a/res/drawable-hdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/drawable-ldrtl/ic_label.xml
similarity index 61%
copy from res/drawable-v22/ic_lap_reset_animation.xml
copy to res/drawable-ldrtl/ic_label.xml
index 041c9e1..25f757f 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/drawable-ldrtl/ic_label.xml
@@ -14,13 +14,17 @@
limitations under the License.
-->
-<animated-vector
+<vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
- <target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
- <target
- android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
-</animated-vector>
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <group
+ android:pivotX="12"
+ android:scaleX="-1">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M17.63,5.84C17.27,5.33 16.67,5 16,5L5,5.01C3.9,5.01 3,5.9 3,7v10c0,1.1 0.9,1.99 2,1.99L16,19c0.67,0 1.27,-0.33 1.63,-0.84L22,12l-4.37,-6.16z" />
+ </group>
+</vector>
diff --git a/res/drawable-mdpi/bg_gray_circle.png b/res/drawable-mdpi/bg_gray_circle.png
deleted file mode 100644
index 68328ff..0000000
--- a/res/drawable-mdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-hdpi/bg_gray_circle.png b/res/drawable-sw600dp-hdpi/bg_gray_circle.png
deleted file mode 100644
index 2be09ea..0000000
--- a/res/drawable-sw600dp-hdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-mdpi/bg_gray_circle.png b/res/drawable-sw600dp-mdpi/bg_gray_circle.png
deleted file mode 100644
index 4145e3d..0000000
--- a/res/drawable-sw600dp-mdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-xhdpi/bg_gray_circle.png b/res/drawable-sw600dp-xhdpi/bg_gray_circle.png
deleted file mode 100644
index 6fe8be4..0000000
--- a/res/drawable-sw600dp-xhdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-xxhdpi/bg_gray_circle.png b/res/drawable-sw600dp-xxhdpi/bg_gray_circle.png
deleted file mode 100644
index 5c222ba..0000000
--- a/res/drawable-sw600dp-xxhdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-sw600dp-xxxhdpi/bg_gray_circle.png b/res/drawable-sw600dp-xxxhdpi/bg_gray_circle.png
deleted file mode 100644
index 06e9dab..0000000
--- a/res/drawable-sw600dp-xxxhdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-v21/bg_day_button_animatable.xml b/res/drawable-v21/bg_day_button_animatable.xml
new file mode 100644
index 0000000..5f34ac8
--- /dev/null
+++ b/res/drawable-v21/bg_day_button_animatable.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="day_button_circle_vector"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <group
+ android:name="day_button_circle_animate"
+ android:alpha="1.0"
+ android:pivotX="0.0"
+ android:pivotY="0.0"
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:name="path"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M 0.0,8.0 c -4.41999816895,0.0 -8.0,-3.58200073242 -8.0,-8.0 c 0.0,-4.41799926758 3.58000183105,-8.0 8.0,-8.0 c 4.41999816895,0.0 8.0,3.58200073242 8.0,8.0 c 0.0,4.41799926758 -3.58000183105,8.0 -8.0,8.0 Z"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/drawable-v21/bg_day_button_checked_to_unchecked_anim.xml b/res/drawable-v21/bg_day_button_checked_to_unchecked_anim.xml
new file mode 100644
index 0000000..8ffcb46
--- /dev/null
+++ b/res/drawable-v21/bg_day_button_checked_to_unchecked_anim.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+<animated-vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/bg_day_button_animatable">
+ <target
+ android:name="day_button_circle_animate"
+ android:animation="@animator/day_button_unchecked_animation" />
+ <target
+ android:name="day_button_circle_vector"
+ android:animation="@animator/day_button_unchecked_alpha_animation" />
+</animated-vector>
\ No newline at end of file
diff --git a/res/drawable-v21/bg_day_button_transparent.xml b/res/drawable-v21/bg_day_button_transparent.xml
new file mode 100644
index 0000000..42001c4
--- /dev/null
+++ b/res/drawable-v21/bg_day_button_transparent.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="day_button_circle_vector"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <group
+ android:name="day_button_circle_transparent"
+ android:alpha="0.0"
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:name="path"
+ android:fillColor="#00FFFFFF"
+ android:pathData="M 0.0,8.0 c -4.41999816895,0.0 -8.0,-3.58200073242 -8.0,-8.0 c 0.0,-4.41799926758 3.58000183105,-8.0 8.0,-8.0 c 4.41999816895,0.0 8.0,3.58200073242 8.0,8.0 c 0.0,4.41799926758 -3.58000183105,8.0 -8.0,8.0 Z"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/drawable-v21/bg_day_button_unchecked_to_checked_anim.xml b/res/drawable-v21/bg_day_button_unchecked_to_checked_anim.xml
new file mode 100644
index 0000000..0bca97e
--- /dev/null
+++ b/res/drawable-v21/bg_day_button_unchecked_to_checked_anim.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+<animated-vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/bg_day_button_animatable">
+ <target
+ android:name="day_button_circle_animate"
+ android:animation="@animator/day_button_checked_animation" />
+ <target
+ android:name="day_button_circle_vector"
+ android:animation="@animator/day_button_checked_alpha_animation" />
+</animated-vector>
\ No newline at end of file
diff --git a/res/drawable-v21/bg_day_button_white.xml b/res/drawable-v21/bg_day_button_white.xml
new file mode 100644
index 0000000..1ebfd57
--- /dev/null
+++ b/res/drawable-v21/bg_day_button_white.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="day_button_circle_vector"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <group
+ android:name="day_button_circle_white"
+ android:alpha="1.0"
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:name="path"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M 0.0,8.0 c -4.41999816895,0.0 -8.0,-3.58200073242 -8.0,-8.0 c 0.0,-4.41799926758 3.58000183105,-8.0 8.0,-8.0 c 4.41999816895,0.0 8.0,3.58200073242 8.0,8.0 c 0.0,4.41799926758 -3.58000183105,8.0 -8.0,8.0 Z"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/drawable-v21/horizontal_divider.xml b/res/drawable-v21/horizontal_divider.xml
deleted file mode 100644
index 7db71f3..0000000
--- a/res/drawable-v21/horizontal_divider.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
- android:insetLeft="@dimen/notification_icon_size">
- <shape>
- <size android:height="1dip" />
- <solid android:color="@color/notification_divider" />
- </shape>
-</inset>
\ No newline at end of file
diff --git a/res/drawable-v21/notification_background.xml b/res/drawable-v21/notification_background.xml
deleted file mode 100644
index bf828df..0000000
--- a/res/drawable-v21/notification_background.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/control_highlight_color">
- <item android:id="@android:id/mask">
- <color android:color="@android:color/white"/>
- </item>
-</ripple>
\ No newline at end of file
diff --git a/res/drawable-v21/notification_icon_bg.xml b/res/drawable-v21/notification_icon_bg.xml
deleted file mode 100644
index 08b491c..0000000
--- a/res/drawable-v21/notification_icon_bg.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
- android:gravity="center"
- android:src="@drawable/bg_gray_circle"/>
diff --git a/res/drawable-v21/toggle_circle.xml b/res/drawable-v21/toggle_circle.xml
index c3ca5c7..ed6a72e 100644
--- a/res/drawable-v21/toggle_circle.xml
+++ b/res/drawable-v21/toggle_circle.xml
@@ -14,18 +14,20 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
-
-<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/clock_white">
- <item>
- <bitmap
- android:src="@drawable/bg_day_selected"
- android:tint="@color/bg_day_tint_color"
- android:gravity="center" />
- </item>
- <item android:id="@android:id/mask">
- <bitmap
- android:src="@drawable/bg_day_selected"
- android:gravity="center" />
- </item>
-</ripple>
\ No newline at end of file
+<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/checked"
+ android:drawable="@drawable/bg_day_button_white"
+ android:state_checked="true" />
+ <item
+ android:id="@+id/unchecked"
+ android:drawable="@drawable/bg_day_button_transparent" />
+ <transition
+ android:drawable="@drawable/bg_day_button_unchecked_to_checked_anim"
+ android:fromId="@+id/unchecked"
+ android:toId="@+id/checked" />
+ <transition
+ android:drawable="@drawable/bg_day_button_checked_to_unchecked_anim"
+ android:fromId="@+id/checked"
+ android:toId="@+id/unchecked" />
+</animated-selector>
\ No newline at end of file
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/drawable-v22/ic_caret_down_animation.xml
similarity index 63%
copy from res/drawable-v22/ic_lap_reset_animation.xml
copy to res/drawable-v22/ic_caret_down_animation.xml
index 041c9e1..9944de5 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/drawable-v22/ic_caret_down_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,14 +14,16 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
+ android:drawable="@drawable/ic_caret_down_static">
<target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
+ android:name="caret01"
+ android:animation="@animator/caret_expand_base_animation" />
<target
- android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
+ android:name="caret_l"
+ android:animation="@animator/caret_expand_l_animation" />
+ <target
+ android:name="caret_r"
+ android:animation="@animator/caret_expand_r_animation" />
</animated-vector>
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/drawable-v22/ic_caret_up_animation.xml
similarity index 63%
copy from res/drawable-v22/ic_lap_reset_animation.xml
copy to res/drawable-v22/ic_caret_up_animation.xml
index 041c9e1..f00f566 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/drawable-v22/ic_caret_up_animation.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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.
@@ -13,14 +14,16 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
+ android:drawable="@drawable/ic_caret_up_static">
<target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
+ android:name="caret02"
+ android:animation="@animator/caret_collapse_base_animation" />
<target
- android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
+ android:name="caret02_l"
+ android:animation="@animator/caret_collapse_l_animation" />
+ <target
+ android:name="caret02_r"
+ android:animation="@animator/caret_collapse_r_animation" />
</animated-vector>
diff --git a/res/drawable-v22/ic_lap_reset.xml b/res/drawable-v22/ic_lap_reset.xml
deleted file mode 100644
index 0a4495b..0000000
--- a/res/drawable-v22/ic_lap_reset.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="ic_lap_reset"
- android:width="36dp"
- android:viewportWidth="48"
- android:height="36dp"
- android:viewportHeight="48" >
- <group
- android:name="ic_refresh_white_48dp_outlines_0"
- android:translateX="26"
- android:translateY="24"
- android:rotation="-270" >
- <group
- android:name="ic_refresh_white_48dp_outlines_pivot_0"
- android:translateX="-26"
- android:translateY="-24" >
- <group
- android:name="group_2"
- android:translateX="23"
- android:translateY="24" >
- <path
- android:name="path_2"
- android:pathData="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 4.48883056641,-4.48062133789 4.4888458252,-4.48063659668 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -11.2804718018,11.2843780518 -11.2804718018,11.284362793 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 3.94947814941,-3.95066833496 3.94371032715,-3.94970703125 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:fillColor="#FFFFFFFF" />
- </group>
- </group>
- </group>
-</vector>
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/drawable-v22/ic_ringtone_active_animated.xml
similarity index 62%
copy from res/drawable-v22/ic_lap_reset_animation.xml
copy to res/drawable-v22/ic_ringtone_active_animated.xml
index 041c9e1..99f4a65 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/drawable-v22/ic_ringtone_active_animated.xml
@@ -16,11 +16,14 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
+ android:drawable="@drawable/ic_ringtone_active_static">
<target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
+ android:name="ic_ringtone_active_outlines_0"
+ android:animation="@animator/ic_ringtone_active_outlines_0_animation" />
<target
- android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
-</animated-vector>
+ android:name="ic_ringtone_active_outlines_1"
+ android:animation="@animator/ic_ringtone_active_outlines_1_animation" />
+ <target
+ android:name="ic_ringtone_active_outlines_2"
+ android:animation="@animator/ic_ringtone_active_outlines_2_animation" />
+</animated-vector>
\ No newline at end of file
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/drawable-v24/ic_pause_play_animation.xml
similarity index 70%
rename from res/drawable-v22/ic_lap_reset_animation.xml
rename to res/drawable-v24/ic_pause_play_animation.xml
index 041c9e1..1c6b938 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/drawable-v24/ic_pause_play_animation.xml
@@ -16,11 +16,14 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
+ android:drawable="@drawable/ic_play_pause">
<target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
+ android:name="path_0"
+ android:animation="@animator/ic_pause_play_path_0_animation" />
+ <target
+ android:name="path_1"
+ android:animation="@animator/ic_pause_play_path_1_animation" />
<target
android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
-</animated-vector>
+ android:animation="@animator/ic_pause_play_path_2_animation" />
+</animated-vector>
\ No newline at end of file
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/drawable-v24/ic_play_pause_animation.xml
similarity index 70%
copy from res/drawable-v22/ic_lap_reset_animation.xml
copy to res/drawable-v24/ic_play_pause_animation.xml
index 041c9e1..42eebe0 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/drawable-v24/ic_play_pause_animation.xml
@@ -16,11 +16,14 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
+ android:drawable="@drawable/ic_pause_play">
<target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
+ android:name="path_0"
+ android:animation="@animator/ic_play_pause_path_0_animation" />
+ <target
+ android:name="path_1"
+ android:animation="@animator/ic_play_pause_path_1_animation" />
<target
android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
-</animated-vector>
+ android:animation="@animator/ic_play_pause_path_2_animation" />
+</animated-vector>
\ No newline at end of file
diff --git a/res/drawable-v22/ic_lap_animation.xml b/res/drawable-v24/ic_stop_play_animation.xml
similarity index 76%
rename from res/drawable-v22/ic_lap_animation.xml
rename to res/drawable-v24/ic_stop_play_animation.xml
index 4ffa8cb..96b2842 100644
--- a/res/drawable-v22/ic_lap_animation.xml
+++ b/res/drawable-v24/ic_stop_play_animation.xml
@@ -16,11 +16,11 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap" >
+ android:drawable="@drawable/ic_stop_play">
<target
- android:name="ic_refresh_white_48dp_outlines"
- android:animation="@animator/ic_lap_ic_refresh_white_outlines_animation" />
+ android:name="path_0"
+ android:animation="@animator/ic_stop_play_ic_stop_play_path_0_animation" />
<target
android:name="path_1"
- android:animation="@animator/ic_lap_path_1_animation" />
+ android:animation="@animator/ic_stop_play_path_1_animation" />
</animated-vector>
diff --git a/res/drawable-xhdpi/bg_gray_circle.png b/res/drawable-xhdpi/bg_gray_circle.png
deleted file mode 100644
index 20aaa41..0000000
--- a/res/drawable-xhdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxhdpi/bg_gray_circle.png b/res/drawable-xxhdpi/bg_gray_circle.png
deleted file mode 100644
index a4528fb..0000000
--- a/res/drawable-xxhdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-xxxhdpi/bg_gray_circle.png b/res/drawable-xxxhdpi/bg_gray_circle.png
deleted file mode 100644
index 7d26b41..0000000
--- a/res/drawable-xxxhdpi/bg_gray_circle.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/bg_circle_accent.xml b/res/drawable/bg_circle_accent.xml
deleted file mode 100644
index a3c256d..0000000
--- a/res/drawable/bg_circle_accent.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 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.
- -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="oval">
- <solid android:color="@color/color_accent" />
-</shape>
diff --git a/res/drawable/divider.xml b/res/drawable/divider.xml
deleted file mode 100644
index d2eff5f..0000000
--- a/res/drawable/divider.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <size android:width="1dp" />
- <solid android:color="@color/notification_divider" />
-</shape>
\ No newline at end of file
diff --git a/res/drawable/fastscroll_preview.xml b/res/drawable/fastscroll_preview.xml
deleted file mode 100644
index 33424f6..0000000
--- a/res/drawable/fastscroll_preview.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
- android:insetRight="@dimen/fastscroll_preview_padding"
- android:insetLeft="@dimen/fastscroll_preview_padding">
- <shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <corners android:radius="44dp"/>
- <solid android:color="@color/color_accent" />
- </shape>
-</inset>
diff --git a/res/drawable/fastscroll_thumb.xml b/res/drawable/fastscroll_thumb.xml
deleted file mode 100644
index d57b079..0000000
--- a/res/drawable/fastscroll_thumb.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_pressed="true">
- <shape android:shape="rectangle">
- <solid android:color="@color/color_accent" />
- <size android:width="@dimen/fastscroll_thumb_width"
- android:height="@dimen/fastscroll_thumb_height" />
- </shape>
- </item>
- <item>
- <shape android:shape="rectangle">
- <solid android:color="#42ffffff" />
- <size android:width="@dimen/fastscroll_thumb_width"
- android:height="@dimen/fastscroll_thumb_height" />
- </shape>
- </item>
-</selector>
diff --git a/res/drawable/fastscroll_track.xml b/res/drawable/fastscroll_track.xml
deleted file mode 100644
index 81b6261..0000000
--- a/res/drawable/fastscroll_track.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_pressed="true">
- <shape android:shape="rectangle">
- <solid android:color="#1fffffff"/>
- <size android:width="@dimen/fastscroll_track_width" />
- </shape>
- </item>
- <item>
- <shape android:shape="rectangle">
- <solid android:color="@color/transparent"/>
- <size android:width="@dimen/fastscroll_track_width" />
- </shape>
- </item>
-</selector>
diff --git a/res/drawable/horizontal_divider.xml b/res/drawable/horizontal_divider.xml
deleted file mode 100644
index 430684b..0000000
--- a/res/drawable/horizontal_divider.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <size android:height="1dip" />
- <solid android:color="@color/notification_divider" />
-</shape>
\ No newline at end of file
diff --git a/res/drawable/ic_add_timer.xml b/res/drawable/ic_add_timer.xml
deleted file mode 100644
index 3f24a20..0000000
--- a/res/drawable/ic_add_timer.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32dp"
- android:height="32dp"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0">
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M23,11l-3,0l0,-3l-2,0l0,3l-3,0l0,2l3,0l0,3l2,0l0,-3l3,0z" />
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M11.9,12l5.9,-6.3C18,5.4 18.1,5 17.9,4.6C17.8,4.2 17.4,4 17,4H4C3.6,4 3.2,4.2 3.1,4.6C2.9,5 3,5.4 3.3,5.7L9.1,12l-5.9,6.3C3,18.6 2.9,19 3.1,19.4C3.2,19.8 3.6,20 4,20h13c0.4,0 0.8,-0.2 0.9,-0.6c0.2,-0.4 0.1,-0.8 -0.2,-1.1L11.9,12zM6.3,6h8.4l-4.2,4.5L6.3,6z" />
-</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_backspace.xml b/res/drawable/ic_backspace.xml
index 0bd63aa..b4a3040 100644
--- a/res/drawable/ic_backspace.xml
+++ b/res/drawable/ic_backspace.xml
@@ -1,26 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/backspace_icon_size"
- android:height="@dimen/backspace_icon_size"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0"
- android:tint="@color/backspace_tint_color">
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/backspace_icon_size"
+ android:height="@dimen/backspace_icon_size"
+ android:tint="?attr/colorControlNormal"
+ android:tintMode="src_in"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
<path
- android:fillColor="#FFFFFFFF"
- android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12 19,15.59z" />
-</vector>
\ No newline at end of file
+ android:fillColor="#FFF"
+ android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53
+ 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17
+ 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12
+ 19,15.59z" />
+</vector>
diff --git a/res/drawable/ic_caret_down_static.xml b/res/drawable/ic_caret_down_static.xml
new file mode 100644
index 0000000..c61c85c
--- /dev/null
+++ b/res/drawable/ic_caret_down_static.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="caret_toclose"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <group
+ android:name="caret01"
+ android:rotation="90"
+ android:translateX="12"
+ android:translateY="15">
+ <group
+ android:name="caret_l"
+ android:rotation="45">
+ <group
+ android:name="caret_l_pivot"
+ android:translateY="4">
+ <group
+ android:name="caret_l_rect_position"
+ android:translateY="-1">
+ <path
+ android:name="caret_l_rect"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -1.0,-4.0 l 2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l -2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,-8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 Z"/>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="caret_r"
+ android:rotation="-45">
+ <group
+ android:name="caret_r_pivot"
+ android:translateY="-4">
+ <group
+ android:name="caret_r_rect_position"
+ android:translateY="1">
+ <path
+ android:name="caret_r_rect"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -1.0,-4.0 l 2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l -2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,-8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 Z"/>
+ </group>
+ </group>
+ </group>
+ </group>
+</vector>
diff --git a/res/drawable/ic_caret_up_static.xml b/res/drawable/ic_caret_up_static.xml
new file mode 100644
index 0000000..8eaa80e
--- /dev/null
+++ b/res/drawable/ic_caret_up_static.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+-->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="caret_up"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <group
+ android:name="caret02"
+ android:rotation="90"
+ android:translateX="12"
+ android:translateY="9">
+ <group
+ android:name="caret02_l"
+ android:rotation="-45">
+ <group
+ android:name="caret02_l_pivot"
+ android:translateY="4">
+ <group
+ android:name="caret02_l_rect_position"
+ android:translateY="-1">
+ <path
+ android:name="caret02_l_rect"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -1.0,-4.0 l 2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l -2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,-8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 Z"/>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="caret02_r"
+ android:rotation="45">
+ <group
+ android:name="caret02_r_pivot"
+ android:translateY="-4">
+ <group
+ android:name="caret02_r_rect_position"
+ android:translateY="1">
+ <path
+ android:name="caret02_r_rect"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -1.0,-4.0 l 2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l -2.0,0.0 c 0.0,0.0 0.0,0.0 0.0,0.0 l 0.0,-8.0 c 0.0,0.0 0.0,0.0 0.0,0.0 Z"/>
+ </group>
+ </group>
+ </group>
+ </group>
+</vector>
diff --git a/res/drawable/ic_checkmark.xml b/res/drawable/ic_checkmark.xml
new file mode 100644
index 0000000..ed5c32d
--- /dev/null
+++ b/res/drawable/ic_checkmark.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="#FFF"
+ android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
+</vector>
diff --git a/res/drawable/ic_close.xml b/res/drawable/ic_close.xml
deleted file mode 100644
index 7fca293..0000000
--- a/res/drawable/ic_close.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32dp"
- android:height="32dp"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0">
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
-</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_delete.xml b/res/drawable/ic_delete.xml
deleted file mode 100644
index 14224bc..0000000
--- a/res/drawable/ic_delete.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32dp"
- android:height="32dp"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0">
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
-</vector>
\ No newline at end of file
diff --git a/res/drawable-v22/ic_reset_lap_animation.xml b/res/drawable/ic_label.xml
similarity index 67%
rename from res/drawable-v22/ic_reset_lap_animation.xml
rename to res/drawable/ic_label.xml
index c34cb37..ec330fd 100644
--- a/res/drawable-v22/ic_reset_lap_animation.xml
+++ b/res/drawable/ic_label.xml
@@ -14,13 +14,13 @@
limitations under the License.
-->
-<animated-vector
+<vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_reset" >
- <target
- android:name="ic_refresh_white_48dp_outlines_1"
- android:animation="@animator/ic_reset_lap_ic_refresh_white_outlines_1_animation" />
- <target
- android:name="path_3"
- android:animation="@animator/ic_reset_lap_path_3_animation" />
-</animated-vector>
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="#FFFFFF"
+ android:pathData="M17.63,5.84C17.27,5.33 16.67,5 16,5L5,5.01C3.9,5.01 3,5.9 3,7v10c0,1.1 0.9,1.99 2,1.99L16,19c0.67,0 1.27,-0.33 1.63,-0.84L22,12l-4.37,-6.16z"/>
+</vector>
diff --git a/res/drawable/ic_lap.xml b/res/drawable/ic_lap.xml
deleted file mode 100644
index 1578b93..0000000
--- a/res/drawable/ic_lap.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="ic_lap"
- android:width="36dp"
- android:viewportWidth="48"
- android:height="36dp"
- android:viewportHeight="48" >
- <group
- android:name="ic_refresh_white_48dp_outlines"
- android:translateX="26"
- android:translateY="24"
- android:rotation="-270" >
- <group
- android:name="ic_refresh_white_48dp_outlines_pivot"
- android:translateX="-26"
- android:translateY="-24" >
- <group
- android:name="group_1"
- android:translateX="23"
- android:translateY="24" >
- <path
- android:name="path_1"
- android:pathData="M 3.0,-17.9921875 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,0.0 2.00430297852,-0.00273132324219 2.00430297852,-0.00273132324219 c 0.0,0.0 0.00115966796875,0.00117492675781 0.0011596679688,0.00117492675781 c 0.0,0.0 1.99844360352,0.00155639648438 1.99844360352,0.00155639648438 c 0.0,0.0 -0.001953125,0.0 -0.001953125,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.16735839844,0.0 -6.05722045898,-1.06898498535 -8.37550354004,-2.80465698242 c 0.00050354003906,-0.00198364257812 4.48883056641,-4.48062133789 4.4888458252,-4.48063659668 c 0.0,0.0 -2.83171081543,-2.82643127441 -2.83169555664,-2.82641601562 c 0.0,0.00002 -11.2804718018,11.2843780518 -11.2804718018,11.284362793 c 0.0,0.0 2.82472229004,2.82685852051 2.82473754883,2.82684326172 c 0.0,0.0 3.94947814941,-3.95066833496 3.94371032715,-3.94970703125 c 3.0442199707,2.48754882813 6.95446777344,3.95021057129 11.2303771973,3.95021057129 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:fillColor="#FFFFFFFF" />
- </group>
- </group>
- </group>
-</vector>
diff --git a/res/drawable/ic_pause_play.xml b/res/drawable/ic_pause_play.xml
new file mode 100644
index 0000000..52a1f39
--- /dev/null
+++ b/res/drawable/ic_pause_play.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="play_pause"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportHeight="48"
+ android:viewportWidth="48">
+ <group
+ android:name="path_0"
+ android:rotation="90"
+ android:translateX="24"
+ android:translateY="24">
+ <group
+ android:name="shape_layer_2">
+ <group
+ android:name="shape_1">
+ <path
+ android:name="path_1"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M 0.0,-14.0625 c 0.0,0.0 0.0,0.0 0.0,0.0 c 0.0,0.0 -14.0625,22.0625 -14.0625,22.0625 c 0.0,0.0 14.0625,0.0 14.0625,0.0 c 0.0,0.0 0.0,-22.0625 0.0,-22.0625 Z"
+ android:strokeColor="#FFFFFFFF"
+ android:strokeWidth="0" />
+ </group>
+ </group>
+ <group
+ android:name="shape_layer_3"
+ android:translateX="4">
+ <group
+ android:name="shape_layer_3_pivot"
+ android:translateX="11.9375">
+ <group
+ android:name="shape_2">
+ <path
+ android:name="path_2"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -15.9375,-14.0625 c 0.0,0.0 0.0,0.0 0.0,0.0 c 0.0,0.0 0.0,22.0625 0.0,22.0625 c 0.0,0.0 14.0625,0.0 14.0625,0.0 c 0.0,0.0 -14.0625,-22.0625 -14.0625,-22.0625 Z"
+ android:strokeColor="#FFFFFFFF"
+ android:strokeWidth="0" />
+ </group>
+ </group>
+ </group>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_play_pause.xml b/res/drawable/ic_play_pause.xml
new file mode 100644
index 0000000..437ca92
--- /dev/null
+++ b/res/drawable/ic_play_pause.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="pause_play"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportHeight="48"
+ android:viewportWidth="48">
+ <group
+ android:name="path_0"
+ android:translateX="24"
+ android:translateY="24">
+ <group
+ android:name="shape_layer_2">
+ <group
+ android:name="shape_1">
+ <path
+ android:name="path_1"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -3.9375,-14.0625 c 0.0,0.0 -8.0625,0.0 -8.0625,0.0 c 0.0,0.0 0.0,28.125 0.0,28.125 c 0.0,0.0 8.0625,0.0 8.0625,0.0 c 0.0,0.0 0.0,-28.125 0.0,-28.125 Z"
+ android:strokeColor="#FFFFFFFF"
+ android:strokeWidth="0" />
+ </group>
+ </group>
+ <group
+ android:name="shape_layer_3"
+ android:translateX="4">
+ <group
+ android:name="shape_layer_3_pivot"
+ android:translateX="11.9375">
+ <group
+ android:name="shape_2">
+ <path
+ android:name="path_2"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -3.9375,-14.0625 c 0.0,0.0 -8.0625,0.0 -8.0625,0.0 c 0.0,0.0 0.0,28.125 0.0,28.125 c 0.0,0.0 8.0625,0.0 8.0625,0.0 c 0.0,0.0 0.0,-28.125 0.0,-28.125 Z"
+ android:strokeColor="#FFFFFFFF"
+ android:strokeWidth="0" />
+ </group>
+ </group>
+ </group>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_plusone.xml b/res/drawable/ic_plusone.xml
deleted file mode 100644
index 628bf7e..0000000
--- a/res/drawable/ic_plusone.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32dp"
- android:height="32dp"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0">
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M-31.5,-0.4h24v24h-24V-0.4zM-31.5,-0.4h24v24h-24V-0.4z" />
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M7.9,7h-2v4h-4v2h4v4h2v-4h4v-2h-4V7zM17.9,18h-2V7.4l-3,1V6.7L17.6,5h0.3V18z" />
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M20,3h2v5h-2z" />
-</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_reset.xml b/res/drawable/ic_reset.xml
deleted file mode 100644
index a60f159..0000000
--- a/res/drawable/ic_reset.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="ic_reset_lap"
- android:width="36dp"
- android:viewportWidth="48"
- android:height="36dp"
- android:viewportHeight="48" >
- <group
- android:name="ic_refresh_white_48dp_outlines_1"
- android:translateX="26"
- android:translateY="24" >
- <group
- android:name="ic_refresh_white_48dp_outlines_pivot_1"
- android:translateX="-26"
- android:translateY="-24" >
- <group
- android:name="group_3"
- android:translateX="23"
- android:translateY="24" >
- <path
- android:name="path_3"
- android:pathData="M 3.0,-18.0 c -9.89999389648,0.0 -18.0,8.10000610352 -18.0,18.0 c 0.0,0.0 -6.0,0.0 -6.0,0.0 c 0.0,0.0 7.80000305176,7.80000305176 7.80000305176,7.80000305176 c 0.0,0.0 0.0999908447266,0.300003051758 0.0999908447266,0.300003051758 c 0.0,0.0 8.10000610352,-8.10000610352 8.10000610352,-8.10000610352 c 0.0,0.0 -6.0,0.0 -6.0,0.0 c 0.0,-7.69999694824 6.30000305176,-14.0 14.0,-14.0 c 7.69999694824,0.0 14.0,6.30000305176 14.0,14.0 c 0.0,7.69999694824 -6.30000305176,14.0 -14.0,14.0 c -3.19044494629,0.0 -6.11320495605,-1.07075500488 -8.43977355957,-2.82905578613 c -0.0890655517578,-0.0673065185547 -0.668563842773,-0.534942626953 -0.862991333008,-0.707794189453 c -0.204864501953,-0.18212890625 -0.404037475586,-0.36994934082 -0.597229003906,-0.563140869141 c 0.0,0.0 -2.80000305176,2.79998779297 -2.80000305176,2.79998779297 c 0.247512817383,0.255249023438 0.502792358398,0.502716064453 0.765487670898,0.742080688477 c 0.103759765625,0.0945434570312 0.547332763672,0.478912353516 0.665634155273,0.576156616211 c 3.04972839355,2.50672912598 6.97491455078,3.98176574707 11.2688751221,3.98176574707 c 10.0,0.0 18.0,-8.09999084473 18.0,-18.0 c 0.0,-9.89999389648 -8.10000610352,-18.0 -18.0,-18.0 Z"
- android:fillColor="#FFFFFFFF" />
- </group>
- </group>
- </group>
-</vector>
diff --git a/res/drawable/ic_ringtone_active_static.xml b/res/drawable/ic_ringtone_active_static.xml
new file mode 100644
index 0000000..ff3fb8e
--- /dev/null
+++ b/res/drawable/ic_ringtone_active_static.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="ic_ringtone_active_static"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <group
+ android:name="ic_ringtone_active_outlines_0"
+ android:rotation="12"
+ android:translateX="12"
+ android:translateY="5.99219">
+ <group
+ android:name="ic_ringtone_active_outlines_0_pivot"
+ android:translateX="-12.00098"
+ android:translateY="-5.98438">
+ <group
+ android:name="group_1"
+ android:translateX="12"
+ android:translateY="12.25">
+ <path
+ android:name="path_2"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M 6.0,-1.25 c 0.0,-3.07000732422 -1.63999938965,-5.63999938965 -4.5,-6.32000732422 c 0.0,0.0 0.0,-0.679992675781 0.0,-0.679992675781 c 0.0,-0.830001831055 -0.669998168945,-1.5 -1.5,-1.5 c -0.830001831055,0.0 -1.5,0.669998168945 -1.5,1.5 c 0.0,0.0 0.0,0.679992675781 0.0,0.679992675781 c -2.86999511719,0.68000793457 -4.5,3.24000549316 -4.5,6.32000732422 c 0.0,0.0 0.0,5.0 0.0,5.0 c 0.0,0.0 -2.0,2.0 -2.0,2.0 c 0.0,0.0 0.0,1.0 0.0,1.0 c 0.0,0.0 16.0,0.0 16.0,0.0 c 0.0,0.0 0.0,-1.0 0.0,-1.0 c 0.0,0.0 -2.0,-2.0 -2.0,-2.0 c 0.0,0.0 0.0,-5.0 0.0,-5.0 Z" />
+ </group>
+ <group
+ android:name="ic_ringtone_active_outlines_1"
+ android:rotation="-8"
+ android:translateX="12.00098"
+ android:translateY="9.99219">
+ <group
+ android:name="ic_ringtone_active_outlines_1_pivot"
+ android:translateX="-12.00098"
+ android:translateY="-9.99219">
+ <group
+ android:name="group_2"
+ android:translateX="12"
+ android:translateY="12.25">
+ <path
+ android:name="path_4_merged"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -4.41999816895,-8.16999816895 c 0.0,0.0 -1.43000793457,-1.43000793457 -1.43000793457,-1.43000793457 c -2.39999389648,1.83000183105 -3.97999572754,4.65000915527 -4.11999511719,7.85000610352 c 0.0,0.0 2.0,0.0 2.0,0.0 c 0.149993896484,-2.64999389648 1.50999450684,-4.9700012207 3.55000305176,-6.41999816895 Z M 7.9700012207,-1.75 c 0.0,0.0 2.0,0.0 2.0,0.0 c -0.149993896484,-3.19999694824 -1.72999572754,-6.02000427246 -4.11999511719,-7.85000610352 c 0.0,0.0 -1.42001342773,1.43000793457 -1.42001342773,1.43000793457 c 2.02000427246,1.44999694824 3.39001464844,3.77000427246 3.54000854492,6.41999816895 Z" />
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="ic_ringtone_active_outlines_2">
+ <group
+ android:name="ic_ringtone_active_outlines_2_pivot"
+ android:translateX="0"
+ android:translateY="0">
+ <group
+ android:name="group_3"
+ android:translateX="12"
+ android:translateY="12.25">
+ <path
+ android:name="path_1"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M 0.0,9.75 c 0.139999389648,0.0 0.270004272461,-0.00999450683594 0.399993896484,-0.0399932861328 c 0.650009155273,-0.139999389648 1.18000793457,-0.580001831055 1.44000244141,-1.18000793457 c 0.100006103516,-0.239990234375 0.150009155273,-0.5 0.150009155273,-0.779998779297 c 0.0,0.0 -4.0,0.0 -4.0,0.0 c 0.00999450683594,1.10000610352 0.899993896484,2.0 2.00999450684,2.0 Z" />
+ </group>
+ </group>
+ </group>
+ </group>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_expand_more.xml b/res/drawable/ic_ringtone_not_found.xml
similarity index 72%
rename from res/drawable/ic_expand_more.xml
rename to res/drawable/ic_ringtone_not_found.xml
index 79bc069..74a4241 100644
--- a/res/drawable/ic_expand_more.xml
+++ b/res/drawable/ic_ringtone_not_found.xml
@@ -15,11 +15,11 @@
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
<path
- android:fillColor="#FFFFFFFF"
- android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
-</vector>
\ No newline at end of file
+ android:fillColor="#FFF"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
+</vector>
diff --git a/res/drawable/ic_share.xml b/res/drawable/ic_share.xml
deleted file mode 100644
index b6f7ae7..0000000
--- a/res/drawable/ic_share.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="32dp"
- android:height="32dp"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0">
- <path
- android:fillColor="#FFFFFFFF"
- android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
-</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_stop_play.xml b/res/drawable/ic_stop_play.xml
new file mode 100644
index 0000000..662edf4
--- /dev/null
+++ b/res/drawable/ic_stop_play.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="ic_stop_play"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <group
+ android:name="path_0"
+ android:translateX="12"
+ android:translateY="12">
+ <group
+ android:name="path_0_pivot"
+ android:translateX="-12"
+ android:translateY="-12">
+ <group
+ android:name="group_1"
+ android:translateX="12"
+ android:translateY="12">
+ <path
+ android:name="path_1"
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M -6.0,-6.0 c 0.0,0.0 12.0,0.0 12.0,0.0 c 0.0,0.0 0.0,12.0 0.0,12.0 c 0.0,0.0 -12.0,0.0 -12.0,0.0 c 0.0,0.0 0.0,-12.0 0.0,-12.0 Z" />
+ </group>
+ </group>
+ </group>
+</vector>
diff --git a/res/drawable/notification_background.xml b/res/drawable/notification_background.xml
deleted file mode 100644
index 4af44fb..0000000
--- a/res/drawable/notification_background.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@color/notification_bg"/>
-
- <item>
- <selector>
- <item android:state_pressed="true"
- android:drawable="@color/notification_pressed"/>
- <item android:drawable="@color/transparent"/>
- </selector>
- </item>
-
-</layer-list>
diff --git a/res/drawable/notification_icon_bg.xml b/res/drawable/notification_icon_bg.xml
deleted file mode 100644
index 1d044af..0000000
--- a/res/drawable/notification_icon_bg.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<color xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/notification_icon_bg" />
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/drawable/placeholder_album_artwork.xml
similarity index 64%
copy from res/drawable-v22/ic_lap_reset_animation.xml
copy to res/drawable/placeholder_album_artwork.xml
index 041c9e1..681b734 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/drawable/placeholder_album_artwork.xml
@@ -14,13 +14,13 @@
limitations under the License.
-->
-<animated-vector
+<vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
- <target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
- <target
- android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
-</animated-vector>
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportHeight="40.0"
+ android:viewportWidth="40.0">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M38,0H2C0.9,0 0,0.9 0,2v36c0,1.1 0.9,2 2,2h36c1.1,0 2,-0.9 2,-2V2C40,0.9 39.1,0 38,0zM26,15h-4v10c0,2.2 -1.8,4 -4,4s-4,-1.8 -4,-4c0,-2.2 1.8,-4 4,-4c0.7,0 1.4,0.2 2,0.5V11h6V15z" />
+</vector>
\ No newline at end of file
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_0.xml b/res/interpolator-v22/ic_lap_animation_interpolator_0.xml
deleted file mode 100644
index e2cf623..0000000
--- a/res/interpolator-v22/ic_lap_animation_interpolator_0.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<pathInterpolator
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.4,0.0 0.66666667,1.0 1.0,1.0" />
diff --git a/res/interpolator-v22/ic_lap_reset_animation_interpolator_0.xml b/res/interpolator-v22/ic_lap_reset_animation_interpolator_0.xml
deleted file mode 100644
index d3728c4..0000000
--- a/res/interpolator-v22/ic_lap_reset_animation_interpolator_0.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<pathInterpolator
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.4,0.0 0.4,1.0 1.0,1.0" />
diff --git a/res/interpolator-v22/ic_reset_lap_animation_interpolator_0.xml b/res/interpolator-v22/ic_reset_lap_animation_interpolator_0.xml
deleted file mode 100644
index d3728c4..0000000
--- a/res/interpolator-v22/ic_reset_lap_animation_interpolator_0.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<pathInterpolator
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.4,0.0 0.4,1.0 1.0,1.0" />
diff --git a/res/interpolator-v22/ic_reset_lap_animation_interpolator_1.xml b/res/interpolator-v22/ic_reset_lap_animation_interpolator_1.xml
deleted file mode 100644
index 114006d..0000000
--- a/res/interpolator-v22/ic_reset_lap_animation_interpolator_1.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<pathInterpolator
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
diff --git a/res/layout-h320dp-port/stopwatch_view.xml b/res/layout-h320dp-port/stopwatch_view.xml
deleted file mode 100644
index e899d79..0000000
--- a/res/layout-h320dp-port/stopwatch_view.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<!-- Sufficient space exists to include the bounding stopwatch circle. -->
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-
- <com.android.deskclock.timer.CountingTimerView
- android:id="@+id/stopwatch_time_text"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
- <com.android.deskclock.stopwatch.StopwatchCircleView
- android:id="@+id/stopwatch_time"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@null" />
-
-</merge>
diff --git a/res/layout-land/clock_fragment.xml b/res/layout-land/clock_fragment.xml
index 91eff5f..b9bb893 100644
--- a/res/layout-land/clock_fragment.xml
+++ b/res/layout-land/clock_fragment.xml
@@ -59,6 +59,7 @@
android:clickable="false"
android:clipToPadding="false"
android:paddingBottom="@dimen/fab_height"
+ android:paddingTop="16dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />
diff --git a/res/layout-land/main_clock_frame.xml b/res/layout-land/main_clock_frame.xml
index a6b208a..b72f623 100644
--- a/res/layout-land/main_clock_frame.xml
+++ b/res/layout-land/main_clock_frame.xml
@@ -29,22 +29,20 @@
android:id="@+id/analog_clock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/bottom_text_spacing_analog"
android:layout_marginEnd="@dimen/analog_clock_margin"
android:layout_marginLeft="@dimen/analog_clock_margin"
android:layout_marginRight="@dimen/analog_clock_margin"
android:layout_marginStart="@dimen/analog_clock_margin"
android:layout_marginTop="@dimen/circle_margin_top" />
- <TextClock
+ <com.android.deskclock.widget.AutoSizingTextClock
android:id="@+id/digital_clock"
- style="@style/big_thin"
+ style="@style/display_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="none"
android:singleLine="true"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/main_clock_font_size" />
+ android:textSize="@dimen/main_clock_digital_font_size" />
</FrameLayout>
diff --git a/res/layout-land/stopwatch_fragment.xml b/res/layout-land/stopwatch_fragment.xml
index 5f72907..103c6dc 100644
--- a/res/layout-land/stopwatch_fragment.xml
+++ b/res/layout-land/stopwatch_fragment.xml
@@ -31,19 +31,18 @@
<com.android.deskclock.stopwatch.StopwatchLandscapeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
- android:layout_weight="@integer/guttered_content_width_percent">
+ android:layout_weight="@integer/guttered_content_width_percent"
+ android:paddingBottom="@dimen/fab_height">
- <!-- A circle in the stopwatch_view implies it should be sized in the remaining area. -->
- <com.android.deskclock.TimerCircleFrameLayout
- android:id="@+id/stopwatch"
+ <FrameLayout
+ android:id="@+id/stopwatch_time_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:paddingBottom="@dimen/fab_height">
+ android:layout_gravity="center">
- <include layout="@layout/stopwatch_view" />
+ <include layout="@layout/stopwatch_time" />
- </com.android.deskclock.TimerCircleFrameLayout>
+ </FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/laps_list"
@@ -60,4 +59,4 @@
android:layout_height="match_parent"
android:layout_weight="@integer/gutter_width_percent" />
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/layout-land/time_setup_view.xml b/res/layout-land/time_setup_view.xml
deleted file mode 100644
index 37404a5..0000000
--- a/res/layout-land/time_setup_view.xml
+++ /dev/null
@@ -1,88 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:baselineAligned="false"
- android:orientation="horizontal">
-
- <!-- This nested LTR layout cannot be combined with the parent because
- in RTL, the Keypad and Timer value should swap. -->
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="3"
- android:layoutDirection="ltr"
- android:gravity="center"
- android:orientation="vertical"
- android:paddingBottom="@dimen/fab_height">
-
- <com.android.deskclock.timer.TimerView
- android:id="@+id/timer_time_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:baselineAligned="true">
-
- <include layout="@layout/timer_h_mm_ss_view" />
-
- <ImageButton
- android:id="@+id/delete"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:layout_marginStart="@dimen/timer_setup_delete_margin"
- android:layout_marginEnd="@dimen/timer_setup_delete_margin"
- android:padding="@dimen/timer_setup_delete_padding"
- android:contentDescription="@string/timer_delete"
- android:scaleType="center"
- app:srcCompat="@drawable/ic_backspace" />
-
- </com.android.deskclock.timer.TimerView>
-
- <View
- android:id="@+id/divider"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginBottom="8dp"
- android:background="@color/dialog_gray" />
-
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="2"
- android:orientation="vertical">
-
- <include
- android:id="@+id/first"
- layout="@layout/three_keys_view" />
- <include
- android:id="@+id/second"
- layout="@layout/three_keys_view" />
- <include
- android:id="@+id/third"
- layout="@layout/three_keys_view" />
- <include
- android:id="@+id/fourth"
- layout="@layout/three_keys_view" />
-
- </LinearLayout>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-land/timer_setup_view.xml b/res/layout-land/timer_setup_view.xml
new file mode 100644
index 0000000..151a376
--- /dev/null
+++ b/res/layout-land/timer_setup_view.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="3"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/fab_height">
+
+ <include layout="@layout/timer_setup_time" />
+
+ <include layout="@layout/timer_setup_divider" />
+
+ </LinearLayout>
+
+ <include
+ layout="@layout/timer_setup_digits"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="2" />
+
+</LinearLayout>
diff --git a/res/layout-land/world_clock_item.xml b/res/layout-land/world_clock_item.xml
index 15683f9..e5e9398 100644
--- a/res/layout-land/world_clock_item.xml
+++ b/res/layout-land/world_clock_item.xml
@@ -13,6 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
@@ -24,62 +25,29 @@
android:layout_weight="29"
android:orientation="vertical">
- <FrameLayout
+ <TextClock
+ android:id="@+id/digital_clock"
+ style="@style/world_clock_time"
android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextClock
- android:id="@+id/digital_clock"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:fontFamily="sans-serif-thin"
- android:gravity="center"
- android:includeFontPadding="false"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/world_clock_digital_font_size" />
-
- <com.android.deskclock.AnalogClock
- android:id="@+id/analog_clock"
- android:layout_width="@dimen/world_clock_analog_size"
- android:layout_height="@dimen/world_clock_analog_size"
- android:layout_gravity="center_horizontal"
- android:layout_marginBottom="@dimen/bottom_text_spacing_analog_small"
- android:gravity="center" />
-
- </FrameLayout>
-
- <LinearLayout
- android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:gravity="center">
+ android:gravity="center_horizontal"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:includeFontPadding="false" />
- <FrameLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1">
+ <com.android.deskclock.AnalogClock
+ android:id="@+id/analog_clock"
+ android:layout_width="@dimen/world_clock_analog_size"
+ android:layout_height="@dimen/world_clock_analog_size"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="@dimen/bottom_text_spacing_analog_small" />
- <TextView
- android:id="@+id/city_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:singleLine="true"
- android:textAppearance="@style/PrimaryLabelTextAppearance" />
-
- </FrameLayout>
-
- <TextView
- android:id="@+id/city_day"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/style_label_space"
- android:ellipsize="none"
- android:singleLine="true"
- android:textAppearance="@style/SecondaryLabelTextAppearance" />
-
- </LinearLayout>
+ <include
+ layout="@layout/world_clock_city_container"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:layout_gravity="center_horizontal"
+ android:layout_weight="1" />
</LinearLayout>
diff --git a/res/layout-v21/stopwatch_notif_expanded.xml b/res/layout-v21/stopwatch_notif_expanded.xml
deleted file mode 100644
index 7ef36b4..0000000
--- a/res/layout-v21/stopwatch_notif_expanded.xml
+++ /dev/null
@@ -1,108 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:divider="@drawable/horizontal_divider"
- android:orientation="vertical"
- android:showDividers="middle">
-
- <FrameLayout
- android:id="@+id/swn_expanded_hitspace"
- android:layout_width="match_parent"
- android:layout_height="64dp"
- android:background="@drawable/notification_background">
-
- <include layout="@layout/stopwatch_notification_icon" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="fill_vertical"
- android:layout_marginStart="@dimen/notification_icon_size"
- android:gravity="center"
- android:minHeight="@dimen/notification_icon_size"
- android:orientation="vertical"
- android:paddingBottom="2dp"
- android:paddingEnd="8dp"
- android:paddingTop="2dp">
-
- <LinearLayout
- android:id="@+id/line1"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <Chronometer
- android:id="@+id/swn_expanded_chronometer"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title" />
- </LinearLayout>
-
- <TextView
- android:id="@+id/swn_expanded_laps"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="marquee"
- android:fadingEdge="horizontal"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
- android:visibility="gone" />
-
- </LinearLayout>
-
- </FrameLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/notification_icon_size"
- android:divider="@drawable/divider"
- android:orientation="horizontal" >
-
- <TextView
- android:id="@+id/swn_left_button"
- android:layout_width="0dp"
- android:layout_height="48dp"
- android:layout_weight="1"
- android:background="@drawable/notification_background"
- android:drawablePadding="8dp"
- android:ellipsize="end"
- android:gravity="start|center_vertical"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Action" />
-
- <TextView
- android:id="@+id/swn_right_button"
- android:layout_width="0dp"
- android:layout_height="48dp"
- android:layout_weight="1"
- android:background="@drawable/notification_background"
- android:drawablePadding="8dp"
- android:ellipsize="end"
- android:gravity="start|center_vertical"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Action" />
-
- </LinearLayout>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-w320dp-h320dp-land/stopwatch_fragment.xml b/res/layout-w320dp-h320dp-land/stopwatch_fragment.xml
new file mode 100644
index 0000000..14c1327
--- /dev/null
+++ b/res/layout-w320dp-h320dp-land/stopwatch_fragment.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:baselineAligned="false"
+ android:orientation="horizontal">
+
+ <!-- Left gutter. -->
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/gutter_width_percent" />
+
+ <!-- Guttered content. -->
+ <com.android.deskclock.stopwatch.StopwatchLandscapeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/guttered_content_width_percent">
+
+ <com.android.deskclock.TimerCircleFrameLayout
+ android:id="@+id/stopwatch_time_wrapper"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:paddingBottom="@dimen/fab_height">
+
+ <include layout="@layout/stopwatch_time" />
+
+ <!-- Sufficient space exists to include the bounding stopwatch circle. -->
+ <com.android.deskclock.stopwatch.StopwatchCircleView
+ android:id="@+id/stopwatch_circle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </com.android.deskclock.TimerCircleFrameLayout>
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/laps_list"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:paddingBottom="@dimen/fab_height" />
+
+ </com.android.deskclock.stopwatch.StopwatchLandscapeLayout>
+
+ <!-- Right gutter. -->
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/gutter_width_percent" />
+
+</LinearLayout>
diff --git a/res/layout-w320dp-h320dp/timer_item.xml b/res/layout-w320dp-h320dp/timer_item.xml
index 3be4cfe..07e8037 100644
--- a/res/layout-w320dp-h320dp/timer_item.xml
+++ b/res/layout-w320dp-h320dp/timer_item.xml
@@ -25,17 +25,15 @@
<android.support.percent.PercentFrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_weight="1"
- android:layout_gravity="center">
+ android:layout_weight="1">
<com.android.deskclock.TimerCircleFrameLayout
+ android:layout_gravity="center"
app:layout_aspectRatio="100%"
- app:layout_widthPercent="@fraction/timer_circle_width_percent"
app:layout_heightPercent="@fraction/timer_circle_height_percent"
- android:layout_gravity="center">
+ app:layout_widthPercent="@fraction/timer_circle_width_percent">
<com.android.deskclock.CircleButtonsLayout
- android:id="@+id/timer_circle"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -44,34 +42,42 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
- <com.android.deskclock.timer.CountingTimerView
+ <com.android.deskclock.widget.AutoSizingTextView
android:id="@+id/timer_time_text"
+ style="@style/display_time"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:includeFontPadding="false"
+ android:paddingEnd="20dp"
+ android:paddingStart="20dp"
+ android:textSize="70sp" />
<Button
android:id="@+id/timer_label"
- style="?android:attr/borderlessButtonStyle"
+ style="?attr/borderlessButtonStyle"
android:layout_width="wrap_content"
- android:layout_height="40dp"
+ android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal"
android:clickable="false"
android:ellipsize="end"
android:gravity="center"
android:hint="@string/label"
android:maxLines="1"
+ android:minHeight="@dimen/touch_target_min_size"
+ android:minWidth="@dimen/touch_target_min_size"
android:textAppearance="@style/SecondaryLabelTextAppearance" />
- <ImageButton
+ <Button
android:id="@+id/reset_add"
- android:layout_width="@dimen/fab_button_size"
- android:layout_height="@dimen/fab_button_size"
+ style="?attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:contentDescription="@string/timer_plus_one"
android:gravity="center"
- android:padding="@dimen/fab_button_padding"
- android:scaleType="centerInside"
- app:srcCompat="@drawable/ic_plusone" />
+ android:scaleType="centerInside" />
</com.android.deskclock.CircleButtonsLayout>
diff --git a/res/layout-w420dp-land/stopwatch_view.xml b/res/layout-w420dp-land/stopwatch_view.xml
deleted file mode 100644
index 5a75818..0000000
--- a/res/layout-w420dp-land/stopwatch_view.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 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.
--->
-
-<!-- Sufficient space exists to include the bounding stopwatch circle. -->
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-
- <com.android.deskclock.timer.CountingTimerView
- android:id="@+id/stopwatch_time_text"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
- <com.android.deskclock.stopwatch.StopwatchCircleView
- android:id="@+id/stopwatch_time"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@null" />
-
-</merge>
\ No newline at end of file
diff --git a/res/layout/alarm_activity.xml b/res/layout/alarm_activity.xml
index b2d7e6e..d446114 100644
--- a/res/layout/alarm_activity.xml
+++ b/res/layout/alarm_activity.xml
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2015 The Android Open Source Project
+ Copyright (C) 2016 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
+ 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,
@@ -30,29 +30,29 @@
app:rowCount="3"
app:columnCount="3">
- <TextView
- android:id="@+id/title"
+ <TextClock
+ android:id="@+id/digital_clock"
+ style="@style/display_time"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:gravity="center"
+ android:includeFontPadding="false"
android:singleLine="true"
- android:textAppearance="@style/header_not_caps"
- android:textColor="@android:color/white"
+ android:textSize="@dimen/big_font_size"
app:layout_row="0"
app:layout_column="0"
app:layout_columnSpan="3"
app:layout_gravity="fill_horizontal" />
- <TextClock
- android:id="@+id/digital_clock"
+ <TextView
+ android:id="@+id/title"
+ style="@style/body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
- android:includeFontPadding="false"
android:singleLine="true"
- android:textAppearance="@style/big_thin"
- android:textColor="@android:color/white"
+ android:textSize="20sp"
app:layout_row="1"
app:layout_column="0"
app:layout_columnSpan="3"
@@ -75,13 +75,14 @@
android:id="@+id/snooze"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@drawable/bg_circle_accent"
+ android:background="@drawable/bg_circle_white"
android:contentDescription="@string/alarm_alert_snooze_text"
- app:srcCompat="@drawable/ic_snooze"
app:layout_row="2"
app:layout_column="0"
app:layout_columnWeight="1"
- app:layout_gravity="center" />
+ app:layout_gravity="center"
+ app:backgroundTint="?attr/colorAccent"
+ app:srcCompat="@drawable/ic_snooze" />
<ImageView
android:id="@+id/dismiss"
@@ -89,11 +90,11 @@
android:layout_height="wrap_content"
android:background="@drawable/bg_circle_white"
android:contentDescription="@string/alarm_alert_dismiss_text"
- app:srcCompat="@drawable/ic_dismiss"
app:layout_row="2"
app:layout_column="2"
app:layout_columnWeight="1"
- app:layout_gravity="center" />
+ app:layout_gravity="center"
+ app:srcCompat="@drawable/ic_dismiss" />
<ImageView
android:id="@+id/alarm"
@@ -105,11 +106,11 @@
android:paddingLeft="@dimen/alarm_lockscreen_alarm_horizontal_padding"
android:paddingRight="@dimen/alarm_lockscreen_alarm_horizontal_padding"
android:paddingTop="@dimen/alarm_lockscreen_alarm_vertical_padding"
- app:srcCompat="@drawable/ic_fab_alarm"
app:layout_row="2"
app:layout_column="1"
app:layout_columnWeight="1"
- app:layout_gravity="center" />
+ app:layout_gravity="center"
+ app:srcCompat="@drawable/ic_fab_alarm" />
<TextView
android:id="@+id/hint"
@@ -154,4 +155,4 @@
</LinearLayout>
-</FrameLayout>
\ No newline at end of file
+</FrameLayout>
diff --git a/res/layout/alarm_clock.xml b/res/layout/alarm_clock.xml
index 45b549d..eaf9178 100644
--- a/res/layout/alarm_clock.xml
+++ b/res/layout/alarm_clock.xml
@@ -20,7 +20,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <android.support.v7.widget.RecyclerView
+ <com.android.deskclock.AlarmRecyclerView
android:id="@+id/alarms_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -29,7 +29,8 @@
android:fadingEdgeLength="0dp"
android:paddingBottom="@dimen/fab_height"
android:scrollbarStyle="outsideOverlay"
- android:scrollbars="vertical" />
+ android:scrollbars="vertical"
+ android:splitMotionEvents="false" />
<TextView
android:id="@+id/alarms_empty_view"
diff --git a/res/layout/alarm_time_collapsed.xml b/res/layout/alarm_time_collapsed.xml
index 39c3bf8..6c0e3c7 100644
--- a/res/layout/alarm_time_collapsed.xml
+++ b/res/layout/alarm_time_collapsed.xml
@@ -1,124 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
-<LinearLayout
+<android.support.v7.widget.GridLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?android:attr/selectableItemBackground"
- android:gravity="center_horizontal|top"
- android:orientation="horizontal">
+ android:background="?attr/selectableItemBackground"
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp"
+ app:columnCount="5"
+ app:columnOrderPreserved="false"
+ app:rowCount="4">
- <!-- Left gutter. -->
- <Space
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="@integer/gutter_width_percent" />
-
- <!-- Guttered content. -->
- <LinearLayout
- android:layout_width="0dp"
+ <include
+ layout="@layout/alarm_time_summary"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_weight="@integer/guttered_content_width_percent"
- android:gravity="center_horizontal|top"
- android:orientation="vertical">
+ android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
+ app:layout_column="0"
+ app:layout_gravity="center_vertical"
+ app:layout_row="0" />
- <include layout="@layout/alarm_time_summary" />
+ <android.support.v7.widget.SwitchCompat
+ android:id="@+id/onoff"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
+ android:minHeight="@dimen/touch_target_min_size"
+ android:minWidth="@dimen/touch_target_min_size"
+ android:theme="@style/ThemeOverlay.Control.Accent"
+ app:layout_column="3"
+ app:layout_columnSpan="2"
+ app:layout_gravity="center_vertical"
+ app:layout_row="0" />
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom">
-
- <com.android.deskclock.widget.EllipsizeLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center_vertical"
- android:layout_marginEnd="@dimen/touch_target_min_size"
- android:gravity="center_vertical">
-
- <TextView
- android:id="@+id/label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="none"
- android:paddingEnd="@dimen/icon_margin"
- android:paddingStart="@dimen/icon_margin"
- android:singleLine="true"
- android:textColor="@color/clock_gray"
- android:textSize="@dimen/alarm_text_font_size" />
-
- <TextView
- android:id="@+id/days_of_week"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:focusable="true"
- android:paddingEnd="@dimen/icon_margin"
- android:paddingStart="@dimen/icon_margin"
- android:singleLine="true"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/alarm_text_font_size"
- android:textStyle="bold" />
-
- <TextView
- android:id="@+id/upcoming_instance_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:focusable="true"
- android:paddingEnd="@dimen/icon_margin"
- android:paddingStart="@dimen/icon_margin"
- android:singleLine="true"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/alarm_text_font_size"
- android:textStyle="bold" />
-
- </com.android.deskclock.widget.EllipsizeLayout>
-
- <ImageButton
- android:id="@+id/arrow"
- android:layout_width="@dimen/touch_target_min_size"
- android:layout_height="@dimen/touch_target_min_size"
- android:layout_gravity="center_vertical|end"
- android:background="?attr/selectableItemBackground"
- android:contentDescription="@string/expand_alarm"
- android:scaleType="center"
- app:srcCompat="@drawable/ic_expand_more" />
-
- </FrameLayout>
-
- <include layout="@layout/preemptive_dismiss" />
-
- <View
- android:id="@+id/hairline"
- android:layout_width="match_parent"
- android:layout_height="@dimen/hairline_height"
- android:layout_gravity="bottom"
- android:layout_marginEnd="@dimen/icon_margin"
- android:layout_marginStart="@dimen/icon_margin"
- android:background="@color/hairline" />
-
- </LinearLayout>
-
- <!-- Right gutter. -->
- <Space
+ <com.android.deskclock.widget.EllipsizeLayout
android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="@integer/gutter_width_percent" />
+ android:layout_height="@dimen/touch_target_min_size"
+ android:gravity="center_vertical"
+ app:layout_column="0"
+ app:layout_columnSpan="3"
+ app:layout_gravity="fill_horizontal"
+ app:layout_row="1">
-</LinearLayout>
\ No newline at end of file
+ <TextView
+ android:id="@+id/label"
+ style="@style/body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:paddingEnd="@dimen/icon_margin"
+ android:paddingStart="@dimen/icon_margin"
+ android:textColor="@color/clock_gray" />
+
+ <TextView
+ android:id="@+id/days_of_week"
+ style="@style/body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:focusable="true"
+ android:maxLines="1"
+ android:paddingEnd="@dimen/icon_margin"
+ android:paddingStart="@dimen/icon_margin" />
+
+ <TextView
+ android:id="@+id/upcoming_instance_label"
+ style="@style/body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:focusable="true"
+ android:paddingEnd="@dimen/icon_margin"
+ android:paddingStart="@dimen/icon_margin"
+ android:singleLine="true" />
+
+ </com.android.deskclock.widget.EllipsizeLayout>
+
+ <include
+ layout="@layout/preemptive_dismiss"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/touch_target_min_size"
+ app:layout_column="0"
+ app:layout_columnSpan="2"
+ app:layout_row="2" />
+
+ <ImageButton
+ android:id="@+id/arrow"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/touch_target_min_size"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/expand_alarm"
+ android:padding="@dimen/checkbox_start_padding"
+ android:scaleType="center"
+ app:layout_column="4"
+ app:layout_gravity="bottom"
+ app:layout_row="1"
+ app:layout_rowSpan="2"
+ app:srcCompat="@drawable/ic_caret_down" />
+
+ <View
+ android:id="@+id/hairline"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/hairline_height"
+ android:layout_marginEnd="@dimen/icon_margin"
+ android:layout_marginStart="@dimen/icon_margin"
+ android:background="@color/hairline"
+ app:layout_column="0"
+ app:layout_columnSpan="5"
+ app:layout_gravity="fill_horizontal"
+ app:layout_row="3" />
+
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/alarm_time_expanded.xml b/res/layout/alarm_time_expanded.xml
index ded035d..8c3513c 100644
--- a/res/layout/alarm_time_expanded.xml
+++ b/res/layout/alarm_time_expanded.xml
@@ -1,166 +1,184 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
-<LinearLayout
+<android.support.v7.widget.GridLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?android:attr/selectableItemBackground"
- android:gravity="center_horizontal|top"
- android:orientation="horizontal">
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp"
+ app:columnCount="8"
+ app:columnOrderPreserved="false"
+ app:rowCount="8">
- <!-- Left gutter. -->
- <Space
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="@integer/gutter_width_percent" />
+ <include
+ layout="@layout/alarm_time_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
+ app:layout_column="0"
+ app:layout_columnSpan="4"
+ app:layout_gravity="center_vertical"
+ app:layout_row="0" />
- <!-- Guttered content. -->
+ <android.support.v7.widget.SwitchCompat
+ android:id="@+id/onoff"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
+ android:minHeight="@dimen/touch_target_min_size"
+ android:minWidth="@dimen/touch_target_min_size"
+ android:theme="@style/ThemeOverlay.Control.Accent"
+ app:layout_column="6"
+ app:layout_columnSpan="2"
+ app:layout_gravity="center_vertical"
+ app:layout_row="0" />
+
+ <CheckBox
+ android:id="@+id/repeat_onoff"
+ style="@style/body"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/touch_target_min_size"
+ android:paddingEnd="@dimen/checkbox_start_padding"
+ android:paddingStart="@dimen/checkbox_start_padding"
+ android:text="@string/alarm_repeat"
+ app:layout_column="0"
+ app:layout_columnSpan="2"
+ app:layout_row="1" />
+
+ <!-- Day buttons are put here programmatically -->
<LinearLayout
+ android:id="@+id/repeat_days"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/touch_target_min_size"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ app:layout_column="0"
+ app:layout_columnSpan="8"
+ app:layout_gravity="fill_horizontal"
+ app:layout_row="2" />
+
+ <TextView
+ android:id="@+id/choose_ringtone"
+ style="@style/body"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/touch_target_min_size"
+ android:background="?attr/selectableItemBackground"
+ android:clickable="true"
+ android:drawablePadding="@dimen/alarm_horizontal_padding"
+ android:ellipsize="marquee"
+ android:gravity="start|center_vertical"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:paddingEnd="@dimen/icon_margin"
+ android:paddingStart="@dimen/icon_margin"
+ android:scrollHorizontally="true"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ app:layout_column="0"
+ app:layout_columnSpan="5"
+ app:layout_gravity="fill_horizontal"
+ app:layout_row="3" />
+
+ <CheckBox
+ android:id="@+id/vibrate_onoff"
+ style="@style/body"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/touch_target_min_size"
+ android:paddingEnd="@dimen/checkbox_start_padding"
+ android:paddingStart="@dimen/checkbox_start_padding"
+ android:text="@string/alarm_vibrate"
+ app:layout_column="5"
+ app:layout_columnSpan="3"
+ app:layout_gravity="center_vertical"
+ app:layout_row="3" />
+
+ <TextView
+ android:id="@+id/edit_label"
+ style="@style/body"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_weight="@integer/guttered_content_width_percent"
- android:gravity="center_horizontal|top"
- android:orientation="vertical">
+ android:background="?attr/selectableItemBackground"
+ android:drawablePadding="@dimen/alarm_horizontal_padding"
+ android:ellipsize="end"
+ android:gravity="start|center_vertical"
+ android:hint="@string/label"
+ android:paddingBottom="12dp"
+ android:paddingEnd="4dp"
+ android:paddingStart="4dp"
+ android:paddingTop="12dp"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ app:layout_column="0"
+ app:layout_columnSpan="8"
+ app:layout_gravity="fill_horizontal"
+ app:layout_row="4" />
- <include layout="@layout/alarm_time_summary" />
+ <include
+ layout="@layout/preemptive_dismiss"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_column="0"
+ app:layout_columnSpan="3"
+ app:layout_row="5" />
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- android:orientation="vertical">
-
- <CheckBox
- android:id="@+id/repeat_onoff"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/touch_target_min_size"
- android:layout_gravity="center_vertical|start"
- android:paddingEnd="@dimen/checkbox_start_padding"
- android:paddingStart="@dimen/checkbox_start_padding"
- android:text="@string/alarm_repeat"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/alarm_text_font_size" />
-
- <!-- Day buttons are put here programmatically -->
- <LinearLayout
- android:id="@+id/repeat_days"
- android:layout_width="match_parent"
- android:layout_height="@dimen/touch_target_min_size"
- android:layout_gravity="top"
- android:orientation="horizontal"
- android:visibility="gone" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:baselineAligned="true"
- android:orientation="horizontal">
-
- <TextView
- android:id="@+id/choose_ringtone"
- android:layout_width="0dp"
- android:layout_height="@dimen/touch_target_min_size"
- android:layout_weight="1"
- android:background="?android:attr/selectableItemBackground"
- android:clickable="true"
- android:drawablePadding="@dimen/alarm_horizontal_padding"
- android:ellipsize="marquee"
- android:gravity="center_vertical"
- android:marqueeRepeatLimit="marquee_forever"
- android:paddingStart="@dimen/icon_margin"
- android:scrollHorizontally="true"
- android:singleLine="true"
- android:textAlignment="viewStart"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/alarm_text_font_size" />
-
- <CheckBox
- android:id="@+id/vibrate_onoff"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/touch_target_min_size"
- android:paddingEnd="@dimen/checkbox_start_padding"
- android:paddingStart="@dimen/checkbox_start_padding"
- android:text="@string/alarm_vibrate"
- android:textColor="@color/white"
- android:textSize="@dimen/alarm_text_font_size" />
-
- </LinearLayout>
-
- <TextView
- android:id="@+id/edit_label"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:hint="@string/label"
- android:paddingBottom="12dp"
- android:paddingStart="44dp"
- android:paddingTop="12dp"
- android:singleLine="true"
- android:textColor="@color/white"
- android:textSize="@dimen/alarm_text_font_size" />
-
- <View
- android:layout_width="wrap_content"
- android:layout_height="@dimen/hairline_height"
- android:layout_marginEnd="@dimen/hairline_side_padding"
- android:layout_marginStart="@dimen/hairline_side_padding"
- android:background="@color/hairline" />
-
- <include layout="@layout/preemptive_dismiss" />
-
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom|start">
-
- <ImageButton
- android:id="@+id/delete"
- android:layout_width="@dimen/touch_target_min_size"
- android:layout_height="@dimen/touch_target_min_size"
- android:layout_gravity="center_vertical|start"
- android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
- android:layout_marginBottom="@dimen/alarm_clock_vertical_margin"
- android:background="?attr/selectableItemBackground"
- android:contentDescription="@string/delete_alarm"
- android:scaleType="center"
- app:srcCompat="@drawable/ic_delete_small" />
-
- <ImageButton
- android:id="@+id/arrow"
- android:layout_width="@dimen/touch_target_min_size"
- android:layout_height="@dimen/touch_target_min_size"
- android:layout_gravity="center_vertical|end"
- android:background="?attr/selectableItemBackground"
- android:contentDescription="@string/collapse_alarm"
- android:rotation="@integer/chevron_rotate_180"
- android:scaleType="center"
- app:srcCompat="@drawable/ic_expand_more" />
-
- </FrameLayout>
-
- </LinearLayout>
-
- </LinearLayout>
-
- <!-- Right gutter. -->
- <Space
+ <View
+ android:id="@+id/hairline"
android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="@integer/gutter_width_percent" />
+ android:layout_height="@dimen/hairline_height"
+ android:layout_marginRight="@dimen/hairline_side_padding"
+ android:layout_marginLeft="@dimen/hairline_side_padding"
+ android:background="@color/hairline"
+ app:layout_column="0"
+ app:layout_columnSpan="8"
+ app:layout_gravity="fill_horizontal"
+ app:layout_row="6" />
-</LinearLayout>
\ No newline at end of file
+ <Button
+ android:id="@+id/delete"
+ style="?attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/alarm_clock_vertical_margin"
+ android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
+ android:drawablePadding="@dimen/alarm_horizontal_padding"
+ android:gravity="start|center_vertical"
+ android:paddingEnd="@dimen/checkbox_start_padding"
+ android:paddingStart="@dimen/icon_margin"
+ android:text="@string/delete"
+ android:textAllCaps="false"
+ android:textAppearance="@style/body"
+ app:layout_column="0"
+ app:layout_gravity="center_vertical"
+ app:layout_row="7" />
+
+ <ImageButton
+ android:id="@+id/arrow"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/touch_target_min_size"
+ android:layout_marginBottom="@dimen/alarm_clock_vertical_margin"
+ android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/collapse_alarm"
+ android:padding="@dimen/checkbox_start_padding"
+ android:scaleType="center"
+ app:layout_column="7"
+ app:layout_gravity="center_vertical"
+ app:layout_row="7"
+ app:srcCompat="@drawable/ic_caret_up" />
+
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/alarm_time_summary.xml b/res/layout/alarm_time_summary.xml
index ef52be5..25c76b3 100644
--- a/res/layout/alarm_time_summary.xml
+++ b/res/layout/alarm_time_summary.xml
@@ -14,31 +14,10 @@
limitations under the License.
-->
-<LinearLayout
+<com.android.deskclock.widget.TextTime
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
+ android:id="@+id/digital_clock"
+ style="@style/display_time"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/alarm_clock_vertical_margin"
- android:orientation="horizontal">
-
- <com.android.deskclock.widget.TextTime
- android:id="@+id/digital_clock"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:fontFamily="sans-serif-thin"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/alarm_time_font_size" />
-
- <Space
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1" />
-
- <android.support.v7.widget.SwitchCompat
- android:id="@+id/onoff"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/touch_target_min_size"
- android:layout_gravity="center_vertical|end"
- android:theme="@style/ControlAccentThemeOverlay" />
-
-</LinearLayout>
\ No newline at end of file
+ android:background="?attr/selectableItemBackground"/>
diff --git a/res/layout/alarm_volume_preference.xml b/res/layout/alarm_volume_preference.xml
index af21dcf..9929502 100644
--- a/res/layout/alarm_volume_preference.xml
+++ b/res/layout/alarm_volume_preference.xml
@@ -39,7 +39,7 @@
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="48dp">
+ android:layout_height="@dimen/touch_target_min_size">
<ImageView
android:id="@+id/alarm_icon"
@@ -47,12 +47,14 @@
android:layout_height="match_parent"
android:importantForAccessibility="no" />
+ <!-- Specify maxHeight to properly set the track height on API < 23 -->
<SeekBar
android:id="@+id/alarm_volume_slider"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
- android:layout_weight="1" />
+ android:layout_weight="1"
+ android:maxHeight="@dimen/touch_target_min_size" />
</LinearLayout>
diff --git a/res/layout/chronometer_notif_content.xml b/res/layout/chronometer_notif_content.xml
index 8cb6d52..c962a0e 100644
--- a/res/layout/chronometer_notif_content.xml
+++ b/res/layout/chronometer_notif_content.xml
@@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2016 The Android Open Source Project
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
@@ -26,13 +26,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title" />
+ android:textAppearance="@style/TextAppearance.AppCompat.Notification.Title" />
<TextView
android:id="@+id/state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent" />
+ android:textAppearance="@style/TextAppearance.AppCompat.Notification" />
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/layout/cities_activity.xml b/res/layout/cities_activity.xml
index 07c5247..d80865d 100644
--- a/res/layout/cities_activity.xml
+++ b/res/layout/cities_activity.xml
@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2012 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
@@ -26,7 +27,9 @@
android:id="@+id/cities_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:clipToPadding="false"
android:divider="@null"
- android:theme="@style/ControlAccentThemeOverlay" />
+ android:scrollbarStyle="insideInset"
+ android:theme="@style/ThemeOverlay.Control.Accent" />
-</FrameLayout>
\ No newline at end of file
+</FrameLayout>
diff --git a/res/layout/city_list_header.xml b/res/layout/city_list_header.xml
index 01793c2..2db78ef 100644
--- a/res/layout/city_list_header.xml
+++ b/res/layout/city_list_header.xml
@@ -25,5 +25,5 @@
android:paddingStart="20dp"
android:text="@string/selected_cities_label"
android:textAlignment="viewStart"
- android:textColor="@color/clock_white"
+ android:textColor="@color/white"
android:textSize="20sp" />
diff --git a/res/layout/city_list_item.xml b/res/layout/city_list_item.xml
index c0c48a6..520ab32 100644
--- a/res/layout/city_list_item.xml
+++ b/res/layout/city_list_item.xml
@@ -19,7 +19,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
+ android:background="?attr/selectableItemBackground"
android:minHeight="@dimen/cities_list_item_height"
android:orientation="horizontal">
<TextView
@@ -29,7 +29,7 @@
android:gravity="center"
android:textStyle="bold"
android:textSize="@dimen/label_text_size"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<CheckBox
android:id="@+id/city_onoff"
android:layout_height="wrap_content"
diff --git a/res/layout/crescendo_length_picker.xml b/res/layout/crescendo_length_picker.xml
deleted file mode 100644
index cadc46e..0000000
--- a/res/layout/crescendo_length_picker.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-/**
- * Copyright (c) 2015, Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
--->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:gravity="center_horizontal">
-
- <com.android.deskclock.NumberPickerCompat
- android:id="@+id/seconds_picker"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:focusable="true"
- android:focusableInTouchMode="true" />
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginStart="16dp"
- android:gravity="center_vertical"
- android:textAppearance="?android:attr/textAppearanceLarge" />
-
-</LinearLayout>
diff --git a/res/layout/date_and_next_alarm_time.xml b/res/layout/date_and_next_alarm_time.xml
index 78dc250..ad25502 100644
--- a/res/layout/date_and_next_alarm_time.xml
+++ b/res/layout/date_and_next_alarm_time.xml
@@ -23,32 +23,29 @@
<TextView
android:id="@+id/date"
+ style="@style/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textAllCaps="true"
- android:textAppearance="@style/PrimaryLabelTextAppearance"
- android:textSize="12sp" />
+ android:textAllCaps="true" />
<TextView
android:id="@+id/nextAlarmIcon"
+ style="@style/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="@dimen/alarm_icon_padding"
android:layout_marginStart="@dimen/alarm_icon_padding"
- android:includeFontPadding="false"
android:ellipsize="none"
- android:singleLine="true"
- android:text="@string/clock_emoji"
- android:textColor="@color/clock_white"
- android:textSize="20sp" />
+ android:includeFontPadding="false"
+ android:maxLines="1"
+ android:text="@string/clock_emoji" />
<TextView
android:id="@+id/nextAlarm"
+ style="@style/body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:textAllCaps="true"
- android:textAppearance="@style/PrimaryLabelTextAppearance"
- android:textSize="12sp" />
+ android:textAllCaps="true" />
</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/day_button.xml b/res/layout/day_button.xml
index 5329042..9cebabf 100644
--- a/res/layout/day_button.xml
+++ b/res/layout/day_button.xml
@@ -15,14 +15,16 @@
~ limitations under the License
-->
-<CheckBox
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="0dp"
- android:layout_height="48dp"
- android:layout_weight="1"
- android:background="@drawable/toggle_circle"
- android:button="@null"
- android:gravity="center"
- android:minWidth="0dp"
- android:minHeight="0dp"
- android:textSize="@dimen/day_button_font_size" />
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:layout_weight="1">
+ <CheckBox
+ android:id="@+id/day_button_box"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:background="@drawable/toggle_circle"
+ android:button="@null"
+ android:gravity="center"
+ android:textSize="@dimen/day_button_font_size" />
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/desk_clock.xml b/res/layout/desk_clock.xml
index e038f55..e5e2f63 100644
--- a/res/layout/desk_clock.xml
+++ b/res/layout/desk_clock.xml
@@ -1,26 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2009 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/coordinator"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:fitsSystemWindows="true">
+ android:fitsSystemWindows="true"
+ app:statusBarBackground="@null">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
@@ -31,23 +34,31 @@
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="?attr/actionBarSize"
- android:padding="0dp"
- app:contentInsetLeft="0dp"
- app:contentInsetStart="0dp">
+ android:layout_height="wrap_content"
+ app:contentInsetStart="0dp"
+ tools:ignore="RtlSymmetry">
<android.support.design.widget.TabLayout
- android:id="@+id/sliding_tabs"
- android:layout_width="wrap_content"
+ android:id="@+id/tabs"
+ android:layout_width="match_parent"
android:layout_height="match_parent"
- app:tabGravity="center"
+ app:tabGravity="fill"
app:tabIndicatorColor="@android:color/transparent"
+ app:tabMaxWidth="0dp"
app:tabMode="fixed"
- app:tabPaddingEnd="20dp"
- app:tabPaddingStart="20dp" />
+ app:tabPaddingEnd="0dp"
+ app:tabPaddingStart="0dp" />
</android.support.v7.widget.Toolbar>
+ <View
+ android:id="@+id/tab_hairline"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/hairline_height"
+ android:layout_gravity="bottom"
+ android:background="@color/hairline"
+ android:importantForAccessibility="no" />
+
</android.support.design.widget.AppBarLayout>
<FrameLayout
@@ -55,10 +66,12 @@
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
- <com.android.deskclock.widget.RtlViewPager
+ <android.support.v4.view.ViewPager
android:id="@+id/desk_clock_pager"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no"
+ android:saveEnabled="false" />
<include layout="@layout/drop_shadow" />
@@ -78,32 +91,24 @@
android:layout_gravity="start|center_vertical"
android:layout_weight="1">
- <ImageButton
+ <Button
android:id="@+id/left_button"
- android:layout_width="@dimen/fab_button_size"
- android:layout_height="@dimen/fab_button_size"
+ style="?attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:layout_gravity="center"
- android:contentDescription="@null"
- android:padding="@dimen/fab_button_padding"
android:scaleType="centerInside" />
</FrameLayout>
- <FrameLayout
- android:layout_width="0dp"
+ <android.support.design.widget.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_weight="1">
-
- <android.support.design.widget.FloatingActionButton
- android:id="@+id/fab"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:layout_margin="@dimen/fab_margin"
- app:borderWidth="0dp"
- app:elevation="@dimen/fab_elevation" />
-
- </FrameLayout>
+ android:layout_gravity="center"
+ android:layout_margin="@dimen/fab_margin"
+ app:borderWidth="0dp"
+ app:elevation="@dimen/fab_elevation" />
<FrameLayout
android:layout_width="0dp"
@@ -111,15 +116,16 @@
android:layout_gravity="end|center_vertical"
android:layout_weight="1">
- <ImageButton
+ <Button
android:id="@+id/right_button"
- android:layout_width="@dimen/fab_button_size"
- android:layout_height="@dimen/fab_button_size"
+ style="?attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
android:layout_gravity="center"
- android:contentDescription="@null"
- android:padding="@dimen/fab_button_padding"
android:scaleType="centerInside" />
</FrameLayout>
+
</LinearLayout>
+
</android.support.design.widget.CoordinatorLayout>
diff --git a/res/layout/desk_clock_saver.xml b/res/layout/desk_clock_saver.xml
index 9bf81f4..dafabfd 100644
--- a/res/layout/desk_clock_saver.xml
+++ b/res/layout/desk_clock_saver.xml
@@ -50,7 +50,7 @@
android:ellipsize="none"
android:gravity="center"
android:singleLine="true"
- android:textColor="@color/clock_white"
+ android:textColor="@color/white"
android:textSize="@dimen/main_clock_font_size" />
<include layout="@layout/date_and_next_alarm_time" />
diff --git a/res/layout/digital_widget.xml b/res/layout/digital_widget.xml
index 32c804b..c5b4837 100644
--- a/res/layout/digital_widget.xml
+++ b/res/layout/digital_widget.xml
@@ -33,7 +33,7 @@
android:format24Hour="@string/lock_screen_24_hour_format"
android:includeFontPadding="false"
android:singleLine="true"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<LinearLayout
android:layout_width="wrap_content"
@@ -50,7 +50,7 @@
android:includeFontPadding="false"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<ImageView
android:id="@+id/nextAlarmIcon"
@@ -70,7 +70,7 @@
android:includeFontPadding="false"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
</LinearLayout>
diff --git a/res/layout/digital_widget_sizer.xml b/res/layout/digital_widget_sizer.xml
index d44f4bc..f524cf5 100644
--- a/res/layout/digital_widget_sizer.xml
+++ b/res/layout/digital_widget_sizer.xml
@@ -15,7 +15,6 @@
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/digital_widget_sizer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
@@ -31,7 +30,7 @@
android:format12Hour="@string/lock_screen_12_hour_format"
android:format24Hour="@string/lock_screen_24_hour_format"
android:singleLine="true"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<LinearLayout
android:layout_width="wrap_content"
@@ -48,7 +47,7 @@
android:ellipsize="none"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<!-- This view is drawn to a Bitmap and sent to the widget as an icon. -->
<TextView
@@ -61,7 +60,7 @@
android:ellipsize="none"
android:singleLine="true"
android:text="@string/clock_emoji"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<TextView
android:id="@+id/nextAlarm"
@@ -73,8 +72,8 @@
android:ellipsize="none"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
</LinearLayout>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/layout/loading_widget.xml b/res/layout/loading_widget.xml
index 38714df..ff33db2 100644
--- a/res/layout/loading_widget.xml
+++ b/res/layout/loading_widget.xml
@@ -22,4 +22,4 @@
android:includeFontPadding="false"
android:singleLine="true"
android:text="@string/loading_widget"
- android:textColor="@color/clock_white" />
\ No newline at end of file
+ android:textColor="@color/white" />
\ No newline at end of file
diff --git a/res/layout/main_clock_frame.xml b/res/layout/main_clock_frame.xml
index bde50b5..ab96072 100644
--- a/res/layout/main_clock_frame.xml
+++ b/res/layout/main_clock_frame.xml
@@ -42,22 +42,22 @@
android:id="@+id/analog_clock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/bottom_text_spacing_analog"
android:layout_marginEnd="@dimen/analog_clock_margin"
android:layout_marginLeft="@dimen/analog_clock_margin"
android:layout_marginRight="@dimen/analog_clock_margin"
android:layout_marginStart="@dimen/analog_clock_margin"
android:layout_marginTop="@dimen/circle_margin_top" />
- <TextClock
+ <com.android.deskclock.widget.AutoSizingTextClock
android:id="@+id/digital_clock"
- style="@style/big_thin"
+ style="@style/display_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="none"
+ android:includeFontPadding="false"
+ android:paddingTop="@dimen/main_clock_digital_padding"
android:singleLine="true"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/main_clock_font_size" />
+ android:textSize="@dimen/main_clock_digital_font_size" />
</FrameLayout>
@@ -77,4 +77,4 @@
android:layout_height="match_parent"
android:layout_weight="@integer/gutter_width_percent" />
-</LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/preemptive_dismiss.xml b/res/layout/preemptive_dismiss.xml
index 03ad1c4..d095fac 100644
--- a/res/layout/preemptive_dismiss.xml
+++ b/res/layout/preemptive_dismiss.xml
@@ -13,32 +13,17 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
- android:id="@+id/preemptive_dismiss_container"
+<Button
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
+ android:id="@+id/preemptive_dismiss_button"
+ style="?attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:orientation="vertical"
- android:visibility="gone" >
-
- <Button
- android:id="@+id/preemptive_dismiss_button"
- android:layout_width="match_parent"
- android:layout_height="@dimen/tall_row_height"
- android:background="?attr/selectableItemBackground"
- android:drawableStart="@drawable/ic_alarm_off_white_24dp"
- android:drawablePadding="@dimen/alarm_horizontal_padding"
- android:gravity="start|center_vertical"
- android:paddingStart="@dimen/icon_margin"
- android:textAllCaps="false"
- android:textColor="@color/clock_white"
- android:textSize="@dimen/alarm_text_font_size" />
-
- <View
- android:layout_width="wrap_content"
- android:layout_height="@dimen/hairline_height"
- android:layout_marginEnd="@dimen/hairline_side_padding"
- android:layout_marginStart="@dimen/hairline_side_padding"
- android:background="@color/hairline" />
-
-</LinearLayout>
+ android:minHeight="@dimen/touch_target_min_size"
+ android:drawablePadding="@dimen/alarm_horizontal_padding"
+ android:drawableStart="@drawable/ic_alarm_off_white_24dp"
+ android:gravity="center_vertical"
+ android:paddingStart="@dimen/icon_margin"
+ android:textAllCaps="false"
+ android:textAppearance="@style/body"
+ android:visibility="gone"/>
diff --git a/res/drawable-v22/ic_lap_reset_animation.xml b/res/layout/ringtone_item_header.xml
similarity index 62%
copy from res/drawable-v22/ic_lap_reset_animation.xml
copy to res/layout/ringtone_item_header.xml
index 041c9e1..415e4de 100644
--- a/res/drawable-v22/ic_lap_reset_animation.xml
+++ b/res/layout/ringtone_item_header.xml
@@ -14,13 +14,17 @@
limitations under the License.
-->
-<animated-vector
+<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:drawable="@drawable/ic_lap_reset" >
- <target
- android:name="ic_refresh_white_48dp_outlines_0"
- android:animation="@animator/ic_lap_reset_ic_refresh_white_outlines_0_animation" />
- <target
- android:name="path_2"
- android:animation="@animator/ic_lap_reset_path_2_animation" />
-</animated-vector>
+ android:id="@+id/ringtone_item_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="start"
+ android:paddingBottom="16dp"
+ android:paddingEnd="15dp"
+ android:paddingStart="15dp"
+ android:paddingTop="16dp"
+ android:textAlignment="viewStart"
+ android:textColor="?attr/colorAccent"
+ android:textSize="15sp" />
\ No newline at end of file
diff --git a/res/layout/ringtone_item_sound.xml b/res/layout/ringtone_item_sound.xml
new file mode 100644
index 0000000..12bd765
--- /dev/null
+++ b/res/layout/ringtone_item_sound.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<!-- This parent FrameLayout provides a tap ripple effect over all children. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:foreground="?attr/selectableItemBackground">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:orientation="horizontal"
+ android:paddingBottom="8dp"
+ android:paddingTop="8dp"
+ tools:ignore="UselessParent">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/gutter_width_percent" />
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="@integer/guttered_content_width_percent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/ringtone_image"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:importantForAccessibility="no"
+ android:scaleType="center" />
+
+ <TextView
+ android:id="@+id/ringtone_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp"
+ android:textColor="@color/white"
+ android:textSize="@dimen/alarm_text_font_size" />
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/sound_image_selected"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ android:tint="?attr/colorAccent"
+ android:visibility="gone"
+ app:srcCompat="@drawable/ic_checkmark" />
+
+ </RelativeLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/gutter_width_percent" />
+
+ </LinearLayout>
+
+</FrameLayout>
diff --git a/res/layout/stopwatch_view.xml b/res/layout/ringtone_picker.xml
similarity index 69%
rename from res/layout/stopwatch_view.xml
rename to res/layout/ringtone_picker.xml
index ecc4322..183b8bf 100644
--- a/res/layout/stopwatch_view.xml
+++ b/res/layout/ringtone_picker.xml
@@ -14,12 +14,17 @@
limitations under the License.
-->
-<!-- Insufficient space exists to include the bounding stopwatch circle. -->
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
- <com.android.deskclock.timer.CountingTimerView
- android:id="@+id/stopwatch_time_text"
+ <include layout="@layout/drop_shadow" />
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/ringtone_content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
-</merge>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/selection_layout.xml b/res/layout/selection_layout.xml
index 0da03d6..0bb11fb 100644
--- a/res/layout/selection_layout.xml
+++ b/res/layout/selection_layout.xml
@@ -29,10 +29,10 @@
android:id="@+id/cancel_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="@string/time_picker_cancel"
+ android:text="@android:string/cancel"
android:clickable="true"
android:gravity="end"
android:background="@android:color/transparent"
android:textColor="@android:color/holo_blue_light"/>
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/layout/simple_menu_dropdown_item.xml
similarity index 63%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/layout/simple_menu_dropdown_item.xml
index 114006d..a252843 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/layout/simple_menu_dropdown_item.xml
@@ -14,6 +14,13 @@
limitations under the License.
-->
-<pathInterpolator
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="?android:attr/spinnerDropDownItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:ellipsize="marquee"
+ android:gravity="center"
+ android:maxLines="1"
+ android:minWidth="112dp"
+ android:paddingEnd="16dp"
+ android:paddingStart="16dp" />
diff --git a/res/layout/snooze_length_picker.xml b/res/layout/snooze_length_picker.xml
deleted file mode 100644
index 355ca76..0000000
--- a/res/layout/snooze_length_picker.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-/**
- * Copyright (c) 2012, Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:gravity="center_horizontal">
-
- <com.android.deskclock.NumberPickerCompat
- android:id="@+id/minutes_picker"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:focusable="true"/>
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginStart="16dp"
- android:gravity="center_vertical"
- android:textAppearance="?android:attr/textAppearanceLarge" />
-
-</LinearLayout>
diff --git a/res/layout/stopwatch_fragment.xml b/res/layout/stopwatch_fragment.xml
index 59c0b52..236d845 100644
--- a/res/layout/stopwatch_fragment.xml
+++ b/res/layout/stopwatch_fragment.xml
@@ -19,8 +19,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:gravity="center"
android:baselineAligned="false"
+ android:gravity="center"
android:orientation="vertical">
<android.support.percent.PercentFrameLayout
@@ -29,12 +29,19 @@
android:layout_gravity="center">
<com.android.deskclock.TimerCircleFrameLayout
+ android:id="@+id/stopwatch_time_wrapper"
+ android:layout_gravity="center"
app:layout_aspectRatio="100%"
- app:layout_widthPercent="@fraction/timer_circle_width_percent"
app:layout_heightPercent="@fraction/timer_circle_height_percent"
- android:layout_gravity="center">
+ app:layout_widthPercent="@fraction/timer_circle_width_percent">
- <include layout="@layout/stopwatch_view" />
+ <include layout="@layout/stopwatch_time" />
+
+ <!-- Sufficient space exists to include the bounding stopwatch circle. -->
+ <com.android.deskclock.stopwatch.StopwatchCircleView
+ android:id="@+id/stopwatch_circle"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
</com.android.deskclock.TimerCircleFrameLayout>
diff --git a/res/layout/stopwatch_notif_collapsed.xml b/res/layout/stopwatch_notif_collapsed.xml
deleted file mode 100644
index bd67423..0000000
--- a/res/layout/stopwatch_notif_collapsed.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/swn_collapsed_hitspace"
- android:layout_width="match_parent"
- android:layout_height="64dp"
- android:background="@drawable/notification_background">
-
- <include layout="@layout/stopwatch_notification_icon" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="fill_vertical"
- android:layout_marginStart="@dimen/notification_icon_size"
- android:gravity="center"
- android:minHeight="@dimen/notification_icon_size"
- android:orientation="vertical"
- android:paddingBottom="2dp"
- android:paddingEnd="8dp"
- android:paddingTop="2dp">
-
- <LinearLayout
- android:id="@+id/line1"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/notification_content_margin_start"
- android:orientation="horizontal">
-
- <Chronometer
- android:id="@+id/swn_collapsed_chronometer"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title" />
-
- </LinearLayout>
-
- <TextView
- android:id="@+id/swn_collapsed_laps"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/notification_content_margin_start"
- android:ellipsize="marquee"
- android:fadingEdge="horizontal"
- android:visibility="gone"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent" />
-
- </LinearLayout>
-
-</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/stopwatch_notif_expanded.xml b/res/layout/stopwatch_notif_expanded.xml
deleted file mode 100644
index b673172..0000000
--- a/res/layout/stopwatch_notif_expanded.xml
+++ /dev/null
@@ -1,114 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:divider="@drawable/horizontal_divider"
- android:orientation="vertical"
- android:showDividers="middle">
-
- <FrameLayout
- android:id="@+id/swn_expanded_hitspace"
- android:layout_width="match_parent"
- android:layout_height="64dp"
- android:background="@drawable/notification_background">
-
- <include layout="@layout/stopwatch_notification_icon" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="fill_vertical"
- android:layout_marginStart="@dimen/notification_icon_size"
- android:gravity="center"
- android:minHeight="@dimen/notification_icon_size"
- android:orientation="vertical"
- android:paddingBottom="2dp"
- android:paddingEnd="8dp"
- android:paddingTop="2dp">
-
- <LinearLayout
- android:id="@+id/line1"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:orientation="horizontal">
-
- <Chronometer
- android:id="@+id/swn_expanded_chronometer"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent.Title" />
- </LinearLayout>
-
- <TextView
- android:id="@+id/swn_expanded_laps"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:ellipsize="marquee"
- android:fadingEdge="horizontal"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent"
- android:visibility="gone" />
-
- </LinearLayout>
-
- </FrameLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/notification_icon_size"
- android:divider="@drawable/divider"
- android:dividerPadding="12dp"
- android:orientation="horizontal"
- android:showDividers="middle">
-
- <TextView
- android:id="@+id/swn_left_button"
- android:layout_width="0dp"
- android:layout_height="48dp"
- android:layout_weight="1"
- android:background="@drawable/notification_background"
- android:drawablePadding="8dp"
- android:ellipsize="end"
- android:gravity="start|center_vertical"
- android:paddingStart="8dp"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent" />
-
- <TextView
- android:id="@+id/swn_right_button"
- android:layout_width="0dp"
- android:layout_height="48dp"
- android:layout_weight="1"
- android:background="@drawable/notification_background"
- android:drawablePadding="8dp"
- android:ellipsize="end"
- android:gravity="start|center_vertical"
- android:paddingStart="8dp"
- android:singleLine="true"
- android:textAppearance="@style/TextAppearance.StatusBar.EventContent" />
-
- </LinearLayout>
-
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/stopwatch_notification_icon.xml b/res/layout/stopwatch_notification_icon.xml
deleted file mode 100644
index f0872a1..0000000
--- a/res/layout/stopwatch_notification_icon.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<ImageView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/notification_icon"
- android:layout_width="@dimen/notification_icon_size"
- android:layout_height="@dimen/notification_icon_size"
- android:background="@drawable/notification_icon_bg"
- android:scaleType="center"
- android:contentDescription="@null" />
\ No newline at end of file
diff --git a/res/layout/stopwatch_time.xml b/res/layout/stopwatch_time.xml
new file mode 100644
index 0000000..c1895b4
--- /dev/null
+++ b/res/layout/stopwatch_time.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:baselineAligned="true"
+ android:duplicateParentState="true"
+ android:layoutDirection="ltr"
+ android:orientation="horizontal">
+
+ <com.android.deskclock.widget.AutoSizingTextView
+ android:id="@+id/stopwatch_time_text"
+ style="@style/display_time"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:duplicateParentState="true"
+ android:includeFontPadding="false"
+ android:paddingEnd="3dp"
+ android:paddingStart="30dp"
+ android:textSize="70sp" />
+
+ <TextView
+ android:id="@+id/stopwatch_hundredths_text"
+ style="@style/display_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:duplicateParentState="true"
+ android:fontFamily="sans-serif"
+ android:includeFontPadding="false"
+ android:paddingEnd="30dp"
+ android:paddingStart="3dp"
+ android:textSize="30sp" />
+
+</LinearLayout>
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/layout/tab_item.xml
similarity index 69%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/layout/tab_item.xml
index 114006d..4d8b17c 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/layout/tab_item.xml
@@ -14,6 +14,13 @@
limitations under the License.
-->
-<pathInterpolator
+<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawablePadding="10dp"
+ android:gravity="center"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.Tab" />
diff --git a/res/layout/three_keys_view.xml b/res/layout/three_keys_view.xml
deleted file mode 100644
index 10e8ccb..0000000
--- a/res/layout/three_keys_view.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:gravity="center"
- android:layoutDirection="ltr">
-
- <Button
- android:id="@+id/key_left"
- style="@style/dialpad"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:gravity="center" />
-
- <Button
- android:id="@+id/key_middle"
- style="@style/dialpad"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:gravity="center" />
-
- <Button
- android:id="@+id/key_right"
- style="@style/dialpad"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:background="?attr/selectableItemBackgroundBorderless"
- android:gravity="center" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/time_setup_view.xml b/res/layout/time_setup_view.xml
deleted file mode 100644
index c5a8377..0000000
--- a/res/layout/time_setup_view.xml
+++ /dev/null
@@ -1,71 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center_horizontal"
- android:orientation="vertical"
- android:paddingBottom="@dimen/fab_height"
- android:paddingTop="@dimen/timer_setup_top_margin">
-
- <com.android.deskclock.timer.TimerView
- android:id="@+id/timer_time_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:baselineAligned="true"
- android:layoutDirection="ltr">
-
- <include layout="@layout/timer_h_mm_ss_view" />
-
- <ImageButton
- android:id="@+id/delete"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginStart="@dimen/timer_setup_delete_margin"
- android:padding="@dimen/timer_setup_delete_padding"
- android:contentDescription="@string/timer_delete"
- android:scaleType="center"
- app:srcCompat="@drawable/ic_backspace" />
-
- </com.android.deskclock.timer.TimerView>
-
- <View
- android:id="@+id/divider"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_marginBottom="8dp"
- android:background="@color/dialog_gray" />
-
- <include
- android:id="@+id/first"
- layout="@layout/three_keys_view" />
-
- <include
- android:id="@+id/second"
- layout="@layout/three_keys_view" />
-
- <include
- android:id="@+id/third"
- layout="@layout/three_keys_view" />
-
- <include
- android:id="@+id/fourth"
- layout="@layout/three_keys_view" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/timer_fragment.xml b/res/layout/timer_fragment.xml
index a34d956..6e9817b 100644
--- a/res/layout/timer_fragment.xml
+++ b/res/layout/timer_fragment.xml
@@ -27,7 +27,6 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginBottom="@dimen/fab_height"
android:orientation="horizontal">
<LinearLayout
diff --git a/res/layout/timer_h_mm_ss_view.xml b/res/layout/timer_h_mm_ss_view.xml
deleted file mode 100644
index 987d2f2..0000000
--- a/res/layout/timer_h_mm_ss_view.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2013 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.
--->
-
-<merge
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <TextView
- android:id="@+id/hours_tens"
- style="@style/timer_setup_digit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <TextView
- android:id="@+id/hours_ones"
- style="@style/timer_setup_digit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <TextView
- style="@style/timer_setup_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:contentDescription="@string/hours_label_description"
- android:text="@string/hours_label" />
-
- <TextView
- android:id="@+id/minutes_tens"
- style="@style/timer_setup_digit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <TextView
- android:id="@+id/minutes_ones"
- style="@style/timer_setup_digit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <TextView
- style="@style/timer_setup_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:contentDescription="@string/minutes_label_description"
- android:text="@string/minutes_label" />
-
- <TextView
- android:id="@+id/seconds"
- style="@style/timer_setup_digit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
-
- <TextView
- style="@style/timer_setup_label"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:contentDescription="@string/seconds_label_description"
- android:text="@string/seconds_label" />
-
-</merge>
\ No newline at end of file
diff --git a/res/layout/timer_item.xml b/res/layout/timer_item.xml
index e003329..607b782 100644
--- a/res/layout/timer_item.xml
+++ b/res/layout/timer_item.xml
@@ -17,7 +17,6 @@
<!-- This TimerItem discards the circle because space is limited. -->
<com.android.deskclock.timer.TimerItem
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -27,52 +26,99 @@
android:layout_height="0dp"
android:layout_weight="1">
- <com.android.deskclock.timer.CountingTimerView
- android:id="@+id/timer_time_text"
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
-
- <Button
- android:id="@+id/timer_label"
- style="?android:attr/borderlessButtonStyle"
- android:layout_width="wrap_content"
- android:layout_height="40dp"
- android:layout_gravity="top|center_horizontal"
- android:clickable="false"
- android:ellipsize="end"
- android:gravity="center"
- android:hint="@string/label"
- android:maxLines="1"
- android:textAppearance="@style/SecondaryLabelTextAppearance" />
-
- <!-- Center the reset/add button on the third of the container above the delete button. -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="horizontal">
+ android:orientation="vertical">
- <FrameLayout
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/fab_height"
+ android:orientation="horizontal">
- <ImageButton
- android:id="@+id/reset_add"
- android:layout_width="@dimen/fab_button_size"
- android:layout_height="@dimen/fab_button_size"
- android:layout_gravity="center"
- android:contentDescription="@string/timer_plus_one"
- android:gravity="center"
- android:padding="@dimen/fab_button_padding"
- android:scaleType="centerInside"
- app:srcCompat="@drawable/ic_plusone" />
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="2" />
- </FrameLayout>
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3">
- <Space
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="2" />
+ <Button
+ android:id="@+id/timer_label"
+ style="?attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:clickable="false"
+ android:ellipsize="end"
+ android:gravity="center"
+ android:hint="@string/label"
+ android:maxLines="1"
+ android:minHeight="@dimen/touch_target_min_size"
+ android:minWidth="@dimen/touch_target_min_size"
+ android:textAppearance="@style/SecondaryLabelTextAppearance" />
+
+ </FrameLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="2" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1">
+
+ <Button
+ android:id="@+id/reset_add"
+ style="?attr/borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@string/timer_plus_one"
+ android:gravity="center"
+ android:scaleType="centerInside" />
+
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="3">
+
+ <com.android.deskclock.widget.AutoSizingTextView
+ android:id="@+id/timer_time_text"
+ style="@style/display_time"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:includeFontPadding="false"
+ android:paddingEnd="20dp"
+ android:paddingStart="20dp"
+ android:textSize="70sp" />
+
+ </FrameLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1" />
+
+
+ </LinearLayout>
</LinearLayout>
diff --git a/res/layout/time_setup_container.xml b/res/layout/timer_setup_container.xml
similarity index 97%
rename from res/layout/time_setup_container.xml
rename to res/layout/timer_setup_container.xml
index 4acbbd4..1cf0fbb 100644
--- a/res/layout/time_setup_container.xml
+++ b/res/layout/timer_setup_container.xml
@@ -28,7 +28,7 @@
<!-- Guttered content. -->
<include
- layout="@layout/time_setup_view"
+ layout="@layout/timer_setup_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="@integer/guttered_content_width_percent" />
@@ -39,4 +39,4 @@
android:layout_height="match_parent"
android:layout_weight="@integer/gutter_width_percent" />
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/layout/timer_setup_digits.xml b/res/layout/timer_setup_digits.xml
new file mode 100644
index 0000000..22e49ad
--- /dev/null
+++ b/res/layout/timer_setup_digits.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<android.support.v7.widget.GridLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layoutDirection="ltr"
+ app:rowCount="4"
+ app:columnCount="3">
+
+ <Button
+ android:id="@+id/timer_setup_digit_1"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="0"
+ app:layout_rowWeight="1"
+ app:layout_column="0"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_2"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="0"
+ app:layout_rowWeight="1"
+ app:layout_column="1"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_3"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="0"
+ app:layout_rowWeight="1"
+ app:layout_column="2"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_4"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="1"
+ app:layout_rowWeight="1"
+ app:layout_column="0"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_5"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="1"
+ app:layout_rowWeight="1"
+ app:layout_column="1"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_6"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="1"
+ app:layout_rowWeight="1"
+ app:layout_column="2"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_7"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="2"
+ app:layout_rowWeight="1"
+ app:layout_column="0"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_8"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="2"
+ app:layout_rowWeight="1"
+ app:layout_column="1"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_9"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="2"
+ app:layout_rowWeight="1"
+ app:layout_column="2"
+ app:layout_columnWeight="1" />
+
+ <Button
+ android:id="@+id/timer_setup_digit_0"
+ style="@style/Widget.Button.TimerSetupDigit"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_row="3"
+ app:layout_rowWeight="1"
+ app:layout_column="1"
+ app:layout_columnWeight="1" />
+
+</android.support.v7.widget.GridLayout>
diff --git a/res/layout/timer_setup_divider.xml b/res/layout/timer_setup_divider.xml
new file mode 100644
index 0000000..a15d3e9
--- /dev/null
+++ b/res/layout/timer_setup_divider.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/timer_setup_divider"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:importantForAccessibility="no"
+ android:theme="@style/ThemeOverlay.Control.Accent"
+ android:background="?android:attr/dividerHorizontal" />
diff --git a/res/layout/timer_setup_time.xml b/res/layout/timer_setup_time.xml
new file mode 100644
index 0000000..947723c
--- /dev/null
+++ b/res/layout/timer_setup_time.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layoutDirection="ltr"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/timer_setup_time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="none"
+ android:fontFamily="sans-serif"
+ android:fontFeatureSettings="tnum"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="@dimen/timer_setup_digit_font_size"
+ tools:targetApi="21" />
+
+ <ImageButton
+ android:id="@+id/timer_setup_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/timer_setup_delete_margin"
+ android:contentDescription="@string/timer_delete"
+ android:padding="12dp"
+ android:scaleType="center"
+ app:srcCompat="@drawable/ic_backspace" />
+
+</LinearLayout>
diff --git a/res/layout/timer_setup_view.xml b/res/layout/timer_setup_view.xml
new file mode 100644
index 0000000..ec99b42
--- /dev/null
+++ b/res/layout/timer_setup_view.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/fab_height"
+ android:paddingTop="@dimen/timer_setup_top_margin">
+
+ <include layout="@layout/timer_setup_time" />
+
+ <include layout="@layout/timer_setup_divider" />
+
+ <include
+ layout="@layout/timer_setup_digits"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/res/layout/world_clock_city_container.xml b/res/layout/world_clock_city_container.xml
new file mode 100644
index 0000000..5fa9798
--- /dev/null
+++ b/res/layout/world_clock_city_container.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/city_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/city_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="@style/body"/>
+
+ <TextView
+ android:id="@+id/hours_ahead"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="3dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:scrollHorizontally="true"
+ android:textAppearance="@style/SecondaryLabelTextAppearance"/>
+
+</LinearLayout>
+
+
diff --git a/res/layout/world_clock_item.xml b/res/layout/world_clock_item.xml
index d34eb77..080c0bf 100644
--- a/res/layout/world_clock_item.xml
+++ b/res/layout/world_clock_item.xml
@@ -26,53 +26,36 @@
android:layout_height="match_parent"
android:layout_weight="@integer/gutter_width_percent" />
- <com.android.deskclock.widget.EllipsizeLayout
+ <LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="@integer/guttered_content_width_percent"
android:gravity="center_vertical">
- <TextView
- android:id="@+id/city_name"
+ <include
+ layout="@layout/world_clock_city_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <TextClock
+ android:id="@+id/digital_clock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:ellipsize="end"
+ android:paddingStart="24dp"
+ android:layout_gravity="end"
android:singleLine="true"
- android:textAppearance="@style/PrimaryLabelTextAppearance" />
+ android:textAppearance="@style/world_clock_time"/>
- <TextView
- android:id="@+id/city_day"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="@dimen/style_label_space"
- android:ellipsize="none"
- android:singleLine="true"
- android:textAppearance="@style/SecondaryLabelTextAppearance" />
+ <com.android.deskclock.AnalogClock
+ android:id="@+id/analog_clock"
+ android:layout_width="@dimen/world_clock_analog_size"
+ android:layout_height="@dimen/world_clock_analog_size"
+ android:paddingStart="24dp"
+ android:layout_gravity="end"
+ android:layout_marginBottom="@dimen/bottom_text_spacing_analog_small" />
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <TextClock
- android:id="@+id/digital_clock"
- style="@style/medium_light"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="end"
- android:baselineAligned="true"
- android:singleLine="true"
- android:textColor="@color/clock_white" />
-
- <com.android.deskclock.AnalogClock
- android:id="@+id/analog_clock"
- android:layout_width="@dimen/world_clock_analog_size"
- android:layout_height="@dimen/world_clock_analog_size"
- android:layout_gravity="end"
- android:layout_marginBottom="@dimen/bottom_text_spacing_analog_small" />
-
- </FrameLayout>
-
- </com.android.deskclock.widget.EllipsizeLayout>
+ </LinearLayout>
<!-- Right gutter. -->
<Space
diff --git a/res/layout/world_clock_remote_list_item.xml b/res/layout/world_clock_remote_list_item.xml
index 48a1106..9a69cb5 100644
--- a/res/layout/world_clock_remote_list_item.xml
+++ b/res/layout/world_clock_remote_list_item.xml
@@ -42,7 +42,7 @@
android:layout_gravity="center"
android:baselineAligned="true"
android:gravity="center"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<LinearLayout
android:layout_width="wrap_content"
@@ -65,7 +65,7 @@
android:includeFontPadding="false"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white"
+ android:textColor="@color/white"
android:textSize="@dimen/city_widget_name_font_size" />
</FrameLayout>
@@ -82,7 +82,7 @@
android:includeFontPadding="false"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white"
+ android:textColor="@color/white"
android:textSize="@dimen/city_widget_name_font_size" />
</LinearLayout>
@@ -104,7 +104,7 @@
android:layout_gravity="center"
android:baselineAligned="true"
android:gravity="center"
- android:textColor="@color/clock_white" />
+ android:textColor="@color/white" />
<LinearLayout
android:layout_width="wrap_content"
@@ -127,7 +127,7 @@
android:includeFontPadding="false"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white"
+ android:textColor="@color/white"
android:textSize="@dimen/city_widget_name_font_size" />
</FrameLayout>
@@ -144,7 +144,7 @@
android:includeFontPadding="false"
android:singleLine="true"
android:textAllCaps="true"
- android:textColor="@color/clock_white"
+ android:textColor="@color/white"
android:textSize="@dimen/city_widget_name_font_size" />
</LinearLayout>
diff --git a/res/values-h420dp/dimens.xml b/res/values-h420dp/dimens.xml
index 401645c..5dbcc17 100644
--- a/res/values-h420dp/dimens.xml
+++ b/res/values-h420dp/dimens.xml
@@ -15,7 +15,5 @@
-->
<resources>
- <dimen name="alarm_time_font_size">64sp</dimen>
-
<dimen name="day_button_font_size">18sp</dimen>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/res/values-h470dp-v21/dimens.xml b/res/values-h470dp-v21/dimens.xml
index 28b7e30..a851004 100644
--- a/res/values-h470dp-v21/dimens.xml
+++ b/res/values-h470dp-v21/dimens.xml
@@ -23,10 +23,4 @@
<!-- Lollipop and later normal floating action button container height. -->
<dimen name="fab_height">88dp</dimen>
- <!-- Lollipop and later normal floating action button size. -->
- <dimen name="fab_button_size">@dimen/design_fab_size_normal</dimen>
-
- <!-- Padding for normal fab buttons.-->
- <dimen name="fab_button_padding">0dp</dimen>
-
-</resources>
\ No newline at end of file
+</resources>
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index ff14e29..0860a68 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -1,28 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
<!-- These resources are around just to allow their values to be customized
for different hardware and product builds. -->
<resources>
<dimen name="time_margin_top">24dip</dimen>
- <dimen name="dialpad_font_size">24sp</dimen>
<dimen name="timer_setup_delete_margin">12sp</dimen>
-
- <dimen name="actionbar_tab_padding">32dip</dimen>
+ <dimen name="timer_setup_digit_font_size">26sp</dimen>
<!-- Size of margin for circles. -->
<dimen name="analog_clock_margin">70dp</dimen>
@@ -30,7 +29,4 @@
<dimen name="circle_margin_top">0dp</dimen>
<dimen name="bottom_text_spacing_analog">5dp</dimen>
-
- <dimen name="medium_font_size">48sp</dimen>
-
</resources>
diff --git a/res/values-sw320dp-port/dimens.xml b/res/values-sw320dp-port/dimens.xml
index ebbdc0f..b3b3702 100644
--- a/res/values-sw320dp-port/dimens.xml
+++ b/res/values-sw320dp-port/dimens.xml
@@ -16,4 +16,5 @@
<resources>
<dimen name="no_alarms_size">120dp</dimen>
-</resources>
\ No newline at end of file
+ <dimen name="timer_setup_digit_font_size">40sp</dimen>
+</resources>
diff --git a/res/values-sw320dp/dimens.xml b/res/values-sw320dp/dimens.xml
index d64c205..cb60168 100644
--- a/res/values-sw320dp/dimens.xml
+++ b/res/values-sw320dp/dimens.xml
@@ -16,4 +16,5 @@
<resources>
<dimen name="big_font_size">60sp</dimen>
+ <dimen name="timer_setup_digit_font_size">32sp</dimen>
</resources>
\ No newline at end of file
diff --git a/res/values-sw360dp-land/dimens.xml b/res/values-sw360dp-land/dimens.xml
index 1ff913a..33bf986 100644
--- a/res/values-sw360dp-land/dimens.xml
+++ b/res/values-sw360dp-land/dimens.xml
@@ -1,24 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 The Android Open Source Project
+<!-- Copyright (C) 2016 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
+ 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
+ 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.
- -->
-
-<!--
- These resources are around just to allow their values to be customized
- for different hardware and product builds.
+ 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.
-->
+
<resources>
- <dimen name="timer_setup_font_size">48sp</dimen>
+ <dimen name="timer_setup_digit_font_size">40sp</dimen>
</resources>
diff --git a/res/values-sw360dp/dimens.xml b/res/values-sw360dp/dimens.xml
index 86141ed..683eda9 100644
--- a/res/values-sw360dp/dimens.xml
+++ b/res/values-sw360dp/dimens.xml
@@ -17,6 +17,6 @@
<resources>
<dimen name="big_font_size">70sp</dimen>
<dimen name="main_clock_font_size">80sp</dimen>
- <dimen name="timer_setup_font_size">56sp</dimen>
<dimen name="no_alarms_size">120dp</dimen>
-</resources>
\ No newline at end of file
+ <dimen name="timer_setup_digit_font_size">56sp</dimen>
+</resources>
diff --git a/res/values-sw600dp-land/dimens.xml b/res/values-sw600dp-land/dimens.xml
index 88bfc05..1793832 100644
--- a/res/values-sw600dp-land/dimens.xml
+++ b/res/values-sw600dp-land/dimens.xml
@@ -19,9 +19,8 @@
<resources>
<dimen name="time_margin_top">32dip</dimen>
- <dimen name="dialpad_font_size">42sp</dimen>
- <dimen name="timer_setup_font_size">78sp</dimen>
<dimen name="timer_setup_delete_margin">19sp</dimen>
+ <dimen name="timer_setup_digit_font_size">46sp</dimen>
<!-- Size of margin for circles. -->
<dimen name="circle_margin_top">0dp</dimen>
diff --git a/res/values/config.xml b/res/values-sw600dp/bools.xml
similarity index 78%
copy from res/values/config.xml
copy to res/values-sw600dp/bools.xml
index 749706d..608c2aa 100644
--- a/res/values/config.xml
+++ b/res/values-sw600dp/bools.xml
@@ -14,8 +14,6 @@
limitations under the License.
-->
-<!-- These resources are around just to allow their values to be customized
- for different hardware and product builds. -->
<resources>
- <bool name="config_rotateAlarmAlert">false</bool>
-</resources>
\ No newline at end of file
+ <bool name="rotateAlarmAlert">true</bool>
+</resources>
diff --git a/res/values-sw600dp/config.xml b/res/values-sw600dp/config.xml
deleted file mode 100644
index 9fbceac..0000000
--- a/res/values-sw600dp/config.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-** Copyright 2011, 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.
-*/
--->
-
-<!-- These resources are around just to allow their values to be customized
- for different hardware and product builds. -->
-<resources>
- <bool name="config_rotateAlarmAlert">true</bool>
-</resources>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index 6a7ecb9..b912546 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -28,7 +28,6 @@
<dimen name="main_clock_font_size">125sp</dimen>
<dimen name="big_font_size">115sp</dimen>
- <dimen name="medium_font_size">96sp</dimen>
<dimen name="label_font_size">18sp</dimen>
<dimen name="alarm_label_size">18sp</dimen>
<dimen name="day_button_font_size">20sp</dimen>
@@ -45,12 +44,10 @@
<dimen name="label_margin_big">8dp</dimen>
- <dimen name="dialpad_font_size">56sp</dimen>
- <dimen name="timer_setup_font_size">86sp</dimen>
- <dimen name="timer_setup_delete_margin">21sp</dimen>
-
<!-- Specified in sp to match the top margin of the time on the adjacent world clock tab. -->
<dimen name="timer_setup_top_margin">24sp</dimen>
+ <dimen name="timer_setup_delete_margin">21sp</dimen>
+ <dimen name="timer_setup_digit_font_size">86sp</dimen>
<!-- Size of margin for circles. -->
<dimen name="circle_margin_top">48dp</dimen>
diff --git a/res/values-sw720dp-land/dimens.xml b/res/values-sw720dp-land/dimens.xml
index 5d70c21..c6665b6 100644
--- a/res/values-sw720dp-land/dimens.xml
+++ b/res/values-sw720dp-land/dimens.xml
@@ -17,8 +17,6 @@
<!-- These resources are around just to allow their values to be customized
for different hardware and product builds. -->
<resources>
-
- <dimen name="timer_setup_font_size">78sp</dimen>
<dimen name="timer_setup_delete_margin">19sp</dimen>
-
+ <dimen name="timer_setup_digit_font_size">76sp</dimen>
</resources>
diff --git a/res/values-sw720dp/dimens.xml b/res/values-sw720dp/dimens.xml
index 93b13bd..92f495a 100644
--- a/res/values-sw720dp/dimens.xml
+++ b/res/values-sw720dp/dimens.xml
@@ -17,12 +17,10 @@
<!-- These resources are around just to allow their values to be customized
for different hardware and product builds. -->
<resources>
-
<dimen name="big_font_size">152sp</dimen>
- <dimen name="medium_font_size">80sp</dimen>
<dimen name="analog_clock_size">312dp</dimen>
<!-- The maximum size of the font for the time in widgets. -->
<dimen name="widget_max_clock_font_size">138dp</dimen>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/res/values-v21/colors.xml b/res/values-v21/colors.xml
deleted file mode 100644
index 4e58dae..0000000
--- a/res/values-v21/colors.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <!-- Values for custom notification -->
- <color name="notification_divider">@color/black_16p</color>
- <color name="control_highlight_color">#f3dbdbdb</color>
-
-</resources>
\ No newline at end of file
diff --git a/res/values-v21/dimens.xml b/res/values-v21/dimens.xml
index f3dfd80..8068a4a 100644
--- a/res/values-v21/dimens.xml
+++ b/res/values-v21/dimens.xml
@@ -22,13 +22,7 @@
<!-- Lollipop and later mini floating action button container height. -->
<dimen name="fab_height">56dp</dimen>
- <!-- Lollipop and later mini floating action button size. -->
- <dimen name="fab_button_size">@dimen/design_fab_size_mini</dimen>
-
- <!-- Padding for mini fab buttons.-->
- <dimen name="fab_button_padding">8dp</dimen>
-
<!-- Size of margin between icon and text / title text. -->
<dimen name="notification_content_margin_start">0dp</dimen>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/res/values-v21/styles.xml b/res/values-v21/styles.xml
index e6770d8..f1dca16 100644
--- a/res/values-v21/styles.xml
+++ b/res/values-v21/styles.xml
@@ -1,58 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
-<resources xmlns:tools="http://schemas.android.com/tools">
+<resources>
- <style name="SettingsAlertDialogTheme" parent="android:Theme.Material.Dialog.Alert">
- <item name="android:colorAccent">?attr/colorAccent</item>
- </style>
-
- <!-- Custom notification content styles -->
- <!-- copied TextAppearance.Material.Button -->
- <style name="TextAppearance.StatusBar.EventContent.Action">
+ <style name="body">
<item name="android:textSize">14sp</item>
+ <item name="android:textColor">@color/white</item>
<item name="android:fontFamily">sans-serif-medium</item>
- <item name="android:textAllCaps">true</item>
- <item name="android:textColor">@color/black_54p</item>
- </style>
-
- <style name="TextAppearance.StatusBar.EventContent" tools:ignore="PrivateResource">
- <item name="android:textSize">14sp</item>
- <item name="android:textColor">@color/black_54p</item>
- </style>
- <style name="TextAppearance.StatusBar.EventContent.Title" tools:ignore="PrivateResource">
- <item name="android:textSize">16sp</item>
- <item name="android:textColor">@color/black_87p</item>
- </style>
-
- <style name="TimePickerTheme" parent="Theme.AppCompat.Dialog">
- <item name="android:textColorPrimary">?attr/colorAccent</item>
- <item name="android:textColorPrimaryInverse">?android:attr/textColorPrimary</item>
- <item name="android:textColorSecondaryInverse">@color/white</item>
- <item name="android:timePickerStyle">@style/TimePickerStyle</item>
- <item name="android:windowBackground">@color/time_picker_gray</item>
-
- <!-- Attributes from support.v7.appcompat -->
- <item name="colorAccent">@color/color_accent</item>
- </style>
-
- <style name="TimePickerStyle" parent="android:Widget.Material.TimePicker" >
- <item name="android:headerBackground">@android:color/transparent</item>
- <item name="android:numbersBackgroundColor">@android:color/transparent</item>
- <item name="android:numbersTextColor">@color/white</item>
</style>
<style name="PrimaryLabelTextAppearance" parent="PrimaryLabelTextParentAppearance">
@@ -63,12 +31,4 @@
<item name="android:fontFamily">sans-serif-medium</item>
</style>
- <style name="CitiesTheme" parent="BaseActivityTheme">
- <item name="android:fastScrollThumbDrawable">@drawable/fastscroll_thumb</item>
- <item name="android:fastScrollTrackDrawable">@drawable/fastscroll_track</item>
- <item name="actionBarStyle">@style/CitiesActionBarStyle</item>
- <item name="windowActionBar">true</item>
- <item name="windowNoTitle">false</item>
- </style>
-
</resources>
diff --git a/res/values-v22/drawable.xml b/res/values-v22/drawable.xml
index 11ff430..22234f3 100644
--- a/res/values-v22/drawable.xml
+++ b/res/values-v22/drawable.xml
@@ -21,5 +21,9 @@
<item name="ic_tab_clock" type="drawable">@drawable/ic_tab_clock_animated</item>
<item name="ic_tab_timer" type="drawable">@drawable/ic_tab_timer_animated</item>
<item name="ic_tab_stopwatch" type="drawable">@drawable/ic_tab_stopwatch_animated</item>
+ <item name="ic_ringtone_active" type="drawable">@drawable/ic_ringtone_active_animated</item>
+
+ <item name="ic_caret_up" type="drawable">@drawable/ic_caret_up_animation</item>
+ <item name="ic_caret_down" type="drawable">@drawable/ic_caret_down_animation</item>
</resources>
\ No newline at end of file
diff --git a/res/values-v23/styles.xml b/res/values-v23/styles.xml
index 8994fdd..1ac2abd 100644
--- a/res/values-v23/styles.xml
+++ b/res/values-v23/styles.xml
@@ -1,23 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2016 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
<resources>
- <style name="SettingsAlertDialogTheme" parent="android:ThemeOverlay.Material.Dialog.Alert" />
-
<style name="widget_big_thin" parent="big_thin">
<item name="android:fontFamily">sans-serif-light</item>
<item name="android:shadowRadius">@dimen/widget_shadow_radius</item>
@@ -33,14 +32,4 @@
<item name="android:shadowDy">@dimen/widget_shadow_dy</item>
</style>
- <style name="timer_setup_digit">
- <item name="android:ellipsize">none</item>
- <item name="android:fontFamily">sans-serif-thin</item>
- <item name="android:fontFeatureSettings">tnum</item>
- <item name="android:includeFontPadding">false</item>
- <item name="android:singleLine">true</item>
- <item name="android:textColor">@color/clock_white</item>
- <item name="android:textSize">@dimen/timer_setup_font_size</item>
- </style>
-
</resources>
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/values-w360dp-h420dp/bools.xml
similarity index 81%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/values-w360dp-h420dp/bools.xml
index 114006d..0708712 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/values-w360dp-h420dp/bools.xml
@@ -14,6 +14,6 @@
limitations under the License.
-->
-<pathInterpolator
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+<resources>
+ <bool name="showTabLabel">true</bool>
+</resources>
diff --git a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml b/res/values-w470dp-land/bools.xml
similarity index 81%
copy from res/interpolator-v22/ic_lap_animation_interpolator_2.xml
copy to res/values-w470dp-land/bools.xml
index 114006d..958e07c 100644
--- a/res/interpolator-v22/ic_lap_animation_interpolator_2.xml
+++ b/res/values-w470dp-land/bools.xml
@@ -14,6 +14,7 @@
limitations under the License.
-->
-<pathInterpolator
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:pathData="M 0.0,0.0 c 0.33333333,0.0 0.2,1.0 1.0,1.0" />
+<resources>
+ <bool name="showTabLabel">true</bool>
+ <bool name="showTabHorizontally">true</bool>
+</resources>
diff --git a/res/values-w470dp-v21/dimens.xml b/res/values-w470dp-v21/dimens.xml
index d011461..f50ef9f 100644
--- a/res/values-w470dp-v21/dimens.xml
+++ b/res/values-w470dp-v21/dimens.xml
@@ -23,10 +23,4 @@
<!-- Lollipop and later normal floating action button container height. -->
<dimen name="fab_height">88dp</dimen>
- <!-- Lollipop and later normal floating action button size. -->
- <dimen name="fab_button_size">@dimen/design_fab_size_normal</dimen>
-
- <!-- Padding for normal fab buttons.-->
- <dimen name="fab_button_padding">0dp</dimen>
-
-</resources>
\ No newline at end of file
+</resources>
diff --git a/res/values/config.xml b/res/values/bools.xml
similarity index 79%
rename from res/values/config.xml
rename to res/values/bools.xml
index 749706d..3683454 100644
--- a/res/values/config.xml
+++ b/res/values/bools.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
-<!-- These resources are around just to allow their values to be customized
- for different hardware and product builds. -->
<resources>
- <bool name="config_rotateAlarmAlert">false</bool>
-</resources>
\ No newline at end of file
+ <bool name="rotateAlarmAlert">false</bool>
+ <bool name="showTabLabel">false</bool>
+ <bool name="showTabHorizontally">false</bool>
+</resources>
diff --git a/res/values/cities.xml b/res/values/cities.xml
index 23e7b0b..b0cf749 100644
--- a/res/values/cities.xml
+++ b/res/values/cities.xml
@@ -15,7 +15,10 @@
limitations under the License
-->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+<resources
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="Typos">
<!-- This array enumerates the resource keys to all cities available to the app. -->
<array name="city_ids" translatable="false">
diff --git a/res/values/colors.xml b/res/values/colors.xml
index c768a73..19db824 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -1,56 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
<resources>
- <color name="ampm_text_color">#99FFFFFF</color>
- <color name="dialog_gray">#28FFFFFF</color>
<color name="black">#FF000000</color>
<color name="black_87p">#DE000000</color>
<color name="black_54p">#8A000000</color>
- <color name="black_16p">#28000000</color>
<color name="white">#FFFFFFFF</color>
+ <color name="white_08p">#14FFFFFF</color>
<color name="white_63p">#A0FFFFFF</color>
<color name="no_alarms">#4CFFFFFF</color>
<color name="transparent">#00000000</color>
- <color name="color_accent">#DA4336</color>
<color name="hairline">#28FFFFFF</color>
- <color name="clock_white">#FFFFFF</color>
<color name="clock_gray">#B3FFFFFF</color>
<color name="default_background">#1A237E</color>
- <color name="status_bar">#66000000</color>
-
- <color name="time_picker_gray">#212121</color>
-
- <!-- Values for custom notification -->
- <color name="notif_text_grey">#9C9C9C</color>
- <color name="control_highlight_color">#42FFFFFF</color>
- <color name="notification_bg">#181818</color>
- <color name="notification_icon_bg">#1C3742</color>
- <color name="notification_pressed">#333333</color>
- <color name="notification_divider">#272727</color>
<!-- shadowColor for widget text -->
<color name="widget_shadow_color">#67000000</color>
-
- <!-- Redefine the fallback color used by preference-v14 to match our accent color. -->
- <color name="preference_fallback_accent_color">@color/color_accent</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 94e813f..06931fa 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -1,18 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
<!-- These resources are around just to allow their values to be customized
for different hardware and product builds. -->
@@ -31,8 +32,6 @@
<!-- Analog clock size in the the screen saver -->
<dimen name="bottom_text_size">16sp</dimen>
- <dimen name="actionbar_tab_padding">0dip</dimen>
-
<dimen name="alarm_text_font_size">16sp</dimen>
<dimen name="circletimer_dot_size">12dp</dimen>
<dimen name="circletimer_circle_size">4dp</dimen>
@@ -45,15 +44,15 @@
<dimen name="alarm_lockscreen_bottom_margin">40dp</dimen>
<dimen name="main_clock_font_size">64sp</dimen>
+ <dimen name="main_clock_digital_font_size">90sp</dimen>
+ <dimen name="main_clock_digital_padding">24dp</dimen>
<dimen name="big_font_size">32sp</dimen>
- <dimen name="medium_font_size">56sp</dimen>
- <dimen name="small_font_size">32sp</dimen>
<dimen name="label_font_size">16sp</dimen>
<dimen name="header_font_size">24sp</dimen>
<dimen name="day_button_font_size">16sp</dimen>
- <dimen name="alarm_time_font_size">48sp</dimen>
<dimen name="alarm_info_font_size">36sp</dimen>
<dimen name="no_alarm_font_size">16sp</dimen>
+ <dimen name="timer_setup_digit_font_size">32sp</dimen>
<dimen name="alarm_picker_dialog_horizontal_margin">30dp</dimen>
<dimen name="alarm_picker_dialog_vertical_margin">20dp</dimen>
@@ -73,12 +72,8 @@
<dimen name="label_margin_big">4dp</dimen>
- <dimen name="dialpad_font_size">36sp</dimen>
- <!--padding should be (in dip) ~ 60% dialpad_font_size -->
- <dimen name="timer_setup_font_size">38sp</dimen>
<!--margin should be ~ half timer_setup_font_size -->
<dimen name="timer_setup_delete_margin">14sp</dimen>
- <dimen name="timer_setup_delete_padding">12dp</dimen>
<!-- Specified in sp to match the top margin of the time on the adjacent world clock tab. -->
<dimen name="timer_setup_top_margin">18sp</dimen>
@@ -90,22 +85,8 @@
<dimen name="analog_clock_margin">60dp</dimen>
<dimen name="analog_clock_size">294dp</dimen>
- <!-- The width and height of the notification icon -->
- <dimen name="notification_icon_size">64dp</dimen>
- <!-- Size of notification text (see TextAppearance.StatusBar.EventContent) -->
- <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. -->
- <!-- Bottom padding for alarm lock screen hint text -->
-
<!-- Size of time zone analog clocks in world clock. -->
<dimen name="world_clock_analog_size">100dp</dimen>
- <!-- Size of digital world clock font in landscape. -->
- <dimen name="world_clock_digital_font_size">40sp</dimen>
<dimen name="min_analog_widget_size">110dp</dimen>
@@ -135,12 +116,6 @@
<!-- The minimum height/width of any touch target -->
<dimen name="touch_target_min_size">48dp</dimen>
- <!-- Dimens for cities fastscroll -->
- <dimen name="fastscroll_thumb_height">48dp</dimen>
- <dimen name="fastscroll_thumb_width">8dp</dimen>
- <dimen name="fastscroll_preview_padding">8dp</dimen>
- <dimen name="fastscroll_track_width">8dp</dimen>
-
<!-- Dimens for drawable padding on Alarms tab -->
<dimen name="alarm_horizontal_padding">16dp</dimen>
@@ -150,9 +125,6 @@
<!-- Padding between checkbox and text. -->
<dimen name="checkbox_start_padding">12dp</dimen>
- <!-- Dimens for taller rows on Alarms tab -->
- <dimen name="tall_row_height">58dp</dimen>
-
<!-- Kitkat and later floating action button elevation. -->
<dimen name="fab_elevation">8dp</dimen>
@@ -161,11 +133,4 @@
<!-- KitKat floating action button container height; see -v21 folder for newer platforms. -->
<dimen name="fab_height">96dp</dimen>
-
- <!-- KitKat floating action button size; see -v21 folder for newer platforms. -->
- <dimen name="fab_button_size">@dimen/design_fab_size_normal</dimen>
-
- <!-- When fab buttons decrease in size this padding is used to scale the icon appropriately. -->
- <dimen name="fab_button_padding">0dp</dimen>
-
</resources>
diff --git a/res/values/donottranslate_events.xml b/res/values/donottranslate_events.xml
index d48e362..62b4cc8 100644
--- a/res/values/donottranslate_events.xml
+++ b/res/values/donottranslate_events.xml
@@ -38,6 +38,17 @@
<string name="action_show">Show</string>
<string name="action_add_minute">Add Minute</string>
<string name="action_lap">Lap</string>
+ <string name="action_enable">Enable</string>
+ <string name="action_disable">Disable</string>
+ <string name="action_expand">Expand</string>
+ <string name="action_collapse">Collapse</string>
+ <string name="action_expand_implied">Expand Implied</string>
+ <string name="action_collapse_implied">Collapse Implied</string>
+ <string name="action_set_time">Set Time</string>
+ <string name="action_set_label">Set Label</string>
+ <string name="action_set_ringtone">Set Ringtone</string>
+ <string name="action_toggle_vibrate">Toggle Vibrate</string>
+ <string name="action_toggle_repeat_days">Toggle Repeat Days</string>
<!-- Label names for events describe the entity responsible for the event. -->
<string name="label_deskclock">DeskClock</string>
diff --git a/res/values/drawable.xml b/res/values/drawable.xml
index 6e3ac48..f010c4b 100644
--- a/res/values/drawable.xml
+++ b/res/values/drawable.xml
@@ -21,5 +21,9 @@
<item name="ic_tab_clock" type="drawable">@drawable/ic_tab_clock_static</item>
<item name="ic_tab_timer" type="drawable">@drawable/ic_tab_timer_static</item>
<item name="ic_tab_stopwatch" type="drawable">@drawable/ic_tab_stopwatch_static</item>
+ <item name="ic_ringtone_active" type="drawable">@drawable/ic_ringtone_active_static</item>
+
+ <item name="ic_caret_up" type="drawable">@drawable/ic_caret_up_static</item>
+ <item name="ic_caret_down" type="drawable">@drawable/ic_caret_down_static</item>
</resources>
\ No newline at end of file
diff --git a/res/values/integers.xml b/res/values/integers.xml
index 58afbb9..6c0ecd4 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -15,7 +15,6 @@
-->
<resources>
- <integer name="chevron_rotate_180">180</integer>
<integer name="gutter_width_percent">4</integer>
<integer name="guttered_content_width_percent">92</integer>
</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 58fe640..deccf92 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,28 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2007 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+<resources
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"
+ xmlns:tools="http://schemas.android.com/tools">
<!-- Label shown on launcher icon -->
<!-- Label for the this application displayed on-screen when this application must be represented to the user. -->
<string name="app_label">Clock</string>
- <!-- Context Menu Item on Alarm Settings screen: Delete alarm -->
- <string name="delete_alarm">Delete alarm</string>
-
<!-- Setting label on Set alarm screen: Label -->
<string name="label">Label</string>
@@ -35,12 +35,36 @@
<!-- Setting labels on Set alarm screen: Repeat -->
<string name="alarm_repeat">Repeat</string>
- <!-- Setting labels on Set alarm screen: Select alarm ringtone -->
- <string name="alert">Alarm Ringtone</string>
+ <!-- Setting labels on Set alarm screen: Delete [CHAR LIMIT=15] -->
+ <string name="delete">Delete</string>
- <!-- Title of default ringtone played when an alarm triggers. -->
+ <!-- Title of default ringtone played when an alarm triggers [CHAR LIMIT=30] -->
<string name="default_alarm_ringtone_title">Default alarm sound</string>
+ <!-- Title for alarm ringtone picker screen [CHAR LIMIT=30] -->
+ <string name="alarm_sound">Alarm sound</string>
+
+ <!-- Title for timer ringtone picker screen [CHAR LIMIT=30] -->
+ <string name="timer_sound">Timer sound</string>
+
+ <!-- Label for adding a new ringtone sound [CHAR LIMIT=30]-->
+ <string name="add_new_sound">Add new</string>
+
+ <!-- Label for removing a ringtone sound [CHAR LIMIT=30] -->
+ <string name="remove_sound">Remove</string>
+
+ <!-- Text that explains what happens to alarms and timers after removing the custom ringtone sound they use [CHAR LIMIT=NONE] -->
+ <string name="confirm_remove_custom_ringtone">Alarms and timers using this sound will play the default sound instead.</string>
+
+ <!-- Label for "Your sounds" section on ringtone selection screen [CHAR LIMIT=60] -->
+ <string name="your_sounds">Your sounds</string>
+
+ <!-- Label for "Device sounds" section on ringtone selection screen [CHAR LIMIT=60] -->
+ <string name="device_sounds">Device sounds</string>
+
+ <!-- Text that explains that the ringtone is inaccessible to the app [CHAR LIMIT=NONE] -->
+ <string name="custom_ringtone_lost_permissions">The sound content cannot be accessed.</string>
+
<!-- Title of default ringtone played when a timer expires. -->
<string name="default_timer_ringtone_title">Timer Expired</string>
@@ -115,11 +139,21 @@
-->
<skip />
- <!-- Timer notification: how long from now until timer goes off. -->
+ <!-- Timer notification: how long from now until timer goes off.
+ (Also used for timer accessibility announcements.) -->
<string name="timer_notifications_less_min">Less than a minute remaining</string>
<string name="timer_notifications_hours"><xliff:g id="HOURS" example="2 hours">%1$s</xliff:g><xliff:g id="REMAINING" example="remaining"> %3$s</xliff:g></string>
<string name="timer_notifications_minutes"><xliff:g id="MINUTES" example="2 minutes">%2$s</xliff:g><xliff:g id="REMAINING" example="remaining"> %3$s</xliff:g></string>
- <string name="timer_notifications_hours_minutes"><xliff:g id="HOURS" example="2 hours">%1$s</xliff:g> <xliff:g id="MINUTES" example="2 minutes">%2$s</xliff:g> remaining</string>
+ <string name="timer_notifications_hours_minutes"><xliff:g id="HOURS" example="2 hours">%1$s</xliff:g><xliff:g id="MINUTES" example="2 minutes"> %2$s</xliff:g> remaining</string>
+ <string name="timer_notifications_seconds"><xliff:g id="SECONDS" example="2 seconds">%4$s</xliff:g><xliff:g id="REMAINING" example="remaining"> %3$s</xliff:g></string>
+ <string name="timer_notifications_minutes_seconds"><xliff:g id="MINUTES" example="2 minutes">%2$s</xliff:g> <xliff:g id="SECONDS" example="2 seconds"> %4$s</xliff:g><xliff:g id="REMAINING" example="remaining"> %3$s</xliff:g></string>
+ <string name="timer_notifications_hours_seconds"><xliff:g id="HOURS" example="2 hours">%1$s</xliff:g> <xliff:g id="SECONDS" example="2 seconds"> %4$s</xliff:g><xliff:g id="REMAINING" example="remaining"> %3$s</xliff:g></string>
+ <string name="timer_notifications_hours_minutes_seconds"><xliff:g id="HOURS" example="2 hours">%1$s</xliff:g><xliff:g id="MINUTES" example="2 minutes"> %2$s</xliff:g><xliff:g id="SECONDS" example="2 seconds"> %4$s</xliff:g><xliff:g id="REMAINING" example="remaining"> %3$s</xliff:g></string>
+
+ <!-- Timer accessibility announcements -->
+ <string name="timer_accessibility_one_minute_added">One minute added to timer, <xliff:g id="TIME" example="5 hours remaining">%1$s</xliff:g></string>
+ <string name="timer_accessibility_stopped">Timer paused, <xliff:g id="TIME" example="5 hours remaining">%1$s</xliff:g></string>
+ <string name="timer_accessibility_started">Timer running, <xliff:g id="TIME" example="5 hours remaining">%1$s</xliff:g></string>
<!--
Verb inflection to use for single time units remaining
@@ -152,7 +186,7 @@
<item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> days</item>
</plurals>
- <!-- Alarm confirmation toast and timer notification: hours -->
+ <!-- Alarm confirmation toast, timer notification and world clock ahead/behind: hours -->
<plurals name="hours">
<!-- Duration for one hour -->
<item quantity="one">1 hour</item>
@@ -160,6 +194,13 @@
<item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> hours</item>
</plurals>
+ <plurals name="hours_short">
+ <!-- Duration for one hour [CHAR LIMIT=8] -->
+ <item quantity="one">1 hr</item>
+ <!-- Duration for more than one hour [CHAR LIMIT=8]-->
+ <item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> hr</item>
+ </plurals>
+
<!-- Alarm confirmation toast and timer notification: minutes -->
<plurals name="minutes">
<!-- Duration for one minute -->
@@ -168,6 +209,21 @@
<item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> minutes</item>
</plurals>
+ <plurals name="minutes_short">
+ <!-- Duration for one minute [CHAR LIMIT=8] -->
+ <item quantity="one">1 min</item>
+ <!-- Duration for more than one minute [CHAR LIMIT=8] -->
+ <item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> min</item>
+ </plurals>
+
+ <!-- Timer accessibility announcement -->
+ <plurals name="seconds">
+ <!-- Duration for one second -->
+ <item quantity="one">1 second</item>
+ <!-- Duration for more than one minute -->
+ <item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> seconds</item>
+ </plurals>
+
<!-- Repeat options that appear under an alarm on main Alarm Clock
screen to identify repetition schedule: special case for when
the alarm is set to repeat every day -->
@@ -194,32 +250,9 @@
<!-- Setting title for changing the snooze duration. -->
<string name="snooze_duration_title">Snooze length</string>
- <plurals name="snooze_duration">
- <!-- Duration for one minute -->
- <item quantity="one">1 minute</item>
- <!-- Duration for more than one minute -->
- <item quantity="other"><xliff:g id="number" example="7">%s</xliff:g> minutes</item>
- </plurals>
-
- <plurals name="snooze_picker_label">
- <!-- Duration for one minute -->
- <item quantity="one">minute</item>
- <!-- Duration for more than one minute -->
- <item quantity="other">minutes</item>
- </plurals>
-
<!-- Setting title for changing the crescendo duration. -->
<string name="crescendo_duration_title">Gradually increase volume</string>
- <!-- Summary value for changing the crescendo duration to 0. -->
- <string name="no_crescendo_duration">Off</string>
-
- <!-- Summary value for changing the crescendo duration. -->
- <string name="crescendo_duration"><xliff:g id="number" example="7">%s</xliff:g> seconds</string>
-
- <!-- Setting label when changing the crescendo duration. -->
- <string name="crescendo_picker_label">seconds</string>
-
<!-- Auto silence preference title -->
<string name="auto_silence_title">Silence after</string>
@@ -259,6 +292,111 @@
<item>-1</item> <!-- Off -->
</string-array>
+ <!-- Entries listed in the ListPreference when invoking gradually increase
+ volume preferences. -->
+ <string-array name="crescendo_entries">
+ <item>Off</item>
+ <item>5 seconds</item>
+ <item>10 seconds</item>
+ <item>15 seconds</item>
+ <item>20 seconds</item>
+ <item>25 seconds</item>
+ <item>30 seconds</item>
+ <item>35 seconds</item>
+ <item>40 seconds</item>
+ <item>45 seconds</item>
+ <item>50 seconds</item>
+ <item>55 seconds</item>
+ <item>60 seconds</item>
+ </string-array>
+
+ <!-- Values that are retrieved from the ListPreference. These must match
+ the crescendo_entries above. -->
+ <string-array name="crescendo_values" translatable="false">
+ <item>0</item>
+ <item>5</item>
+ <item>10</item>
+ <item>15</item>
+ <item>20</item>
+ <item>25</item>
+ <item>30</item>
+ <item>35</item>
+ <item>40</item>
+ <item>45</item>
+ <item>50</item>
+ <item>55</item>
+ <item>60</item>
+ </string-array>
+
+ <!-- Entries listed in the ListPreference when invoking the snooze duration preference -->
+ <string-array name="snooze_duration_entries">
+ <item>1 minute</item>
+ <item>2 minutes</item>
+ <item>3 minutes</item>
+ <item>4 minutes</item>
+ <item>5 minutes</item>
+ <item>6 minutes</item>
+ <item>7 minutes</item>
+ <item>8 minutes</item>
+ <item>9 minutes</item>
+ <item>10 minutes</item>
+ <item>11 minutes</item>
+ <item>12 minutes</item>
+ <item>13 minutes</item>
+ <item>14 minutes</item>
+ <item>15 minutes</item>
+ <item>16 minutes</item>
+ <item>17 minutes</item>
+ <item>18 minutes</item>
+ <item>19 minutes</item>
+ <item>20 minutes</item>
+ <item>21 minutes</item>
+ <item>22 minutes</item>
+ <item>23 minutes</item>
+ <item>24 minutes</item>
+ <item>25 minutes</item>
+ <item>26 minutes</item>
+ <item>27 minutes</item>
+ <item>28 minutes</item>
+ <item>29 minutes</item>
+ <item>30 minutes</item>
+ </string-array>
+
+ <!-- Values that are retrieved from the ListPreference. These must match
+ the snooze_duration_entries above. -->
+ <string-array name="snooze_duration_values" translatable="false">
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ <item>4</item>
+ <item>5</item>
+ <item>6</item>
+ <item>7</item>
+ <item>8</item>
+ <item>9</item>
+ <item>10</item>
+ <item>11</item>
+ <item>12</item>
+ <item>13</item>
+ <item>14</item>
+ <item>15</item>
+ <item>16</item>
+ <item>17</item>
+ <item>18</item>
+ <item>19</item>
+ <item>20</item>
+ <item>21</item>
+ <item>22</item>
+ <item>23</item>
+ <item>24</item>
+ <item>25</item>
+ <item>26</item>
+ <item>27</item>
+ <item>28</item>
+ <item>29</item>
+ <item>30</item>
+ </string-array>
+
<!-- Week start day preference title. -->
<string name="week_start_title">Start week on</string>
@@ -295,7 +433,10 @@
<string name="silent_default_alarm_ringtone">Default alarm ringtone is silent</string>
<!-- Text for action that shows the UI that changes the default alarm ringtone. [CHAR LIMIT=20] -->
- <string name="change_default_alarm_ringtone">Change</string>
+ <string name="change_setting_action">Change</string>
+
+ <!-- Text to display when app notifications are blocked. [CHAR LIMIT=50] -->
+ <string name="app_notifications_blocked">Clock notifications are blocked</string>
<!-- Text to display when do-not-disturb is blocking alarms. [CHAR LIMIT=60] -->
<string name="alarms_blocked_by_dnd">Device is set to total silence</string>
@@ -308,7 +449,7 @@
<string-array name="volume_button_setting_entries">
<item>Snooze</item>
<item>Dismiss</item>
- <item>Do nothing</item>
+ <item>Control volume</item>
</string-array>
<!-- Values for the side-button setting. -->
@@ -382,10 +523,14 @@
<string name="minutes_label">m</string>
<!-- Abbreviation for temporal seconds [CHAR LIMIT=1] -->
<string name="seconds_label">s</string>
- <!-- Accessibility strings -->
- <string name="hours_label_description">hours</string>
- <string name="minutes_label_description">minutes</string>
- <string name="seconds_label_description">seconds</string>
+
+ <!-- Accessibility description for new timer setup duration (e.g. 1 hour, 10 minutes, 30 seconds). [CHAR LIMIT=NONE] -->
+ <string name="timer_setup_description"><xliff:g id="hours" example="1 hour">%1$s</xliff:g>, <xliff:g id="minutes" example="15 minutes">%2$s</xliff:g>, <xliff:g id="seconds" example="30 seconds">%3$s</xliff:g></string>
+
+ <!-- Formats the lap number display. Allows for control of the symbol preceding the lap number. [CHAR LIMIT=5] -->
+ <string name="lap_number_single_digit"># <xliff:g id="lapNumber">%d</xliff:g></string>
+ <!-- Formats the lap number display. Allows for control of the symbol preceding the lap number. [CHAR LIMIT=5] -->
+ <string name="lap_number_double_digit"># <xliff:g id="lapNumber">%02d</xliff:g></string>
<!-- Stopwatch share strings -->
<!-- Sentence within the message created to share the total time recorded within the stopwatch -->
@@ -395,26 +540,6 @@
<!-- 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>
- <!-- Stopwatch accessibility strings -->
- <plurals name="Nhours_description">
- <!-- 1 hour -->
- <item quantity="one">1 hour</item>
- <!-- more -->
- <item quantity="other"><xliff:g id="number" example="7">%d</xliff:g> hours</item>
- </plurals>
- <plurals name="Nminutes_description">
- <!-- 1 minute -->
- <item quantity="one">1 minute</item>
- <!-- more -->
- <item quantity="other"><xliff:g id="number" example="7">%d</xliff:g> minutes</item>
- </plurals>
- <plurals name="Nseconds_description">
- <!-- 1 second -->
- <item quantity="one">1 second</item>
- <!-- more -->
- <item quantity="other"><xliff:g id="number" example="7">%d</xliff:g> seconds</item>
- </plurals>
-
<!-- timer strings -->
<!-- Describes the purpose of the button to add a new timer -->
<string name="timer_add_timer">Add Timer</string>
@@ -426,6 +551,8 @@
<string name="timer_descriptive_delete">Delete <xliff:g id="number_string">%s</xliff:g></string>
<!-- Describes the purpose of the button increase the remaining time on a timer by one minute. -->
<string name="timer_plus_one">Add 1 Minute</string>
+ <!-- Label for adding one minute to a timer. [CHAR LIMIT=10] -->
+ <string name="timer_add_minute">+ 1:00</string>
<!-- Like "timer_plus_one", but with 'minute' abbreviated for the notification. -->
<string name="timer_plus_1_min">Add 1 min</string>
<!-- Describes the purpose of the button to stop the timer. -->
@@ -461,6 +588,10 @@
<!-- Describes the purpose of the notification button to reset all running timers. [CHAR LIMIT=31] -->
<string name="timer_reset_all">Reset all timers</string>
+ <string name="hours_minutes_seconds"><xliff:g id="hours" example="2">%d</xliff:g>:<xliff:g id="minutes" example="3">%02d</xliff:g>:<xliff:g id="seconds" example="44">%02d</xliff:g></string>
+ <string name="minutes_seconds"><xliff:g id="minutes" example="3">%d</xliff:g>:<xliff:g id="seconds" example="44">%02d</xliff:g></string>
+ <string name="seconds"><xliff:g id="seconds" example="44">%d</xliff:g></string>
+
<!-- Jocular content that user may append when sharing the lap times -->
<string-array name="sw_share_strings" translatable="true">
<item>You\'re quite the speed demon.</item>
@@ -485,6 +616,8 @@
<string name="clock_settings">Clock</string>
<!-- Header for a Clock Dream Setting referring to choosing analog or digital style -->
<string name="clock_style">Style</string>
+ <!-- Header for a setting referring to showing seconds on the main clock -->
+ <string name="display_clock_seconds_pref">Display time with seconds</string>
<!-- Title for preference to change date & time -->
<string name="open_date_settings">Change date \u0026 time</string>
@@ -512,11 +645,6 @@
<!-- Title in a list dialog box to pick a time zone for the user's home -->
<string name="home_time_zone_title">Home time zone</string>
- <!-- Textual content of the button to discard the current dialog values and close the dialog -->
- <string name="time_picker_cancel">Cancel</string>
- <!-- Textual content of the button to update an alarm with the current dialog values -->
- <string name="time_picker_set">OK</string>
-
<!--
Accessibility string read when a city checkbox is checked.
Ex. "Ann Arbor checked"
@@ -529,7 +657,7 @@
<string name="city_unchecked"><xliff:g id="city_name">%s</xliff:g> unchecked</string>
<!-- Choices for timezones, must be kept in sync with timezone_values. CHAR LIMIT=25] -->
- <string-array name="timezone_labels">
+ <string-array name="timezone_labels" tools:ignore="Typos">
<item>"Marshall Islands"</item>
<item>"Midway Island"</item>
<item>"Hawaii"</item>
@@ -619,92 +747,92 @@
<!-- Choices for timezones, must be kept in sync with timezone_values. -->
- <string-array name="timezone_values" translatable="false">
- <item>"Pacific/Majuro"</item>
- <item>"Pacific/Midway"</item>
- <item>"Pacific/Honolulu"</item>
- <item>"America/Anchorage"</item>
- <item>"America/Los_Angeles"</item>
- <item>"America/Tijuana"</item>
- <item>"America/Phoenix"</item>
- <item>"America/Chihuahua"</item>
- <item>"America/Denver"</item>
- <item>"America/Costa_Rica"</item>
- <item>"America/Chicago"</item>
- <item>"America/Mexico_City"</item>
- <item>"America/Regina"</item>
- <item>"America/Bogota"</item>
- <item>"America/New_York"</item>
- <item>"America/Caracas"</item>
- <item>"America/Barbados"</item>
- <item>"America/Halifax"</item>
- <item>"America/Manaus"</item>
- <item>"America/Santiago"</item>
- <item>"America/St_Johns"</item>
- <item>"America/Sao_Paulo"</item>
- <item>"America/Argentina/Buenos_Aires"</item>
- <item>"America/Godthab"</item>
- <item>"America/Montevideo"</item>
- <item>"Atlantic/South_Georgia"</item>
- <item>"Atlantic/Azores"</item>
- <item>"Atlantic/Cape_Verde"</item>
- <item>"Africa/Casablanca"</item>
- <item>"Europe/London"</item>
- <item>"Europe/Amsterdam"</item>
- <item>"Europe/Belgrade"</item>
- <item>"Europe/Brussels"</item>
- <item>"Europe/Sarajevo"</item>
- <item>"Africa/Windhoek"</item>
- <item>"Africa/Brazzaville"</item>
- <item>"Asia/Amman"</item>
- <item>"Europe/Athens"</item>
- <item>"Asia/Beirut"</item>
- <item>"Africa/Cairo"</item>
- <item>"Europe/Helsinki"</item>
- <item>"Asia/Jerusalem"</item>
- <item>"Europe/Minsk"</item>
- <item>"Africa/Harare"</item>
- <item>"Asia/Baghdad"</item>
- <item>"Europe/Moscow"</item>
- <item>"Asia/Kuwait"</item>
- <item>"Africa/Nairobi"</item>
- <item>"Asia/Tehran"</item>
- <item>"Asia/Baku"</item>
- <item>"Asia/Tbilisi"</item>
- <item>"Asia/Yerevan"</item>
- <item>"Asia/Dubai"</item>
- <item>"Asia/Kabul"</item>
- <item>"Asia/Karachi"</item>
- <item>"Asia/Oral"</item>
- <item>"Asia/Yekaterinburg"</item>
- <item>"Asia/Calcutta"</item>
- <item>"Asia/Colombo"</item>
- <item>"Asia/Katmandu"</item>
- <item>"Asia/Almaty"</item>
- <item>"Asia/Rangoon"</item>
- <item>"Asia/Krasnoyarsk"</item>
- <item>"Asia/Bangkok"</item>
- <item>"Asia/Shanghai"</item>
- <item>"Asia/Hong_Kong"</item>
- <item>"Asia/Irkutsk"</item>
- <item>"Asia/Kuala_Lumpur"</item>
- <item>"Australia/Perth"</item>
- <item>"Asia/Taipei"</item>
- <item>"Asia/Seoul"</item>
- <item>"Asia/Tokyo"</item>
- <item>"Asia/Yakutsk"</item>
- <item>"Australia/Adelaide"</item>
- <item>"Australia/Darwin"</item>
- <item>"Australia/Brisbane"</item>
- <item>"Australia/Hobart"</item>
- <item>"Australia/Sydney"</item>
- <item>"Asia/Vladivostok"</item>
- <item>"Pacific/Guam"</item>
- <item>"Asia/Magadan"</item>
- <item>"Pacific/Auckland"</item>
- <item>"Pacific/Fiji"</item>
- <item>"Pacific/Tongatapu"</item>
- <item>"Asia/Jakarta"</item>
+ <string-array name="timezone_values" translatable="false" tools:ignore="Typos">
+ <item>Pacific/Majuro</item>
+ <item>Pacific/Midway</item>
+ <item>Pacific/Honolulu</item>
+ <item>America/Anchorage</item>
+ <item>America/Los_Angeles</item>
+ <item>America/Tijuana</item>
+ <item>America/Phoenix</item>
+ <item>America/Chihuahua</item>
+ <item>America/Denver</item>
+ <item>America/Costa_Rica</item>
+ <item>America/Chicago</item>
+ <item>America/Mexico_City</item>
+ <item>America/Regina</item>
+ <item>America/Bogota</item>
+ <item>America/New_York</item>
+ <item>America/Caracas</item>
+ <item>America/Barbados</item>
+ <item>America/Halifax</item>
+ <item>America/Manaus</item>
+ <item>America/Santiago</item>
+ <item>America/St_Johns</item>
+ <item>America/Sao_Paulo</item>
+ <item>America/Argentina/Buenos_Aires</item>
+ <item>America/Godthab</item>
+ <item>America/Montevideo</item>
+ <item>Atlantic/South_Georgia</item>
+ <item>Atlantic/Azores</item>
+ <item>Atlantic/Cape_Verde</item>
+ <item>Africa/Casablanca</item>
+ <item>Europe/London</item>
+ <item>Europe/Amsterdam</item>
+ <item>Europe/Belgrade</item>
+ <item>Europe/Brussels</item>
+ <item>Europe/Sarajevo</item>
+ <item>Africa/Windhoek</item>
+ <item>Africa/Brazzaville</item>
+ <item>Asia/Amman</item>
+ <item>Europe/Athens</item>
+ <item>Asia/Beirut</item>
+ <item>Africa/Cairo</item>
+ <item>Europe/Helsinki</item>
+ <item>Asia/Jerusalem</item>
+ <item>Europe/Minsk</item>
+ <item>Africa/Harare</item>
+ <item>Asia/Baghdad</item>
+ <item>Europe/Moscow</item>
+ <item>Asia/Kuwait</item>
+ <item>Africa/Nairobi</item>
+ <item>Asia/Tehran</item>
+ <item>Asia/Baku</item>
+ <item>Asia/Tbilisi</item>
+ <item>Asia/Yerevan</item>
+ <item>Asia/Dubai</item>
+ <item>Asia/Kabul</item>
+ <item>Asia/Karachi</item>
+ <item>Asia/Oral</item>
+ <item>Asia/Yekaterinburg</item>
+ <item>Asia/Calcutta</item>
+ <item>Asia/Colombo</item>
+ <item>Asia/Katmandu</item>
+ <item>Asia/Almaty</item>
+ <item>Asia/Rangoon</item>
+ <item>Asia/Krasnoyarsk</item>
+ <item>Asia/Bangkok</item>
+ <item>Asia/Shanghai</item>
+ <item>Asia/Hong_Kong</item>
+ <item>Asia/Irkutsk</item>
+ <item>Asia/Kuala_Lumpur</item>
+ <item>Australia/Perth</item>
+ <item>Asia/Taipei</item>
+ <item>Asia/Seoul</item>
+ <item>Asia/Tokyo</item>
+ <item>Asia/Yakutsk</item>
+ <item>Australia/Adelaide</item>
+ <item>Australia/Darwin</item>
+ <item>Australia/Brisbane</item>
+ <item>Australia/Hobart</item>
+ <item>Australia/Sydney</item>
+ <item>Asia/Vladivostok</item>
+ <item>Pacific/Guam</item>
+ <item>Asia/Magadan</item>
+ <item>Pacific/Auckland</item>
+ <item>Pacific/Fiji</item>
+ <item>Pacific/Tongatapu</item>
+ <item>Asia/Jakarta</item>
</string-array>
<!-- Short label for a shortcut to create a new alarm. The maximum length is ~10 characters
@@ -762,8 +890,6 @@
<!-- Header in the preferences settings for the section pertaining to timers. -->
<string name="timer_settings">Timers</string>
- <!-- Description for timer ringtone setting. -->
- <string name="timer_ringtone_title">Timer ringtone</string>
<!-- Description for timer vibration. -->
<string name="timer_vibrate_title">Timer vibrate</string>
@@ -795,8 +921,27 @@
<string name="alarm_undo">undo</string>
<!-- Toast content when an alarm was deleted -->
<string name="alarm_deleted">Alarm deleted</string>
- <!-- slash between date and next alarm in the clock -->
+
+ <!-- slash between date and next alarm in the clock: used for the DigitalAppWidget -->
<string name="world_day_of_week_label"> / <xliff:g id="label">%s</xliff:g></string>
+
+ <!-- Number of hours or minutes the world city is ahead of the current city [CHAR LIMIT=NONE] -->
+ <string name="world_time_ahead"><xliff:g id="time" example="6 hours">%1$s</xliff:g> ahead</string>
+ <!-- Number of hours or minutes the world city is behind the current city [CHAR LIMIT=NONE] -->
+ <string name="world_time_behind"><xliff:g id="time" example="4 hours">%1$s</xliff:g> behind</string>
+ <!-- Number of hours and minutes the world city is ahead of the current city [CHAR LIMIT=NONE] -->
+ <string name="world_hours_minutes_ahead"><xliff:g id="hours" example="6 hr">%1$s</xliff:g> <xliff:g id="minutes" example="30 min">%2$s</xliff:g> ahead</string>
+ <!-- Number of hours and minutes the world city is behind the current city [CHAR LIMIT=NONE] -->
+ <string name="world_hours_minutes_behind"><xliff:g id="hours" example="4 hr">%1$s</xliff:g> <xliff:g id="minutes" example="30 min">%2$s</xliff:g> behind</string>
+ <!-- The time and day the world city is ahead of the current city [CHAR LIMIT=NONE] -->
+ <string name="world_hours_tomorrow">Tomorrow, <xliff:g id="time" example="6 hours ahead">%1$s</xliff:g></string>
+ <!-- Number of hours the world city is behind the current city [CHAR LIMIT=NONE] -->
+ <string name="world_hours_yesterday">Yesterday, <xliff:g id="time" example="4 hours behind">%1$s</xliff:g></string>
+ <!-- The time in a world city is the day after the time in the current city (used in landscape) [CHAR LIMIT=NONE] -->
+ <string name="world_tomorrow">Tomorrow</string>
+ <!-- The time in a world city is the day before the time in the current city (used in landscape) [CHAR LIMIT=NONE] -->
+ <string name="world_yesterday">Yesterday</string>
+
<!-- Description of field showing the next alarm time in the clock page, for accessibility. -->
<string name="next_alarm_description">Next alarm: <xliff:g id="alarm_time" example="Wed 8:00am">%s</xliff:g></string>
@@ -811,13 +956,6 @@
-->
<string name="invalid_time">Invalid time <xliff:g id="invalid_hour" example="25">%1$d</xliff:g>:<xliff:g id="invalid_minutes" example="63">%2$d</xliff:g> <xliff:g id="invalid_ampm" example="PM">%3$s</xliff:g></string>
- <!-- String that indicates no alarm can be located with the given deeplink. This happens when a
- user sends a voice command to mutate an existing alarm but the deeplink no longer refers to a
- valid alarm.
- [CHAR LIMIT=NONE]
- -->
- <string name="cannot_locate_alarm">Unable to locate alarm.</string>
-
<!-- String that represents that no alarm has been specified for a requested hour:minutes.
This happens when a user sends a voice command 'dismiss my alarm at 3:00pm' but they have no
alarms specified for that time. %s represents the time of the alarm.
@@ -832,29 +970,6 @@
-->
<string name="no_scheduled_alarms">No scheduled alarms</string>
- <!-- String that indicates the alarm is still scheduled and cannot yet be snoozed.
- [CHAR LIMIT=NONE]
- -->
- <string name="cannot_snooze_scheduled_alarm">The alarm has not yet fired.</string>
-
- <!-- String that indicates the alarm is missed and cannot be snoozed or dismissed. This
- happens when a user sends a voice command 'dismiss alarm' or 'snooze alarm' but the alarm is
- already in the missed state.
- [CHAR LIMIT=NONE]
- -->
- <string name="alarm_is_already_missed">The alarm is already missed.</string>
-
- <!-- String that indicates the alarm is dismissed or predismissed and cannot be snoozed or
- dismissed again.
- [CHAR LIMIT=NONE]
- -->
- <string name="alarm_is_already_dismissed">The alarm is already dismissed.</string>
-
- <!-- String that indicates the alarm is snoozed cannot be snoozed again.
- [CHAR LIMIT=NONE]
- -->
- <string name="alarm_is_already_snoozed">The alarm is already snoozed.</string>
-
<!-- String that represents that the user specified that they want to select an alarm to
dismiss by specifying a 'label' but they didn't specify any labels.
[CHAR LIMIT=NONE]
diff --git a/res/values/styles.xml b/res/values/styles.xml
index f6a86b5..64d05e8 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -1,151 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!--
+ Copyright (C) 2016 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
+ 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
+ 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.
--->
+ 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.
+ -->
<resources xmlns:tools="http://schemas.android.com/tools">
- <style name="BaseActivityTheme" parent="Theme.AppCompat.NoActionBar">
- <item name="android:windowBackground">@color/default_background</item>
- <item name="android:windowContentOverlay">@null</item>
- <item name="android:windowTranslucentStatus">true</item>
- <item name="android:windowTranslucentNavigation">true</item>
-
- <item name="alertDialogTheme">@style/AlertDialogTheme</item>
- <item name="colorAccent">@color/color_accent</item>
- <item name="colorControlActivated">@color/white</item>
- <item name="colorControlNormal">@color/white</item>
- <item name="colorPrimaryDark">@color/status_bar</item>
- <item name="imageButtonStyle">@style/ImageButtonStyle</item>
- </style>
-
- <style name="AlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
- <!-- Attributes from support.v7.appcompat -->
- <item name="colorAccent">@color/color_accent</item>
- </style>
-
- <style name="DialogTheme" parent="Theme.AppCompat.Dialog">
- <!-- Attributes from support.v7.appcompat -->
- <item name="colorAccent">@color/color_accent</item>
- </style>
-
- <style name="ControlAccentThemeOverlay">
- <!-- Attributes from support.v7.appcompat -->
- <item name="colorControlActivated">?attr/colorAccent</item>
- </style>
-
- <style name="DeskClockTabBaseStyle" parent="Widget.AppCompat.ActionBar.TabView">
- <item name="android:paddingLeft">@dimen/actionbar_tab_padding</item>
- <item name="android:paddingRight">@dimen/actionbar_tab_padding</item>
- <item name="android:gravity">center</item>
- </style>
-
- <style name="SettingsTheme" parent="BaseActivityTheme">
- <item name="android:alertDialogTheme">@style/SettingsAlertDialogTheme</item>
- <item name="android:detailsElementBackground">@null</item>
-
- <!-- Attributes from support.v7.appcompat -->
- <item name="actionBarStyle">@style/SettingsActionBarStyle</item>
- <item name="colorControlActivated">?attr/colorAccent</item>
- <item name="preferenceTheme">@style/SettingsPreferenceTheme</item>
- <item name="windowActionBar">true</item>
- <item name="windowNoTitle">false</item>
- </style>
-
- <style name="SettingsActionBarStyle" parent="Widget.AppCompat.Toolbar">
- <item name="android:background">@null</item>
-
- <!-- Attributes from support.v7.appcompat -->
- <item name="displayOptions">showTitle|homeAsUp</item>
- </style>
-
- <style name="SettingsAlertDialogTheme" parent="android:Theme.Holo.Dialog.MinWidth">
- <item name="android:windowBackground">@android:color/transparent</item>
- </style>
-
- <style name="SettingsPreferenceTheme" parent="PreferenceThemeOverlay.v14.Material">
- <!-- Attributes from support.v7.preference -->
- <item name="preferenceFragmentStyle">@style/SettingsPreferenceFragmentStyle</item>
- </style>
-
- <style name="SettingsPreferenceFragmentStyle" parent="PreferenceFragment.Material">
- <item name="android:divider">@color/hairline</item>
- <item name="android:dividerHeight">@dimen/hairline_height</item>
- </style>
-
- <style name="CitiesTheme" parent="BaseActivityTheme">
- <item name="android:fastScrollPreviewBackgroundLeft">@drawable/fastscroll_preview</item>
- <item name="android:fastScrollPreviewBackgroundRight">@drawable/fastscroll_preview</item>
- <item name="android:fastScrollTextColor">@color/white</item>
- <item name="android:fastScrollThumbDrawable">@drawable/fastscroll_thumb</item>
- <item name="android:fastScrollTrackDrawable">@drawable/fastscroll_track</item>
- <!-- Attributes from support.v7.appcompat -->
- <item name="windowActionBar">true</item>
- <item name="windowNoTitle">false</item>
- <item name="actionBarStyle">@style/CitiesActionBarStyle</item>
- </style>
-
- <style name="CitiesActionBarStyle" parent="Widget.AppCompat.ActionBar">
- <item name="android:background">@null</item>
-
- <!-- Attributes from support.v7.appcompat -->
- <item name="displayOptions">homeAsUp</item>
- </style>
-
- <style name="AlarmAlertFullScreenTheme" parent="BaseActivityTheme" />
-
- <style name="DeskClockTheme" parent="BaseActivityTheme" />
-
- <style name="ExpiredTimersActivityTheme" parent="BaseActivityTheme" />
-
- <style name="ScreensaverActivityTheme" parent="Theme.AppCompat.NoActionBar" />
-
- <style name="ImageButtonStyle" parent="Widget.AppCompat.ImageButton">
- <item name="android:background">?attr/selectableItemBackgroundBorderless</item>
- </style>
-
- <style name="alarm_list_left_column">
- <item name="android:layout_width">68dip</item>
- <item name="android:layout_height">68dip</item>
- </style>
-
<style name="no_alarms">
<item name="android:textSize">@dimen/no_alarm_font_size</item>
<item name="android:textColor">@color/white_63p</item>
</style>
- <style name="timer_setup_digit">
- <item name="android:ellipsize">none</item>
- <item name="android:includeFontPadding">false</item>
- <item name="android:fontFamily">sans-serif-thin</item>
- <item name="android:singleLine">true</item>
- <item name="android:textColor">@color/clock_white</item>
- <item name="android:textSize">@dimen/timer_setup_font_size</item>
- </style>
-
- <style name="timer_setup_label" parent="label">
- <item name="android:ellipsize">none</item>
- <item name="android:fontFamily">sans-serif-thin</item>
- <item name="android:singleLine">true</item>
- <item name="android:textColor">@color/clock_white</item>
- </style>
-
- <style name="medium_light">
- <item name="android:textSize">@dimen/medium_font_size</item>
- <item name="android:fontFamily">sans-serif-thin</item>
- </style>
-
<style name="widget_medium_thin">
<item name="android:fontFamily">sans-serif-thin</item>
<item name="android:shadowRadius">@dimen/widget_shadow_radius</item>
@@ -157,23 +33,28 @@
<item name="android:textSize">@dimen/label_font_size</item>
</style>
- <style name="label_not_caps" parent="label">
- <item name="android:textAllCaps">false</item>
- </style>
-
<style name="widget_label" parent="label">
<item name="android:shadowRadius">@dimen/widget_shadow_radius</item>
<item name="android:shadowColor">@color/widget_shadow_color</item>
<item name="android:shadowDy">@dimen/widget_shadow_dy</item>
</style>
- <style name="header">
- <item name="android:textSize">@dimen/header_font_size</item>
- <item name="android:textStyle">bold</item>
+ <style name="display_time">
+ <item name="android:textSize">56sp</item>
+ <item name="android:textColor">@color/white</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:fontFeatureSettings" tools:targetApi="21">tnum</item>
</style>
- <style name="header_not_caps" parent ="header">
- <item name="android:textAllCaps">false</item>
+ <style name="world_clock_time">
+ <item name="android:textSize">48sp</item>
+ <item name="android:textColor">@color/white</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="body">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">@color/white</item>
</style>
<style name="big_thin">
@@ -196,19 +77,10 @@
<item name="android:textSize">@dimen/alarm_label_size</item>
</style>
- <style name="alarm_label_not_caps" parent="alarm_label">
- <item name="android:textAllCaps">false</item>
- </style>
-
<style name="alarm_label_bold" parent="alarm_label">
<item name="android:textStyle">bold</item>
</style>
- <style name="dialpad" parent="Widget.AppCompat.Button.Borderless">
- <item name="android:textSize">@dimen/dialpad_font_size</item>
- <item name="android:fontFamily">sans-serif-thin</item>
- </style>
-
<style name="PrimaryLabelTextParentAppearance">
<item name="android:textColor">@color/white</item>
<item name="android:textSize">@dimen/label_text_size</item>
@@ -223,25 +95,42 @@
<style name="SecondaryLabelTextAppearance" parent="SecondaryLabelTextParentAppearance" />
- <style name="TextAppearance">
- <item name="android:textSize">16sp</item>
- <item name="android:textStyle">normal</item>
+ <style name="TextAppearance.Tab" parent="TextAppearance.Design.Tab">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ <item name="android:textSize">12sp</item>
</style>
- <!-- Status Bar Styles -->
- <style name="TextAppearance.StatusBar">
- <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ <style name="Widget.ActionBar" parent="Widget.AppCompat.ActionBar">
+ <item name="android:background">@null</item>
+
+ <!-- Attributes from android.support.v7.appcompat -->
+ <item name="displayOptions">showTitle|homeAsUp</item>
</style>
- <!-- Notification content styles -->
- <style name="TextAppearance.StatusBar.EventContent" tools:ignore="PrivateResource">
- <item name="android:textSize">@dimen/notification_text_size</item>
- <item name="android:textColor">@color/notif_text_grey</item>
+ <style name="Widget.ActionBar.NoTitle">
+ <!-- Attributes from android.support.v7.appcompat -->
+ <item name="displayOptions">homeAsUp</item>
</style>
- <style name="TextAppearance.StatusBar.EventContent.Title" tools:ignore="PrivateResource">
- <item name="android:textSize">@dimen/notification_title_text_size</item>
- <item name="android:textColor">@color/white</item>
+ <style name="Widget.Button.TimerSetupDigit" parent="Widget.AppCompat.Button.Borderless">
+ <item name="android:background">?attr/selectableItemBackgroundBorderless</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:gravity">center</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textSize">36sp</item>
+ </style>
+
+ <style name="Widget.ImageButton" parent="Widget.AppCompat.ImageButton">
+ <item name="android:background">?attr/selectableItemBackgroundBorderless</item>
+ </style>
+
+ <style name="Widget.TimePicker" parent="android:Widget.Material.TimePicker"
+ tools:targetApi="21">
+ <item name="android:headerBackground">@android:color/transparent</item>
+ <item name="android:numbersBackgroundColor">@android:color/transparent</item>
+ <item name="android:numbersSelectorColor">?attr/colorAccent</item>
+ <item name="android:numbersTextColor">?android:attr/textColorPrimary</item>
</style>
</resources>
diff --git a/res/values/themes.xml b/res/values/themes.xml
new file mode 100644
index 0000000..5847add
--- /dev/null
+++ b/res/values/themes.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 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.
+ -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <style name="Theme.DeskClock" parent="Theme.AppCompat.NoActionBar">
+ <item name="android:colorBackgroundFloating" tools:targetApi="23">
+ ?attr/colorBackgroundFloating
+ </item>
+ <item name="android:navigationBarColor" tools:targetApi="21">?attr/colorPrimaryDark</item>
+ <item name="android:popupTheme" tools:targetApi="21">?attr/popupTheme</item>
+ <item name="android:timePickerStyle" tools:targetApi="21">@style/Widget.TimePicker</item>
+ <item name="android:windowBackground">@color/default_background</item>
+ <item name="android:windowContentOverlay">@null</item>
+
+ <!-- Attributes from android.support.v7.appcompat -->
+ <item name="colorAccent">#DA4336</item>
+ <item name="colorBackgroundFloating">#303030</item>
+ <item name="colorControlActivated">@android:color/white</item>
+ <item name="colorControlNormal">?android:attr/textColorPrimary</item>
+ <item name="colorPrimaryDark">#66000000</item>
+ <item name="imageButtonStyle">@style/Widget.ImageButton</item>
+ <item name="popupTheme">@style/ThemeOverlay.Popup</item>
+ </style>
+
+ <style name="Theme.DeskClock.Settings">
+ <!-- Attributes from android.support.v7.appcompat -->
+ <item name="actionBarStyle">@style/Widget.ActionBar</item>
+ <item name="colorControlActivated">?attr/colorAccent</item>
+ <item name="windowActionBar">true</item>
+ <item name="windowNoTitle">false</item>
+
+ <!-- Attributes from android.support.v7.preference -->
+ <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
+ </style>
+
+ <style name="Theme.DeskClock.RingtonePicker">
+ <!-- Attributes from android.support.v7.appcompat -->
+ <item name="actionBarStyle">@style/Widget.ActionBar</item>
+ <item name="windowActionBar">true</item>
+ <item name="windowNoTitle">false</item>
+ </style>
+
+ <style name="Theme.DeskClock.CitySelection">
+ <!-- Attributes from android.support.v7.appcompat -->
+ <item name="actionBarStyle">@style/Widget.ActionBar.NoTitle</item>
+ <item name="windowActionBar">true</item>
+ <item name="windowNoTitle">false</item>
+ </style>
+
+ <style name="ThemeOverlay.Control.Accent" parent="ThemeOverlay.AppCompat">
+ <!-- Attributes from android.support.v7.appcompat -->
+ <item name="colorControlActivated">?attr/colorAccent</item>
+ </style>
+
+ <style name="ThemeOverlay.Popup" parent="ThemeOverlay.AppCompat">
+ <item name="android:colorBackground">?attr/colorBackgroundFloating</item>
+ </style>
+
+</resources>
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
index d390499..73c3359 100644
--- a/res/xml/settings.xml
+++ b/res/xml/settings.xml
@@ -17,25 +17,29 @@
android:title="@string/settings">
<PreferenceCategory
android:title="@string/clock_settings">
- <ListPreference
- android:key="clock_style"
- android:title="@string/clock_style"
+ <com.android.deskclock.settings.SimpleMenuPreference
+ android:defaultValue="@string/default_clock_style"
+ android:dialogTitle="@string/clock_style"
android:entries="@array/clock_style_entries"
android:entryValues="@array/clock_style_values"
- android:defaultValue="@string/default_clock_style"
- android:dialogTitle="@string/clock_style" />
+ android:key="clock_style"
+ android:title="@string/clock_style" />
<SwitchPreferenceCompat
+ android:key="display_clock_seconds"
+ android:title="@string/display_clock_seconds_pref" />
+
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
android:key="automatic_home_clock"
- android:title="@string/automatic_home_clock"
android:summary="@string/automatic_home_clock_summary"
- android:defaultValue="true" />
+ android:title="@string/automatic_home_clock" />
<ListPreference
- android:key="home_time_zone"
+ android:dialogTitle="@string/home_time_zone_title"
android:entries="@array/timezone_labels"
android:entryValues="@array/timezone_values"
- android:dialogTitle="@string/home_time_zone_title"
+ android:key="home_time_zone"
android:title="@string/home_time_zone" />
<Preference
@@ -46,59 +50,69 @@
<PreferenceCategory
android:title="@string/alarm_settings">
<ListPreference
- android:key="auto_silence"
- android:title="@string/auto_silence_title"
+ android:defaultValue="10"
+ android:dialogTitle="@string/auto_silence_title"
android:entries="@array/auto_silence_entries"
android:entryValues="@array/auto_silence_values"
- android:defaultValue="10"
- android:dialogTitle="@string/auto_silence_title" />
+ android:key="auto_silence"
+ android:title="@string/auto_silence_title" />
- <com.android.deskclock.settings.SnoozeLengthDialogPreference
+ <ListPreference
+ android:defaultValue="10"
+ android:dialogTitle="@string/snooze_duration_title"
+ android:entries="@array/snooze_duration_entries"
+ android:entryValues="@array/snooze_duration_values"
android:key="snooze_duration"
- android:title="@string/snooze_duration_title"
- android:dialogLayout="@layout/snooze_length_picker" />
+ android:title="@string/snooze_duration_title" />
<com.android.deskclock.settings.AlarmVolumePreference
android:key="volume_setting"
- android:title="@string/alarm_volume_title"
- android:layout="@layout/alarm_volume_preference" />
-
- <com.android.deskclock.settings.CrescendoLengthDialogPreference
- android:key="alarm_crescendo_duration"
- android:title="@string/crescendo_duration_title"
- android:dialogLayout="@layout/crescendo_length_picker" />
+ android:layout="@layout/alarm_volume_preference"
+ android:title="@string/alarm_volume_title" />
<ListPreference
- android:key="volume_button_setting"
- android:title="@string/volume_button_setting_title"
+ android:defaultValue="0"
+ android:dialogTitle="@string/crescendo_duration_title"
+ android:entries="@array/crescendo_entries"
+ android:entryValues="@array/crescendo_values"
+ android:key="alarm_crescendo_duration"
+ android:title="@string/crescendo_duration_title" />
+
+ <com.android.deskclock.settings.SimpleMenuPreference
+ android:defaultValue="0"
android:dialogTitle="@string/volume_button_setting_title"
android:entries="@array/volume_button_setting_entries"
android:entryValues="@array/volume_button_setting_values"
- android:defaultValue="0" />
+ android:key="volume_button_setting"
+ android:title="@string/volume_button_setting_title" />
- <ListPreference
- android:key="week_start"
- android:title="@string/week_start_title"
+ <com.android.deskclock.settings.SimpleMenuPreference
android:dialogTitle="@string/week_start_title"
android:entries="@array/week_start_entries"
- android:entryValues="@array/week_start_values" />
+ android:entryValues="@array/week_start_values"
+ android:key="week_start"
+ android:title="@string/week_start_title" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/timer_settings">
<Preference
android:key="timer_ringtone"
- android:title="@string/timer_ringtone_title" />
+ android:title="@string/timer_sound" />
- <com.android.deskclock.settings.CrescendoLengthDialogPreference
+ <ListPreference
+ android:defaultValue="0"
+ android:dialogTitle="@string/crescendo_duration_title"
+ android:entries="@array/crescendo_entries"
+ android:entryValues="@array/crescendo_values"
android:key="timer_crescendo_duration"
- android:title="@string/crescendo_duration_title"
- android:dialogLayout="@layout/crescendo_length_picker" />
+ android:title="@string/crescendo_duration_title" />
+
<SwitchPreferenceCompat
+ android:defaultValue="false"
android:key="timer_vibrate"
- android:title="@string/timer_vibrate_title"
- android:defaultValue="false" />
+ android:title="@string/timer_vibrate_title" />
</PreferenceCategory>
-</PreferenceScreen>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java
index de102e5..849e8cf 100644
--- a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java
+++ b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java
@@ -178,8 +178,8 @@
}
private void update(RemoteViews rv, City city, int clockId, int labelId, int dayId) {
- rv.setCharSequence(clockId, "setFormat12Hour", Utils.get12ModeFormat(0.4f));
- rv.setCharSequence(clockId, "setFormat24Hour", Utils.get24ModeFormat());
+ rv.setCharSequence(clockId, "setFormat12Hour", Utils.get12ModeFormat(0.4f, false));
+ rv.setCharSequence(clockId, "setFormat24Hour", Utils.get24ModeFormat(false));
final boolean is24HourFormat = DateFormat.is24HourFormat(mContext);
final float fontSize = is24HourFormat ? m24HourFontSize : m12HourFontSize;
diff --git a/src/com/android/alarmclock/DigitalAppWidgetProvider.java b/src/com/android/alarmclock/DigitalAppWidgetProvider.java
index 1ae7a00..7f322ad 100644
--- a/src/com/android/alarmclock/DigitalAppWidgetProvider.java
+++ b/src/com/android/alarmclock/DigitalAppWidgetProvider.java
@@ -156,7 +156,9 @@
final DataModel dm = DataModel.getDataModel();
dm.updateWidgetCount(getClass(), widgetIds.length, R.string.category_digital_widget);
- updateDayChangeCallback(context);
+ if (widgetIds.length > 0) {
+ updateDayChangeCallback(context);
+ }
}
/**
diff --git a/src/com/android/deskclock/AlarmClockFragment.java b/src/com/android/deskclock/AlarmClockFragment.java
index 7c8f93f..d5c0223 100644
--- a/src/com/android/deskclock/AlarmClockFragment.java
+++ b/src/com/android/deskclock/AlarmClockFragment.java
@@ -22,29 +22,26 @@
import android.content.Loader;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
-import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageButton;
+import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.deskclock.alarms.AlarmTimeClickHandler;
import com.android.deskclock.alarms.AlarmUpdateHandler;
import com.android.deskclock.alarms.ScrollHandler;
-import com.android.deskclock.alarms.TimePickerCompat;
+import com.android.deskclock.alarms.TimePickerDialogFragment;
import com.android.deskclock.alarms.dataadapter.AlarmItemHolder;
import com.android.deskclock.alarms.dataadapter.CollapsedAlarmViewHolder;
import com.android.deskclock.alarms.dataadapter.ExpandedAlarmViewHolder;
-import com.android.deskclock.data.DataModel;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
import com.android.deskclock.uidata.UiDataModel;
@@ -62,9 +59,8 @@
*/
public final class AlarmClockFragment extends DeskClockFragment implements
LoaderManager.LoaderCallbacks<Cursor>,
- RingtonePickerDialogFragment.OnRingtoneSelectedListener,
ScrollHandler,
- TimePickerCompat.OnTimeSetListener {
+ TimePickerDialogFragment.OnTimeSetListener {
// This extra is used when receiving an intent to create an alarm, but no alarm details
// have been passed in, so the alarm page should start the process of creating a new alarm.
@@ -104,11 +100,6 @@
}
@Override
- public void processTimeSet(int hourOfDay, int minute) {
- mAlarmTimeClickHandler.processTimeSet(hourOfDay, minute);
- }
-
- @Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
mCursorLoader = getLoaderManager().initLoader(0, null, this);
@@ -124,7 +115,16 @@
final Context context = getActivity();
mRecyclerView = (RecyclerView) v.findViewById(R.id.alarms_recycler_view);
- mLayoutManager = new LinearLayoutManager(context);
+ mLayoutManager = new LinearLayoutManager(context) {
+ @Override
+ protected int getExtraLayoutSpace(RecyclerView.State state) {
+ final int extraSpace = super.getExtraLayoutSpace(state);
+ if (state.willRunPredictiveAnimations()) {
+ return Math.max(getHeight(), extraSpace);
+ }
+ return extraSpace;
+ }
+ };
mRecyclerView.setLayoutManager(mLayoutManager);
mMainLayout = (ViewGroup) v.findViewById(R.id.main);
mAlarmUpdateHandler = new AlarmUpdateHandler(context, this, mMainLayout);
@@ -139,7 +139,7 @@
mItemAdapter.setHasStableIds();
mItemAdapter.withViewTypes(new CollapsedAlarmViewHolder.Factory(inflater),
null, CollapsedAlarmViewHolder.VIEW_TYPE);
- mItemAdapter.withViewTypes(new ExpandedAlarmViewHolder.Factory(context, inflater),
+ mItemAdapter.withViewTypes(new ExpandedAlarmViewHolder.Factory(context),
null, ExpandedAlarmViewHolder.VIEW_TYPE);
mItemAdapter.setOnItemChangedListener(new ItemAdapter.OnItemChangedListener() {
@Override
@@ -164,12 +164,20 @@
mExpandedAlarmId = Alarm.INVALID_ID;
}
}
+
+ @Override
+ public void onItemChanged(ItemAdapter.ItemHolder<?> holder, Object payload) {
+ /* No additional work to do */
+ }
});
final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
mRecyclerView.addOnLayoutChangeListener(scrollPositionWatcher);
mRecyclerView.addOnScrollListener(scrollPositionWatcher);
mRecyclerView.setAdapter(mItemAdapter);
-
+ final ItemAnimator itemAnimator = new ItemAnimator();
+ itemAnimator.setChangeDuration(300L);
+ itemAnimator.setMoveDuration(300L);
+ mRecyclerView.setItemAnimator(itemAnimator);
return v;
}
@@ -178,7 +186,7 @@
super.onStart();
if (!isTabSelected()) {
- TimePickerCompat.removeTimeEditDialog(getFragmentManager());
+ TimePickerDialogFragment.removeTimeEditDialog(getFragmentManager());
}
}
@@ -186,8 +194,16 @@
public void onResume() {
super.onResume();
+ // Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
+ // alarms when midnight passes.
+ UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
+
// Check if another app asked us to create a blank new alarm.
final Intent intent = getActivity().getIntent();
+ if (intent == null) {
+ return;
+ }
+
if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
@@ -213,10 +229,6 @@
// Remove the SCROLL_TO_ALARM extra now that we've processed it.
intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA);
}
-
- // Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
- // alarms when midnight passes.
- UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
}
@Override
@@ -255,21 +267,6 @@
}
@Override
- public void onRingtoneSelected(String tag, Uri ringtoneUri) {
- // Update the default ringtone for future new alarms.
- DataModel.getDataModel().setDefaultAlarmRingtoneUri(ringtoneUri);
-
- final Alarm alarm = mAlarmTimeClickHandler.getSelectedAlarm();
- if (alarm == null) {
- LogUtils.e("Could not get selected alarm to set ringtone");
- return;
- }
- alarm.alert = ringtoneUri;
- // Save the change to alarm.
- mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false /* popToast */, true /* minorUpdate */);
- }
-
- @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return Alarm.getAlarmsCursorLoader(getActivity());
}
@@ -324,7 +321,12 @@
mItemAdapter.setItems(items);
// Show or hide the empty view as appropriate.
- mEmptyViewController.setEmpty(items.isEmpty());
+ final boolean noAlarms = items.isEmpty();
+ mEmptyViewController.setEmpty(noAlarms);
+ if (noAlarms) {
+ // Ensure the drop shadow is hidden when no alarms exist.
+ setTabScrolledToTop(true);
+ }
// Expand the correct alarm.
if (mExpandedAlarmId != Alarm.INVALID_ID) {
@@ -394,7 +396,7 @@
}
@Override
- public void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
+ public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
left.setVisibility(View.INVISIBLE);
right.setVisibility(View.INVISIBLE);
}
@@ -402,8 +404,16 @@
private void startCreatingAlarm() {
// Clear the currently selected alarm.
mAlarmTimeClickHandler.setSelectedAlarm(null);
- TimePickerCompat.showTimeEditDialog(this, null /* alarm */,
- DateFormat.is24HourFormat(getActivity()));
+ TimePickerDialogFragment.show(this);
+ }
+
+ @Override
+ public void onTimeSet(TimePickerDialogFragment fragment, int hourOfDay, int minute) {
+ mAlarmTimeClickHandler.onTimeSet(hourOfDay, minute);
+ }
+
+ public void removeItem(AlarmItemHolder itemHolder) {
+ mItemAdapter.removeItem(itemHolder);
}
/**
diff --git a/src/com/android/deskclock/AlarmInitReceiver.java b/src/com/android/deskclock/AlarmInitReceiver.java
index 3848ad1..8bd7cde 100644
--- a/src/com/android/deskclock/AlarmInitReceiver.java
+++ b/src/com/android/deskclock/AlarmInitReceiver.java
@@ -58,7 +58,7 @@
wl.acquire();
// We need to increment the global id out of the async task to prevent race conditions
- AlarmStateManager.updateGlobalIntentId(context);
+ DataModel.getDataModel().updateGlobalIntentId();
// Updates stopwatch and timer data after a device reboot so they are as accurate as
// possible.
diff --git a/src/com/android/deskclock/AlarmRecyclerView.java b/src/com/android/deskclock/AlarmRecyclerView.java
new file mode 100644
index 0000000..f9c63bc
--- /dev/null
+++ b/src/com/android/deskclock/AlarmRecyclerView.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+/**
+ * Thin wrapper around RecyclerView to prevent simultaneous layout passes, particularly during
+ * animations.
+ */
+public class AlarmRecyclerView extends RecyclerView {
+
+ private boolean mIgnoreRequestLayout;
+
+ public AlarmRecyclerView(Context context) {
+ this(context, null);
+ }
+
+ public AlarmRecyclerView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AlarmRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ // Disable scrolling/user action to prevent choppy animations.
+ return rv.getItemAnimator().isRunning();
+ }
+ });
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mIgnoreRequestLayout = true;
+ super.onLayout(changed, left, top, right, bottom);
+ mIgnoreRequestLayout = false;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mIgnoreRequestLayout &&
+ (getItemAnimator() == null || !getItemAnimator().isRunning())) {
+ super.requestLayout();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AnalogClock.java b/src/com/android/deskclock/AnalogClock.java
index bb8bd65..2024fc7 100644
--- a/src/com/android/deskclock/AnalogClock.java
+++ b/src/com/android/deskclock/AnalogClock.java
@@ -84,18 +84,26 @@
mTime = Calendar.getInstance();
mDescFormat = ((SimpleDateFormat) DateFormat.getTimeFormat(context)).toLocalizedPattern();
+ // Must call mutate on these instances, otherwise the drawables will blur, because they're
+ // sharing their size characteristics with the (smaller) world cities analog clocks.
final ImageView dial = new AppCompatImageView(context);
dial.setImageResource(R.drawable.clock_analog_dial);
+ dial.getDrawable().mutate();
addView(dial);
mHourHand = new AppCompatImageView(context);
mHourHand.setImageResource(R.drawable.clock_analog_hour);
+ mHourHand.getDrawable().mutate();
addView(mHourHand);
+
mMinuteHand = new AppCompatImageView(context);
mMinuteHand.setImageResource(R.drawable.clock_analog_minute);
+ mMinuteHand.getDrawable().mutate();
addView(mMinuteHand);
+
mSecondHand = new AppCompatImageView(context);
mSecondHand.setImageResource(R.drawable.clock_analog_second);
+ mSecondHand.getDrawable().mutate();
addView(mSecondHand);
}
diff --git a/src/com/android/deskclock/AnimatorUtils.java b/src/com/android/deskclock/AnimatorUtils.java
index 0794cf8..770b7c7 100644
--- a/src/com/android/deskclock/AnimatorUtils.java
+++ b/src/com/android/deskclock/AnimatorUtils.java
@@ -16,13 +16,18 @@
package com.android.deskclock;
+import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
+import android.graphics.Rect;
+import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.Property;
import android.view.View;
import android.view.animation.Interpolator;
@@ -33,8 +38,6 @@
public class AnimatorUtils {
- public static final long ANIM_DURATION_SHORT = 266L; // 8/30 frames long
-
public static final Interpolator DECELERATE_ACCELERATE_INTERPOLATOR = new Interpolator() {
@Override
public float getInterpolation(float x) {
@@ -42,19 +45,44 @@
}
};
+ public static final Interpolator INTERPOLATOR_FAST_OUT_SLOW_IN =
+ new FastOutSlowInInterpolator();
+
public static final Property<View, Integer> BACKGROUND_ALPHA =
new Property<View, Integer>(Integer.class, "background.alpha") {
@Override
public Integer get(View view) {
- return view.getBackground().getAlpha();
+ Drawable background = view.getBackground();
+ if (background instanceof LayerDrawable
+ && ((LayerDrawable) background).getNumberOfLayers() > 0) {
+ background = ((LayerDrawable) background).getDrawable(0);
+ }
+ return background.getAlpha();
}
@Override
public void set(View view, Integer value) {
- view.getBackground().setAlpha(value);
+ setBackgroundAlpha(view, value);
}
};
+ /**
+ * Sets the alpha of the top layer's drawable (of the background) only, if the background is a
+ * layer drawable, to ensure that the other layers (i.e., the selectable item background, and
+ * therefore the touch feedback RippleDrawable) are not affected.
+ *
+ * @param view the affected view
+ * @param value the alpha value (0-255)
+ */
+ public static void setBackgroundAlpha(View view, Integer value) {
+ Drawable background = view.getBackground();
+ if (background instanceof LayerDrawable
+ && ((LayerDrawable) background).getNumberOfLayers() > 0) {
+ background = ((LayerDrawable) background).getDrawable(0);
+ }
+ background.setAlpha(value);
+ }
+
public static final Property<ImageView, Integer> DRAWABLE_ALPHA =
new Property<ImageView, Integer>(Integer.class, "drawable.alpha") {
@Override
@@ -149,4 +177,114 @@
public static ValueAnimator getAlphaAnimator(View view, float... values) {
return ObjectAnimator.ofFloat(view, View.ALPHA, values);
}
-}
+
+ public static final Property<View, Integer> VIEW_LEFT =
+ new Property<View, Integer>(Integer.class, "left") {
+ @Override
+ public Integer get(View view) {
+ return view.getLeft();
+ }
+
+ @Override
+ public void set(View view, Integer left) {
+ view.setLeft(left);
+ }
+ };
+
+ public static final Property<View, Integer> VIEW_TOP =
+ new Property<View, Integer>(Integer.class, "top") {
+ @Override
+ public Integer get(View view) {
+ return view.getTop();
+ }
+
+ @Override
+ public void set(View view, Integer top) {
+ view.setTop(top);
+ }
+ };
+
+ public static final Property<View, Integer> VIEW_BOTTOM =
+ new Property<View, Integer>(Integer.class, "bottom") {
+ @Override
+ public Integer get(View view) {
+ return view.getBottom();
+ }
+
+ @Override
+ public void set(View view, Integer bottom) {
+ view.setBottom(bottom);
+ }
+ };
+
+ public static final Property<View, Integer> VIEW_RIGHT =
+ new Property<View, Integer>(Integer.class, "right") {
+ @Override
+ public Integer get(View view) {
+ return view.getRight();
+ }
+
+ @Override
+ public void set(View view, Integer right) {
+ view.setRight(right);
+ }
+ };
+
+ /**
+ * @param target the view to be morphed
+ * @param from the bounds of the {@code target} before animating
+ * @param to the bounds of the {@code target} after animating
+ * @return an animator that morphs the {@code target} between the {@code from} bounds and the
+ * {@code to} bounds. Note that it is the *content* bounds that matter here, so padding
+ * insets contributed by the background are subtracted from the views when computing the
+ * {@code target} bounds.
+ */
+ public static Animator getBoundsAnimator(View target, View from, View to) {
+ // Fetch the content insets for the views. Content bounds are what matter, not total bounds.
+ final Rect targetInsets = new Rect();
+ target.getBackground().getPadding(targetInsets);
+ final Rect fromInsets = new Rect();
+ from.getBackground().getPadding(fromInsets);
+ final Rect toInsets = new Rect();
+ to.getBackground().getPadding(toInsets);
+
+ // Before animating, the content bounds of target must match the content bounds of from.
+ final int startLeft = from.getLeft() - fromInsets.left + targetInsets.left;
+ final int startTop = from.getTop() - fromInsets.top + targetInsets.top;
+ final int startRight = from.getRight() - fromInsets.right + targetInsets.right;
+ final int startBottom = from.getBottom() - fromInsets.bottom + targetInsets.bottom;
+
+ // After animating, the content bounds of target must match the content bounds of to.
+ final int endLeft = to.getLeft() - toInsets.left + targetInsets.left;
+ final int endTop = to.getTop() - toInsets.top + targetInsets.top;
+ final int endRight = to.getRight() - toInsets.right + targetInsets.right;
+ final int endBottom = to.getBottom() - toInsets.bottom + targetInsets.bottom;
+
+ return getBoundsAnimator(target, startLeft, startTop, startRight, startBottom, endLeft,
+ endTop, endRight, endBottom);
+ }
+
+ /**
+ * Returns an animator that animates the bounds of a single view.
+ */
+ public static Animator getBoundsAnimator(View view, int fromLeft, int fromTop, int fromRight,
+ int fromBottom, int toLeft, int toTop, int toRight, int toBottom) {
+ view.setLeft(fromLeft);
+ view.setTop(fromTop);
+ view.setRight(fromRight);
+ view.setBottom(fromBottom);
+
+ return ObjectAnimator.ofPropertyValuesHolder(view,
+ PropertyValuesHolder.ofInt(VIEW_LEFT, toLeft),
+ PropertyValuesHolder.ofInt(VIEW_TOP, toTop),
+ PropertyValuesHolder.ofInt(VIEW_RIGHT, toRight),
+ PropertyValuesHolder.ofInt(VIEW_BOTTOM, toBottom));
+ }
+
+ public static void startDrawableAnimation(ImageView view) {
+ final Drawable d = view.getDrawable();
+ if (d instanceof Animatable) {
+ ((Animatable) d).start();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AsyncRingtonePlayer.java b/src/com/android/deskclock/AsyncRingtonePlayer.java
index dc4ec60..afb46d6 100644
--- a/src/com/android/deskclock/AsyncRingtonePlayer.java
+++ b/src/com/android/deskclock/AsyncRingtonePlayer.java
@@ -13,17 +13,18 @@
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
-import android.os.SystemClock;
import android.telephony.TelephonyManager;
-import android.text.format.DateUtils;
import java.io.IOException;
import java.lang.reflect.Method;
+import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
+import static android.media.AudioManager.STREAM_ALARM;
+
/**
- * <p>Plays the alarm ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
- * used from the main thread. Consequently, problems controlling the ringtone do not cause ANRs in
- * the main thread of the application.</p>
+ * <p>This class controls playback of ringtones. Uses {@link Ringtone} or {@link MediaPlayer} in a
+ * dedicated thread so that this class can be called from the main thread. Consequently, problems
+ * controlling the ringtone do not cause ANRs in the main thread of the application.</p>
*
* <p>This class also serves a second purpose. It accomplishes alarm ringtone playback using two
* different mechanisms depending on the underlying platform.</p>
@@ -41,13 +42,15 @@
* those methods are marked @hide in M and thus invoked using reflection. Consequently, revoking
* the android.permission.READ_EXTERNAL_STORAGE permission has no effect on playback in M+.</li>
* </ul>
+ *
+ * <p>If either the {@link Ringtone} or {@link MediaPlayer} fails to play the requested audio, an
+ * {@link #getFallbackRingtoneUri in-app fallback} is used because playing <strong>some</strong>
+ * sort of noise is always preferable to remaining silent.</p>
*/
public final class AsyncRingtonePlayer {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AsyncRingtonePlayer");
- private static final String DEFAULT_CRESCENDO_LENGTH = "0";
-
// Volume suggested by media team for in-call alarms.
private static final float IN_CALL_VOLUME = 0.125f;
@@ -56,6 +59,7 @@
private static final int EVENT_STOP = 2;
private static final int EVENT_VOLUME = 3;
private static final String RINGTONE_URI_KEY = "RINGTONE_URI_KEY";
+ private static final String CRESCENDO_DURATION_KEY = "CRESCENDO_DURATION_KEY";
/** Handler running on the ringtone thread. */
private Handler mHandler;
@@ -66,28 +70,20 @@
/** The context. */
private final Context mContext;
- /** The key of the preference that controls the crescendo behavior when playing a ringtone. */
- private final String mCrescendoPrefKey;
-
- /**
- * @param crescendoPrefKey the key to the user preference that defines the crescendo behavior
- * associated with this ringtone player, or null to ignore crescendo
- */
- public AsyncRingtonePlayer(Context context, String crescendoPrefKey) {
+ public AsyncRingtonePlayer(Context context) {
mContext = context;
- mCrescendoPrefKey = crescendoPrefKey;
}
/** Plays the ringtone. */
- public void play(Uri ringtoneUri) {
+ public void play(Uri ringtoneUri, long crescendoDuration) {
LOGGER.d("Posting play.");
- postMessage(EVENT_PLAY, ringtoneUri, 0);
+ postMessage(EVENT_PLAY, ringtoneUri, crescendoDuration, 0);
}
/** Stops playing the ringtone. */
public void stop() {
LOGGER.d("Posting stop.");
- postMessage(EVENT_STOP, null, 0);
+ postMessage(EVENT_STOP, null, 0, 0);
}
/** Schedules an adjustment of the playback volume 50ms in the future. */
@@ -98,17 +94,19 @@
mHandler.removeMessages(EVENT_VOLUME);
// Queue the next volume adjustment.
- postMessage(EVENT_VOLUME, null, 50);
+ postMessage(EVENT_VOLUME, null, 0, 50);
}
/**
* Posts a message to the ringtone-thread handler.
*
- * @param messageCode The message to post.
- * @param ringtoneUri The ringtone in question, if any.
- * @param delayMillis The amount of time to delay sending the message, if any.
+ * @param messageCode the message to post
+ * @param ringtoneUri the ringtone in question, if any
+ * @param crescendoDuration the length of time, in ms, over which to crescendo the ringtone
+ * @param delayMillis the amount of time to delay sending the message, if any
*/
- private void postMessage(int messageCode, Uri ringtoneUri, long delayMillis) {
+ private void postMessage(int messageCode, Uri ringtoneUri, long crescendoDuration,
+ long delayMillis) {
synchronized (this) {
if (mHandler == null) {
mHandler = getNewHandler();
@@ -118,6 +116,7 @@
if (ringtoneUri != null) {
final Bundle bundle = new Bundle();
bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri);
+ bundle.putLong(CRESCENDO_DURATION_KEY, crescendoDuration);
message.setData(bundle);
}
@@ -138,8 +137,10 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_PLAY:
- final Uri ringtoneUri = msg.getData().getParcelable(RINGTONE_URI_KEY);
- if (getPlaybackDelegate().play(mContext, ringtoneUri)) {
+ final Bundle data = msg.getData();
+ final Uri ringtoneUri = data.getParcelable(RINGTONE_URI_KEY);
+ final long crescendoDuration = data.getLong(CRESCENDO_DURATION_KEY);
+ if (getPlaybackDelegate().play(mContext, ringtoneUri, crescendoDuration)) {
scheduleVolumeAdjustment();
}
break;
@@ -213,23 +214,6 @@
}
/**
- * Returns true if the crescendo preference was given and the duration is more than
- * 0 seconds.
- */
- private boolean isCrescendoEnabled(Context context) {
- return mCrescendoPrefKey != null && getCrescendoDurationMillis(context) > 0;
- }
-
- /**
- * @return the duration of the crescendo in milliseconds
- */
- private long getCrescendoDurationMillis(Context context) {
- final String crescendoSecondsStr = Utils.getDefaultSharedPreferences(context)
- .getString(mCrescendoPrefKey, DEFAULT_CRESCENDO_LENGTH);
- return Integer.parseInt(crescendoSecondsStr) * DateUtils.SECOND_IN_MILLIS;
- }
-
- /**
* @return the platform-specific playback delegate to use to play the ringtone
*/
private PlaybackDelegate getPlaybackDelegate() {
@@ -258,7 +242,11 @@
/**
* @return {@code true} iff a {@link #adjustVolume volume adjustment} should be scheduled
*/
- boolean play(Context context, Uri ringtoneUri);
+ boolean play(Context context, Uri ringtoneUri, long crescendoDuration);
+
+ /**
+ * Stop any ongoing ringtone playback.
+ */
void stop(Context context);
/**
@@ -288,8 +276,9 @@
* Starts the actual playback of the ringtone. Executes on ringtone-thread.
*/
@Override
- public boolean play(final Context context, Uri ringtoneUri) {
+ public boolean play(final Context context, Uri ringtoneUri, long crescendoDuration) {
checkAsyncRingtonePlayerThread();
+ mCrescendoDuration = crescendoDuration;
LOGGER.i("Play ringtone via android.media.MediaPlayer.");
@@ -297,8 +286,9 @@
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
- Uri alarmNoise = ringtoneUri;
- // Fall back to the default alarm if the database does not have an alarm stored.
+ final boolean inTelephoneCall = isInTelephoneCall(context);
+ Uri alarmNoise = inTelephoneCall ? getInCallRingtoneUri(context) : ringtoneUri;
+ // Fall back to the system default alarm if the database does not have an alarm stored.
if (alarmNoise == null) {
alarmNoise = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
LOGGER.v("Using default alarm: " + alarmNoise.toString());
@@ -314,68 +304,75 @@
}
});
- boolean scheduleVolumeAdjustment = false;
try {
- // Check if we are in a call. If we are, use the in-call alarm resource at a
- // low volume to not disrupt the call.
- if (isInTelephoneCall(context)) {
- LOGGER.v("Using the in-call alarm");
- mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
- alarmNoise = getInCallRingtoneUri(context);
- } else if (isCrescendoEnabled(context)) {
- mMediaPlayer.setVolume(0, 0);
-
- // Compute the time at which the crescendo will stop.
- mCrescendoDuration = getCrescendoDurationMillis(context);
- mCrescendoStopTime = now() + mCrescendoDuration;
- scheduleVolumeAdjustment = true;
- }
-
// If alarmNoise is a custom ringtone on the sd card the app must be granted
// android.permission.READ_EXTERNAL_STORAGE. Pre-M this is ensured at app
// installation time. M+, this permission can be revoked by the user any time.
mMediaPlayer.setDataSource(context, alarmNoise);
- startAlarm(mMediaPlayer);
- scheduleVolumeAdjustment = true;
+ return startPlayback(inTelephoneCall);
} catch (Throwable t) {
- LOGGER.e("Use the fallback ringtone, original was " + alarmNoise, t);
+ LOGGER.e("Using the fallback ringtone, could not play " + alarmNoise, t);
// The alarmNoise may be on the sd card which could be busy right now.
// Use the fallback ringtone.
try {
// Must reset the media player to clear the error state.
mMediaPlayer.reset();
mMediaPlayer.setDataSource(context, getFallbackRingtoneUri(context));
- startAlarm(mMediaPlayer);
+ return startPlayback(inTelephoneCall);
} catch (Throwable t2) {
// At this point we just don't play anything.
LOGGER.e("Failed to play fallback ringtone", t2);
}
}
- return scheduleVolumeAdjustment;
+ return false;
}
/**
- * Do the common stuff when starting the alarm.
+ * Prepare the MediaPlayer for playback if the alarm stream is not muted, then start the
+ * playback.
+ *
+ * @param inTelephoneCall {@code true} if there is currently an active telephone call
+ * @return {@code true} if a crescendo has started and future volume adjustments are
+ * required to advance the crescendo effect
*/
- private void startAlarm(MediaPlayer player) throws IOException {
- // do not play alarms if stream volume is 0 (typically because ringer mode is silent).
- if (mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
- if (Utils.isLOrLater()) {
- player.setAudioAttributes(new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build());
- }
-
- player.setAudioStreamType(AudioManager.STREAM_ALARM);
- player.setLooping(true);
- player.prepare();
- mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
- player.start();
+ private boolean startPlayback(boolean inTelephoneCall)
+ throws IOException {
+ // Do not play alarms if stream volume is 0 (typically because ringer mode is silent).
+ if (mAudioManager.getStreamVolume(STREAM_ALARM) == 0) {
+ return false;
}
+
+ // Indicate the ringtone should be played via the alarm stream.
+ if (Utils.isLOrLater()) {
+ mMediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build());
+ }
+
+ // Check if we are in a call. If we are, use the in-call alarm resource at a low volume
+ // to not disrupt the call.
+ boolean scheduleVolumeAdjustment = false;
+ if (inTelephoneCall) {
+ LOGGER.v("Using the in-call alarm");
+ mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
+ } else if (mCrescendoDuration > 0) {
+ mMediaPlayer.setVolume(0, 0);
+
+ // Compute the time at which the crescendo will stop.
+ mCrescendoStopTime = Utils.now() + mCrescendoDuration;
+ scheduleVolumeAdjustment = true;
+ }
+
+ mMediaPlayer.setAudioStreamType(STREAM_ALARM);
+ mMediaPlayer.setLooping(true);
+ mMediaPlayer.prepare();
+ mAudioManager.requestAudioFocus(null, STREAM_ALARM, AUDIOFOCUS_GAIN_TRANSIENT);
+ mMediaPlayer.start();
+
+ return scheduleVolumeAdjustment;
}
/**
@@ -417,7 +414,7 @@
}
// If the crescendo is complete set the volume to the maximum; we're done.
- final long currentTime = now();
+ final long currentTime = Utils.now();
if (currentTime > mCrescendoStopTime) {
mCrescendoDuration = 0;
mCrescendoStopTime = 0;
@@ -476,8 +473,9 @@
* Starts the actual playback of the ringtone. Executes on ringtone-thread.
*/
@Override
- public boolean play(Context context, Uri ringtoneUri) {
+ public boolean play(Context context, Uri ringtoneUri, long crescendoDuration) {
checkAsyncRingtonePlayerThread();
+ mCrescendoDuration = crescendoDuration;
LOGGER.i("Play ringtone via android.media.Ringtone.");
@@ -490,13 +488,13 @@
ringtoneUri = getInCallRingtoneUri(context);
}
- // attempt to fetch the specified ringtone
+ // Attempt to fetch the specified ringtone.
mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
if (mRingtone == null) {
- // fall back to the default ringtone
- final Uri defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
- mRingtone = RingtoneManager.getRingtone(context, defaultUri);
+ // Fall back to the system default ringtone.
+ ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
+ mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
}
// Attempt to enable looping the ringtone.
@@ -510,13 +508,39 @@
mRingtone = null;
}
- // if we don't have a ringtone at this point there isn't much recourse
+ // If no ringtone exists at this point there isn't much recourse.
if (mRingtone == null) {
- LOGGER.i("Unable to locate alarm ringtone, using internal fallback " +
- "ringtone.");
- mRingtone = RingtoneManager.getRingtone(context, getFallbackRingtoneUri(context));
+ LOGGER.i("Unable to locate alarm ringtone, using internal fallback ringtone.");
+ ringtoneUri = getFallbackRingtoneUri(context);
+ mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
}
+ try {
+ return startPlayback(inTelephoneCall);
+ } catch (Throwable t) {
+ LOGGER.e("Using the fallback ringtone, could not play " + ringtoneUri, t);
+ // Recover from any/all playback errors by attempting to play the fallback tone.
+ mRingtone = RingtoneManager.getRingtone(context, getFallbackRingtoneUri(context));
+ try {
+ return startPlayback(inTelephoneCall);
+ } catch (Throwable t2) {
+ // At this point we just don't play anything.
+ LOGGER.e("Failed to play fallback ringtone", t2);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Prepare the Ringtone for playback, then start the playback.
+ *
+ * @param inTelephoneCall {@code true} if there is currently an active telephone call
+ * @return {@code true} if a crescendo has started and future volume adjustments are
+ * required to advance the crescendo effect
+ */
+ private boolean startPlayback(boolean inTelephoneCall) {
+ // Indicate the ringtone should be played via the alarm stream.
if (Utils.isLOrLater()) {
mRingtone.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
@@ -529,17 +553,16 @@
if (inTelephoneCall) {
LOGGER.v("Using the in-call alarm");
setRingtoneVolume(IN_CALL_VOLUME);
- } else if (isCrescendoEnabled(context)) {
+ } else if (mCrescendoDuration > 0) {
setRingtoneVolume(0);
// Compute the time at which the crescendo will stop.
- mCrescendoDuration = getCrescendoDurationMillis(context);
- mCrescendoStopTime = now() + mCrescendoDuration;
+ mCrescendoStopTime = Utils.now() + mCrescendoDuration;
scheduleVolumeAdjustment = true;
}
- mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ mAudioManager.requestAudioFocus(null, STREAM_ALARM, AUDIOFOCUS_GAIN_TRANSIENT);
+
mRingtone.play();
return scheduleVolumeAdjustment;
@@ -598,7 +621,7 @@
}
// If the crescendo is complete set the volume to the maximum; we're done.
- final long currentTime = now();
+ final long currentTime = Utils.now();
if (currentTime > mCrescendoStopTime) {
mCrescendoDuration = 0;
mCrescendoStopTime = 0;
@@ -613,12 +636,4 @@
return true;
}
}
-
- /**
- * @return the current elapsed time which is immune to device time changes
- */
- private static long now() {
- return SystemClock.elapsedRealtime();
- }
-}
-
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/BaseActivity.java b/src/com/android/deskclock/BaseActivity.java
index 6594354..1d99d1f 100644
--- a/src/com/android/deskclock/BaseActivity.java
+++ b/src/com/android/deskclock/BaseActivity.java
@@ -24,9 +24,7 @@
import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.v7.app.AppCompatActivity;
-
-import com.android.deskclock.uidata.OnAppColorChangeListener;
-import com.android.deskclock.uidata.UiDataModel;
+import android.view.View;
import static com.android.deskclock.AnimatorUtils.ARGB_EVALUATOR;
@@ -35,12 +33,6 @@
*/
public abstract class BaseActivity extends AppCompatActivity {
- /** Key used to save/restore the current app window color from the saved instance state. */
- private static final String KEY_WINDOW_COLOR = "window_color";
-
- /** Reacts to app window color changes from the model when this activity is forward. */
- private final OnAppColorChangeListener mAppColorChangeListener = new AppColorChangeListener();
-
/** Sets the app window color on each frame of the {@link #mAppColorAnimator}. */
private final AppColorAnimationListener mAppColorAnimationListener
= new AppColorAnimationListener();
@@ -55,46 +47,29 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- final @ColorInt int currentColor = UiDataModel.getUiDataModel().getWindowBackgroundColor();
- final @ColorInt int bgColor = savedInstanceState == null ? currentColor
- : savedInstanceState.getInt(KEY_WINDOW_COLOR, currentColor);
- adjustAppColor(bgColor, false /* animate */);
+ // Allow the content to layout behind the status and navigation bars.
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+
+ final @ColorInt int color = ThemeUtils.resolveColor(this, android.R.attr.windowBackground);
+ adjustAppColor(color, false /* animate */);
}
@Override
protected void onStart() {
super.onStart();
- // Start updating the app window color each time it changes.
- UiDataModel.getUiDataModel().addOnAppColorChangeListener(mAppColorChangeListener);
-
// Ensure the app window color is up-to-date.
- final @ColorInt int bgColor = UiDataModel.getUiDataModel().getWindowBackgroundColor();
- adjustAppColor(bgColor, true /* animate */);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
-
- // Stop updating the app window color when not active.
- UiDataModel.getUiDataModel().removeOnAppColorChangeListener(mAppColorChangeListener);
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- // Save the app window color so we can animate the change when the activity is restored.
- if (mBackground != null) {
- outState.putInt(KEY_WINDOW_COLOR, mBackground.getColor());
- }
+ final @ColorInt int color = ThemeUtils.resolveColor(this, android.R.attr.windowBackground);
+ adjustAppColor(color, false /* animate */);
}
/**
* Adjusts the current app window color of this activity; animates the change if desired.
*
- * @param color the ARGB value to set as the current app window color
+ * @param color the ARGB value to set as the current app window color
* @param animate {@code true} if the change should be animated
*/
protected void adjustAppColor(@ColorInt int color, boolean animate) {
@@ -123,30 +98,8 @@
}
}
- /**
- * Subclasses may react to the new app window color as they see fit.
- *
- * @param color the newly installed app window color
- */
- protected void onAppColorChanged(@ColorInt int color) {
- // Do nothing here, only in derived classes
- }
-
private void setAppColor(@ColorInt int color) {
mBackground.setColor(color);
-
- // Allow the activity and its hierarchy to react to the app window color change.
- onAppColorChanged(color);
- }
-
- /**
- * Alters the app window color each time the model reports a change.
- */
- private final class AppColorChangeListener implements OnAppColorChangeListener {
- @Override
- public void onAppColorChange(@ColorInt int oldColor, @ColorInt int newColor) {
- adjustAppColor(newColor, true /* animate */);
- }
}
/**
@@ -167,4 +120,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/deskclock/CircleButtonsLayout.java b/src/com/android/deskclock/CircleButtonsLayout.java
index 1830ef8..079472e 100644
--- a/src/com/android/deskclock/CircleButtonsLayout.java
+++ b/src/com/android/deskclock/CircleButtonsLayout.java
@@ -4,8 +4,8 @@
import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.View;
+import android.widget.Button;
import android.widget.FrameLayout;
-import android.widget.ImageButton;
import android.widget.TextView;
/**
@@ -18,7 +18,7 @@
private float mDiamOffset;
private View mCircleView;
- private ImageButton mResetAddButton;
+ private Button mResetAddButton;
private TextView mLabel;
@SuppressWarnings("unused")
@@ -50,7 +50,7 @@
if (mLabel == null) {
mCircleView = findViewById(R.id.timer_time);
mLabel = (TextView) findViewById(R.id.timer_label);
- mResetAddButton = (ImageButton) findViewById(R.id.reset_add);
+ mResetAddButton = (Button) findViewById(R.id.reset_add);
}
final int frameWidth = mCircleView.getMeasuredWidth();
diff --git a/src/com/android/deskclock/ClockFragment.java b/src/com/android/deskclock/ClockFragment.java
index 84cde0b..7afe72d 100644
--- a/src/com/android/deskclock/ClockFragment.java
+++ b/src/com/android/deskclock/ClockFragment.java
@@ -31,12 +31,13 @@
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
+import android.text.format.DateUtils;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageButton;
+import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextClock;
import android.widget.TextView;
@@ -50,7 +51,6 @@
import java.util.Calendar;
import java.util.List;
-import java.util.Locale;
import java.util.TimeZone;
import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
@@ -75,7 +75,8 @@
private ContentObserver mAlarmObserver;
private TextClock mDigitalClock;
- private View mAnalogClock, mClockFrame;
+ private AnalogClock mAnalogClock;
+ private View mClockFrame;
private SelectedCitiesAdapter mCityAdapter;
private RecyclerView mCityList;
private String mDateFormat;
@@ -111,6 +112,7 @@
mCityList = (RecyclerView) fragmentView.findViewById(R.id.cities);
mCityList.setLayoutManager(new LinearLayoutManager(getActivity()));
mCityList.setAdapter(mCityAdapter);
+ mCityList.setItemAnimator(null);
DataModel.getDataModel().addCityListener(mCityAdapter);
final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
@@ -125,11 +127,11 @@
mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane);
if (mClockFrame != null) {
mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock);
- mAnalogClock = mClockFrame.findViewById(R.id.analog_clock);
- Utils.refreshAlarm(context, mClockFrame);
- Utils.setTimeFormat(mDigitalClock);
+ mAnalogClock = (AnalogClock) mClockFrame.findViewById(R.id.analog_clock);
+ Utils.setClockIconTypeface(mClockFrame);
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame);
Utils.setClockStyle(mDigitalClock, mAnalogClock);
+ Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
}
// Schedule a runnable to update the date every quarter hour.
@@ -139,15 +141,6 @@
}
@Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if (mDigitalClock != null) {
- Utils.setTimeFormat(mDigitalClock);
- }
- }
-
- @Override
public void onResume() {
super.onResume();
@@ -162,9 +155,10 @@
activity.registerReceiver(mAlarmChangeReceiver, filter);
}
- // Resume can be invoked after changing the clock style.
+ // Resume can be invoked after changing the clock style or seconds display.
if (mDigitalClock != null && mAnalogClock != null) {
Utils.setClockStyle(mDigitalClock, mAnalogClock);
+ Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
}
final View view = getView();
@@ -216,7 +210,7 @@
}
@Override
- public void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
+ public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
left.setVisibility(INVISIBLE);
right.setVisibility(INVISIBLE);
}
@@ -254,13 +248,16 @@
private final GestureDetector mGestureDetector;
- public CityListOnLongClickListener(Context context) {
+ private CityListOnLongClickListener(Context context) {
mGestureDetector = new GestureDetector(context, this);
}
@Override
public void onLongPress(MotionEvent e) {
- getView().performLongClick();
+ final View view = getView();
+ if (view != null) {
+ view.performLongClick();
+ }
}
@Override
@@ -292,7 +289,7 @@
* {@link AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED}.
*/
private final class AlarmObserverPreL extends ContentObserver {
- public AlarmObserverPreL() {
+ private AlarmObserverPreL() {
super(new Handler());
}
@@ -349,7 +346,7 @@
private final String mDateFormat;
private final String mDateFormatForAccessibility;
- public SelectedCitiesAdapter(Context context, String dateFormat,
+ private SelectedCitiesAdapter(Context context, String dateFormat,
String dateFormatForAccessibility) {
mContext = context;
mDateFormat = dateFormat;
@@ -422,7 +419,7 @@
return DataModel.getDataModel().getSelectedCities();
}
- public void refreshAlarm() {
+ private void refreshAlarm() {
if (mIsPortrait && getItemCount() > 0) {
notifyItemChanged(0);
}
@@ -436,32 +433,35 @@
private static final class CityViewHolder extends RecyclerView.ViewHolder {
private final TextView mName;
- private final TextView mCityDay;
private final TextClock mDigitalClock;
private final AnalogClock mAnalogClock;
+ private final TextView mHoursAhead;
private CityViewHolder(View itemView) {
super(itemView);
mName = (TextView) itemView.findViewById(R.id.city_name);
- mCityDay = (TextView) itemView.findViewById(R.id.city_day);
mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock);
mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock);
+ mHoursAhead = (TextView) itemView.findViewById(R.id.hours_ahead);
}
private void bind(Context context, City city, int position, boolean isPortrait) {
+ final String cityTimeZoneId = city.getTimeZone().getID();
+
// Configure the digital clock or analog clock depending on the user preference.
if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) {
mDigitalClock.setVisibility(GONE);
mAnalogClock.setVisibility(VISIBLE);
- mAnalogClock.setTimeZone(city.getTimeZone().getID());
+ mAnalogClock.setTimeZone(cityTimeZoneId);
mAnalogClock.enableSeconds(false);
} else {
mAnalogClock.setVisibility(GONE);
mDigitalClock.setVisibility(VISIBLE);
- mDigitalClock.setTimeZone(city.getTimeZone().getID());
- mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.22f /* amPmRatio */));
- mDigitalClock.setFormat24Hour(Utils.get24ModeFormat());
+ mDigitalClock.setTimeZone(cityTimeZoneId);
+ mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */,
+ false));
+ mDigitalClock.setFormat24Hour(Utils.get24ModeFormat(false));
}
// Supply top and bottom padding dynamically.
@@ -482,13 +482,37 @@
final boolean displayDayOfWeek =
localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
- // Bind the week day display.
- mCityDay.setVisibility(displayDayOfWeek ? VISIBLE : GONE);
- if (displayDayOfWeek) {
- final Locale locale = Locale.getDefault();
- final String weekday = cityCal.getDisplayName(DAY_OF_WEEK, Calendar.SHORT,
- locale);
- mCityDay.setText(context.getString(R.string.world_day_of_week_label, weekday));
+ // Compare offset from UTC time on today's date (daylight savings time, etc.)
+ final TimeZone currentTimeZone = TimeZone.getDefault();
+ final TimeZone cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId);
+ final long currentTimeMillis = System.currentTimeMillis();
+ final long currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis);
+ final long cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis);
+ final long offsetDelta = cityUtcOffset - currentUtcOffset;
+
+ final int hoursDifferent = (int) (offsetDelta / DateUtils.HOUR_IN_MILLIS);
+ final int minutesDifferent = (int) (offsetDelta / DateUtils.MINUTE_IN_MILLIS) % 60;
+ final boolean displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0;
+ final boolean isAhead = hoursDifferent > 0 || (hoursDifferent == 0
+ && minutesDifferent > 0);
+ if (!Utils.isLandscape(context)) {
+ // Bind the number of hours ahead or behind, or hide if the time is the same.
+ final boolean displayDifference = hoursDifferent != 0 || displayMinutes;
+ mHoursAhead.setVisibility(displayDifference ? VISIBLE : GONE);
+ final String timeString = Utils.createHoursDifferentString(
+ context, displayMinutes, isAhead, hoursDifferent, minutesDifferent);
+ mHoursAhead.setText(displayDayOfWeek ?
+ (context.getString(isAhead ? R.string.world_hours_tomorrow
+ : R.string.world_hours_yesterday, timeString))
+ : timeString);
+ } else {
+ // Only tomorrow/yesterday should be shown in landscape view.
+ mHoursAhead.setVisibility(displayDayOfWeek ? View.VISIBLE : View.GONE);
+ if (displayDayOfWeek) {
+ mHoursAhead.setText(context.getString(isAhead ? R.string.world_tomorrow
+ : R.string.world_yesterday));
+ }
+
}
}
}
@@ -505,15 +529,18 @@
mHairline = itemView.findViewById(R.id.hairline);
mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock);
mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock);
+ Utils.setClockIconTypeface(itemView);
}
private void bind(Context context, String dateFormat,
String dateFormatForAccessibility, boolean showHairline) {
Utils.refreshAlarm(context, itemView);
- Utils.setTimeFormat(mDigitalClock);
+
Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView);
Utils.setClockStyle(mDigitalClock, mAnalogClock);
mHairline.setVisibility(showHairline ? VISIBLE : GONE);
+
+ Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
}
}
}
diff --git a/src/com/android/deskclock/DeskClock.java b/src/com/android/deskclock/DeskClock.java
index 02a65ea..6554a35 100644
--- a/src/com/android/deskclock/DeskClock.java
+++ b/src/com/android/deskclock/DeskClock.java
@@ -20,70 +20,44 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.NotificationManager;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.media.AudioManager;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Build;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
-import android.os.Handler;
-import android.provider.Settings;
-import android.support.annotation.ColorInt;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
-import android.support.design.widget.TabLayout.ViewPagerOnTabSelectedListener;
-import android.support.v13.app.FragmentPagerAdapter;
-import android.support.v4.content.ContextCompat;
+import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
-import android.support.v7.app.AppCompatActivity;
+import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
-import android.view.ViewGroup;
-import android.widget.ImageButton;
+import android.widget.Button;
import android.widget.ImageView;
+import android.widget.TextView;
import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
import com.android.deskclock.actionbarmenu.OptionsMenuManager;
import com.android.deskclock.actionbarmenu.SettingsMenuItemController;
import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.DataModel.SilentSetting;
+import com.android.deskclock.data.OnSilentSettingsListener;
import com.android.deskclock.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.uidata.TabListener;
import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.uidata.UiDataModel.Tab;
-import com.android.deskclock.widget.RtlViewPager;
import com.android.deskclock.widget.toast.SnackbarManager;
-import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
-import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
-import static android.media.AudioManager.FLAG_SHOW_UI;
-import static android.media.AudioManager.STREAM_ALARM;
-import static android.media.RingtoneManager.TYPE_ALARM;
-import static android.provider.Settings.System.CONTENT_URI;
-import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static com.android.deskclock.AnimatorUtils.getAlphaAnimator;
import static com.android.deskclock.AnimatorUtils.getScaleAnimator;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_IMMEDIATE;
/**
* The main activity of the application which displays 4 different tabs contains alarms, world
@@ -92,14 +66,6 @@
public class DeskClock extends BaseActivity
implements FabContainer, LabelDialogFragment.AlarmLabelDialogHandler {
- /** The Uri to the settings entry that stores alarm stream volume. */
- private static final Uri VOLUME_URI = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker");
-
- /** The intent filter that identifies do-not-disturb change broadcasts. */
- @SuppressLint("NewApi")
- private static final IntentFilter DND_CHANGE_FILTER
- = new IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED);
-
/** Models the interesting state of display the {@link #mFab} button may inhabit. */
private enum FabState { SHOWING, HIDE_ARMED, HIDING }
@@ -115,38 +81,21 @@
/** Hides, updates, and shows only the {@link #mFab}; the buttons are untouched. */
private final AnimatorSet mUpdateFabOnlyAnimation = new AnimatorSet();
+ /** Hides, updates, and shows only the {@link #mLeftButton} and {@link #mRightButton}. */
+ private final AnimatorSet mUpdateButtonsOnlyAnimation = new AnimatorSet();
+
/** Automatically starts the {@link #mShowAnimation} after {@link #mHideAnimation} ends. */
private final AnimatorListenerAdapter mAutoStartShowListener = new AutoStartShowListener();
/** Updates the user interface to reflect the selected tab from the backing model. */
private final TabListener mTabChangeWatcher = new TabChangeWatcher();
- /** Displays a snackbar explaining that the system default alarm ringtone is silent. */
- private final Runnable mShowSilentAlarmSnackbarRunnable = new ShowSilentAlarmSnackbarRunnable();
+ /** Shows/hides a snackbar explaining which setting is suppressing alarms from firing. */
+ private final OnSilentSettingsListener mSilentSettingChangeWatcher =
+ new SilentSettingChangeWatcher();
- /** Observes default alarm ringtone changes while the app is in the foreground. */
- private final ContentObserver mAlarmRingtoneChangeObserver = new AlarmRingtoneChangeObserver();
-
- /** Displays a snackbar explaining that the alarm volume is muted. */
- private final Runnable mShowMutedVolumeSnackbarRunnable = new ShowMutedVolumeSnackbarRunnable();
-
- /** Observes alarm volume changes while the app is in the foreground. */
- private final ContentObserver mAlarmVolumeChangeObserver = new AlarmVolumeChangeObserver();
-
- /** Displays a snackbar explaining that do-not-disturb is blocking alarms. */
- private final Runnable mShowDNDBlockingSnackbarRunnable = new ShowDNDBlockingSnackbarRunnable();
-
- /** Observes do-not-disturb changes while the app is in the foreground. */
- private final BroadcastReceiver mDoNotDisturbChangeReceiver = new DoNotDisturbChangeReceiver();
-
- /** Used to query the alarm volume and display the system control to change the alarm volume. */
- private AudioManager mAudioManager;
-
- /** Used to query the do-not-disturb setting value, also called "interruption filter". */
- private NotificationManager mNotificationManager;
-
- /** {@code true} permits the muted alarm volume snackbar to show when starting this activity. */
- private boolean mShowSilencedAlarmsSnackbar;
+ /** Displays a snackbar explaining why alarms may not fire or may fire silently. */
+ private Runnable mShowSilentSettingSnackbarRunnable;
/** The view to which snackbar items are anchored. */
private View mSnackbarAnchor;
@@ -158,19 +107,19 @@
private ImageView mFab;
/** The button left of the {@link #mFab} shared across all tabs in the user interface. */
- private ImageButton mLeftButton;
+ private Button mLeftButton;
/** The button right of the {@link #mFab} shared across all tabs in the user interface. */
- private ImageButton mRightButton;
+ private Button mRightButton;
/** The controller that shows the drop shadow when content is not scrolled to the top. */
private DropShadowController mDropShadowController;
/** The ViewPager that pages through the fragments representing the content of the tabs. */
- private RtlViewPager mFragmentTabPager;
+ private ViewPager mFragmentTabPager;
/** Generates the fragments that are displayed by the {@link #mFragmentTabPager}. */
- private TabFragmentAdapter mFragmentTabPagerAdapter;
+ private FragmentTabPagerAdapter mFragmentTabPagerAdapter;
/** The container that stores the tab headers. */
private TabLayout mTabLayout;
@@ -191,43 +140,68 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.desk_clock);
-
- mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
- mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-
- // Don't show the volume muted snackbar on rotations.
- mShowSilencedAlarmsSnackbar = savedInstanceState == null;
- mSnackbarAnchor = findViewById(R.id.coordinator);
+ mSnackbarAnchor = findViewById(R.id.content);
// Configure the toolbar.
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
- getSupportActionBar().setDisplayShowTitleEnabled(false);
+
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(false);
+ }
// Configure the menu item controllers add behavior to the toolbar.
- mOptionsMenuManager
- .addMenuItemController(new NightModeMenuItemController(this))
- .addMenuItemController(new SettingsMenuItemController(this))
- .addMenuItemController(MenuItemControllerFactory.getInstance()
- .buildMenuItemControllers(this));
+ mOptionsMenuManager.addMenuItemController(
+ new NightModeMenuItemController(this), new SettingsMenuItemController(this));
+ mOptionsMenuManager.addMenuItemController(
+ MenuItemControllerFactory.getInstance().buildMenuItemControllers(this));
// Inflate the menu during creation to avoid a double layout pass. Otherwise, the menu
// inflation occurs *after* the initial draw and a second layout pass adds in the menu.
onCreateOptionsMenu(toolbar.getMenu());
// Create the tabs that make up the user interface.
- mTabLayout = (TabLayout) findViewById(R.id.sliding_tabs);
- for (int i = 0; i < UiDataModel.getUiDataModel().getTabCount(); i++) {
- final Tab tab = UiDataModel.getUiDataModel().getTab(i);
- mTabLayout.addTab(mTabLayout.newTab()
- .setIcon(tab.getIconId())
- .setContentDescription(tab.getContentDescriptionId()));
+ mTabLayout = (TabLayout) findViewById(R.id.tabs);
+ final int tabCount = UiDataModel.getUiDataModel().getTabCount();
+ final boolean showTabLabel = getResources().getBoolean(R.bool.showTabLabel);
+ final boolean showTabHorizontally = getResources().getBoolean(R.bool.showTabHorizontally);
+ for (int i = 0; i < tabCount; i++) {
+ final UiDataModel.Tab tabModel = UiDataModel.getUiDataModel().getTab(i);
+ final @StringRes int labelResId = tabModel.getLabelResId();
+
+ final TabLayout.Tab tab = mTabLayout.newTab()
+ .setTag(tabModel)
+ .setIcon(tabModel.getIconResId())
+ .setContentDescription(labelResId);
+
+ if (showTabLabel) {
+ tab.setText(labelResId);
+ tab.setCustomView(R.layout.tab_item);
+
+ @SuppressWarnings("ConstantConditions")
+ final TextView text = (TextView) tab.getCustomView()
+ .findViewById(android.R.id.text1);
+ text.setTextColor(mTabLayout.getTabTextColors());
+
+ // Bind the icon to the TextView.
+ final Drawable icon = tab.getIcon();
+ if (showTabHorizontally) {
+ // Remove the icon so it doesn't affect the minimum TabLayout height.
+ tab.setIcon(null);
+ text.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
+ } else {
+ text.setCompoundDrawablesRelativeWithIntrinsicBounds(null, icon, null, null);
+ }
+ }
+
+ mTabLayout.addTab(tab);
}
// Configure the buttons shared by the tabs.
mFab = (ImageView) findViewById(R.id.fab);
- mLeftButton = (ImageButton) findViewById(R.id.left_button);
- mRightButton = (ImageButton) findViewById(R.id.right_button);
+ mLeftButton = (Button) findViewById(R.id.left_button);
+ mRightButton = (Button) findViewById(R.id.right_button);
mFab.setOnClickListener(new OnClickListener() {
@Override
@@ -248,90 +222,94 @@
}
});
- // Build the reusable animations that hide and show the fab and left/right buttons.
- // These may be used independently or be chained together.
final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
- mHideAnimation
- .setDuration(duration)
- .play(getScaleAnimator(mFab, 1f, 0f))
- .with(getAlphaAnimator(mLeftButton, 1f, 0f))
- .with(getAlphaAnimator(mRightButton, 1f, 0f));
- mShowAnimation
- .setDuration(duration)
- .play(getScaleAnimator(mFab, 0f, 1f))
- .with(getAlphaAnimator(mLeftButton, 0f, 1f))
- .with(getAlphaAnimator(mRightButton, 0f, 1f));
-
- // Build the reusable animation that hides and shows only the fab.
final ValueAnimator hideFabAnimation = getScaleAnimator(mFab, 1f, 0f);
+ final ValueAnimator showFabAnimation = getScaleAnimator(mFab, 0f, 1f);
+
+ final ValueAnimator leftHideAnimation = getScaleAnimator(mLeftButton, 1f, 0f);
+ final ValueAnimator rightHideAnimation = getScaleAnimator(mRightButton, 1f, 0f);
+ final ValueAnimator leftShowAnimation = getScaleAnimator(mLeftButton, 0f, 1f);
+ final ValueAnimator rightShowAnimation = getScaleAnimator(mRightButton, 0f, 1f);
+
hideFabAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
getSelectedDeskClockFragment().onUpdateFab(mFab);
}
});
- final ValueAnimator showFabAnimation = getScaleAnimator(mFab, 0f, 1f);
+
+ leftHideAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ getSelectedDeskClockFragment().onUpdateFabButtons(mLeftButton, mRightButton);
+ }
+ });
+
+ // Build the reusable animations that hide and show the fab and left/right buttons.
+ // These may be used independently or be chained together.
+ mHideAnimation
+ .setDuration(duration)
+ .play(hideFabAnimation)
+ .with(leftHideAnimation)
+ .with(rightHideAnimation);
+
+ mShowAnimation
+ .setDuration(duration)
+ .play(showFabAnimation)
+ .with(leftShowAnimation)
+ .with(rightShowAnimation);
+
+ // Build the reusable animation that hides and shows only the fab.
mUpdateFabOnlyAnimation
.setDuration(duration)
.play(showFabAnimation)
.after(hideFabAnimation);
+ // Build the reusable animation that hides and shows only the buttons.
+ mUpdateButtonsOnlyAnimation
+ .setDuration(duration)
+ .play(leftShowAnimation)
+ .with(rightShowAnimation)
+ .after(leftHideAnimation)
+ .after(rightHideAnimation);
+
// Customize the view pager.
- mFragmentTabPagerAdapter = new TabFragmentAdapter(this);
- mFragmentTabPager = (RtlViewPager) findViewById(R.id.desk_clock_pager);
+ mFragmentTabPagerAdapter = new FragmentTabPagerAdapter(this);
+ mFragmentTabPager = (ViewPager) findViewById(R.id.desk_clock_pager);
// Keep all four tabs to minimize jank.
mFragmentTabPager.setOffscreenPageLimit(3);
// Set Accessibility Delegate to null so view pager doesn't intercept movements and
// prevent the fab from being selected.
mFragmentTabPager.setAccessibilityDelegate(null);
// Mirror changes made to the selected page of the view pager into UiDataModel.
- mFragmentTabPager.setOnRTLPageChangeListener(new PageChangeWatcher());
+ mFragmentTabPager.addOnPageChangeListener(new PageChangeWatcher());
mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter);
- // Selecting a tab implicitly selects a page in the view pager.
- mTabLayout.setOnTabSelectedListener(new ViewPagerOnTabSelectedListener(mFragmentTabPager));
+ // Mirror changes made to the selected tab into UiDataModel.
+ mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ UiDataModel.getUiDataModel().setSelectedTab((UiDataModel.Tab) tab.getTag());
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ }
+ });
// Honor changes to the selected tab from outside entities.
UiDataModel.getUiDataModel().addTabListener(mTabChangeWatcher);
-
- if (savedInstanceState == null) {
- // Set the background color to initially match the theme value so that we can
- // smoothly transition to the dynamic color.
- final int backgroundColor = ContextCompat.getColor(this, R.color.default_background);
- adjustAppColor(backgroundColor, false /* animate */);
- }
}
@Override
protected void onStart() {
super.onStart();
-
- if (mShowSilencedAlarmsSnackbar) {
- if (isDoNotDisturbBlockingAlarms()) {
- mSnackbarAnchor.postDelayed(mShowDNDBlockingSnackbarRunnable, SECOND_IN_MILLIS);
- } else if (isAlarmStreamMuted()) {
- mSnackbarAnchor.postDelayed(mShowMutedVolumeSnackbarRunnable, SECOND_IN_MILLIS);
- } else if (isSystemAlarmRingtoneSilent()) {
- mSnackbarAnchor.postDelayed(mShowSilentAlarmSnackbarRunnable, SECOND_IN_MILLIS);
- }
- }
-
- // Subsequent starts of this activity should show the snackbar by default.
- mShowSilencedAlarmsSnackbar = true;
-
- final ContentResolver cr = getContentResolver();
- // Watch for system alarm ringtone changes while the app is in the foreground.
- cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, mAlarmRingtoneChangeObserver);
-
- // Watch for alarm volume changes while the app is in the foreground.
- cr.registerContentObserver(VOLUME_URI, false, mAlarmVolumeChangeObserver);
-
- if (Utils.isMOrLater()) {
- // Watch for do-not-disturb changes while the app is in the foreground.
- registerReceiver(mDoNotDisturbChangeReceiver, DND_CHANGE_FILTER);
- }
-
+ DataModel.getDataModel().addSilentSettingsListener(mSilentSettingChangeWatcher);
DataModel.getDataModel().setApplicationInForeground(true);
}
@@ -340,10 +318,11 @@
super.onResume();
final View dropShadow = findViewById(R.id.drop_shadow);
- mDropShadowController = new DropShadowController(dropShadow, UiDataModel.getUiDataModel());
+ mDropShadowController = new DropShadowController(dropShadow, UiDataModel.getUiDataModel(),
+ mSnackbarAnchor.findViewById(R.id.tab_hairline));
- // Honor the selected tab in case it changed while the app was paused.
- updateCurrentTab(UiDataModel.getUiDataModel().getSelectedTabIndex());
+ // ViewPager does not save state; this honors the selected tab in the user interface.
+ updateCurrentTab();
}
@Override
@@ -376,25 +355,11 @@
@Override
protected void onStop() {
+ DataModel.getDataModel().removeSilentSettingsListener(mSilentSettingChangeWatcher);
if (!isChangingConfigurations()) {
DataModel.getDataModel().setApplicationInForeground(false);
}
- // Stop watching for system alarm ringtone changes while the app is in the background.
- getContentResolver().unregisterContentObserver(mAlarmRingtoneChangeObserver);
-
- // Stop watching for alarm volume changes while the app is in the background.
- getContentResolver().unregisterContentObserver(mAlarmVolumeChangeObserver);
-
- if (Utils.isMOrLater()) {
- // Stop watching for do-not-disturb changes while the app is in the background.
- unregisterReceiver(mDoNotDisturbChangeReceiver);
- }
-
- // Remove any scheduled work to show snackbars; it is no longer relevant.
- mSnackbarAnchor.removeCallbacks(mShowSilentAlarmSnackbarRunnable);
- mSnackbarAnchor.removeCallbacks(mShowDNDBlockingSnackbarRunnable);
- mSnackbarAnchor.removeCallbacks(mShowMutedVolumeSnackbarRunnable);
super.onStop();
}
@@ -439,60 +404,51 @@
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (getSelectedDeskClockFragment().onKeyDown(keyCode,event)) {
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
-
- /**
- * @param color the newly installed window background color
- */
- @Override
- protected void onAppColorChanged(@ColorInt int color) {
- super.onAppColorChanged(color);
-
- // Notify each fragment of the background color change.
- for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
- mFragmentTabPagerAdapter.getItem(i).onAppColorChanged(color);
- }
+ return getSelectedDeskClockFragment().onKeyDown(keyCode,event)
+ || super.onKeyDown(keyCode, event);
}
@Override
- public void updateFab(UpdateType updateType) {
- switch (updateType) {
- case DISABLE_BUTTONS: {
- mLeftButton.setEnabled(false);
- mRightButton.setEnabled(false);
- break;
- }
- case FAB_AND_BUTTONS_IMMEDIATE: {
- final DeskClockFragment f = getSelectedDeskClockFragment();
- f.onUpdateFab(mFab);
- f.onUpdateFabButtons(mLeftButton, mRightButton);
- break;
- }
- case FAB_AND_BUTTONS_MORPH: {
- final DeskClockFragment f = getSelectedDeskClockFragment();
- f.onUpdateFab(mFab);
- f.onMorphFabButtons(mLeftButton, mRightButton);
- break;
- }
- case FAB_ONLY_SHRINK_AND_EXPAND: {
+ public void updateFab(@UpdateFabFlag int updateType) {
+ final DeskClockFragment f = getSelectedDeskClockFragment();
+
+ switch (updateType & FAB_ANIMATION_MASK) {
+ case FAB_SHRINK_AND_EXPAND:
mUpdateFabOnlyAnimation.start();
break;
- }
- case FAB_AND_BUTTONS_SHRINK_AND_EXPAND: {
- // Ensure there is never more than one mAutoStartShowListener registered.
- mHideAnimation.removeListener(mAutoStartShowListener);
- mHideAnimation.addListener(mAutoStartShowListener);
- mHideAnimation.start();
+ case FAB_IMMEDIATE:
+ f.onUpdateFab(mFab);
break;
- }
- case FAB_REQUESTS_FOCUS: {
+ case FAB_MORPH:
+ f.onMorphFab(mFab);
+ break;
+ }
+ switch (updateType & FAB_REQUEST_FOCUS_MASK) {
+ case FAB_REQUEST_FOCUS:
mFab.requestFocus();
break;
- }
+ }
+ switch (updateType & BUTTONS_ANIMATION_MASK) {
+ case BUTTONS_IMMEDIATE:
+ f.onUpdateFabButtons(mLeftButton, mRightButton);
+ break;
+ case BUTTONS_SHRINK_AND_EXPAND:
+ mUpdateButtonsOnlyAnimation.start();
+ break;
+ }
+ switch (updateType & BUTTONS_DISABLE_MASK) {
+ case BUTTONS_DISABLE:
+ mLeftButton.setClickable(false);
+ mRightButton.setClickable(false);
+ break;
+ }
+ switch (updateType & FAB_AND_BUTTONS_SHRINK_EXPAND_MASK) {
+ case FAB_AND_BUTTONS_SHRINK:
+ mHideAnimation.start();
+ break;
+ case FAB_AND_BUTTONS_EXPAND:
+ mShowAnimation.start();
+ break;
}
}
@@ -506,91 +462,44 @@
}
/**
- * Configure the {@link #mFragmentTabPager} and {@link #mTabLayout} to display the tab at the
- * given {@code index}.
- *
- * @param index the index of the page to display
+ * Configure the {@link #mFragmentTabPager} and {@link #mTabLayout} to display UiDataModel's
+ * selected tab.
*/
- private void updateCurrentTab(int index) {
- final TabLayout.Tab tab = mTabLayout.getTabAt(index);
- if (tab != null && !tab.isSelected()) {
- tab.select();
+ private void updateCurrentTab() {
+ // Fetch the selected tab from the source of truth: UiDataModel.
+ final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
+
+ // Update the selected tab in the tablayout if it does not agree with UiDataModel.
+ for (int i = 0; i < mTabLayout.getTabCount(); i++) {
+ final TabLayout.Tab tab = mTabLayout.getTabAt(i);
+ if (tab != null && tab.getTag() == selectedTab && !tab.isSelected()) {
+ tab.select();
+ break;
+ }
}
- if (mFragmentTabPager.getCurrentItem() != index) {
- mFragmentTabPager.setCurrentItem(index);
+
+ // Update the selected fragment in the viewpager if it does not agree with UiDataModel.
+ for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
+ final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
+ if (fragment.isTabSelected() && mFragmentTabPager.getCurrentItem() != i) {
+ mFragmentTabPager.setCurrentItem(i);
+ break;
+ }
}
}
+ /**
+ * @return the DeskClockFragment that is currently selected according to UiDataModel
+ */
private DeskClockFragment getSelectedDeskClockFragment() {
- final int index = UiDataModel.getUiDataModel().getSelectedTabIndex();
- return mFragmentTabPagerAdapter.getItem(index);
- }
-
- private boolean isSystemAlarmRingtoneSilent() {
- try {
- return RingtoneManager.getActualDefaultRingtoneUri(this, TYPE_ALARM) == null;
- } catch (Exception e) {
- // Since this is purely informational, avoid crashing the app.
- return false;
- }
- }
-
- private void showSilentRingtoneSnackbar() {
- final OnClickListener changeClickListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
+ final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
+ if (fragment.isTabSelected()) {
+ return fragment;
}
- };
-
- SnackbarManager.show(
- createSnackbar(R.string.silent_default_alarm_ringtone)
- .setAction(R.string.change_default_alarm_ringtone, changeClickListener)
- );
- }
-
- private boolean isAlarmStreamMuted() {
- try {
- return mAudioManager.getStreamVolume(STREAM_ALARM) <= 0;
- } catch (Exception e) {
- // Since this is purely informational, avoid crashing the app.
- return false;
}
- }
-
- private void showAlarmVolumeMutedSnackbar() {
- final OnClickListener unmuteClickListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- // Set the alarm volume to ~30% of max and show the slider UI.
- final int index = mAudioManager.getStreamMaxVolume(STREAM_ALARM) / 3;
- mAudioManager.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI);
- }
- };
-
- SnackbarManager.show(
- createSnackbar(R.string.alarm_volume_muted)
- .setAction(R.string.unmute_alarm_volume, unmuteClickListener)
- );
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- private boolean isDoNotDisturbBlockingAlarms() {
- if (!Utils.isMOrLater()) {
- return false;
- }
-
- try {
- return mNotificationManager.getCurrentInterruptionFilter() == INTERRUPTION_FILTER_NONE;
- } catch (Exception e) {
- // Since this is purely informational, avoid crashing the app.
- return false;
- }
- }
-
- private void showDoNotDisturbIsBlockingAlarmsSnackbar() {
- SnackbarManager.show(createSnackbar(R.string.alarms_blocked_by_dnd));
+ final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
+ throw new IllegalStateException("Unable to locate selected fragment (" + selectedTab + ")");
}
/**
@@ -624,7 +533,6 @@
mHideAnimation.addListener(mAutoStartShowListener);
mHideAnimation.start();
mFabState = FabState.HIDING;
-
} else if (mPriorState == SCROLL_STATE_SETTLING && state == SCROLL_STATE_DRAGGING) {
// The user has interrupted settling on a tab and the fab button must be re-hidden.
if (mShowAnimation.isStarted()) {
@@ -663,7 +571,7 @@
@Override
public void onPageSelected(int position) {
- UiDataModel.getUiDataModel().setSelectedTabIndex(position);
+ mFragmentTabPagerAdapter.getDeskClockFragment(position).selectTab();
}
}
@@ -689,87 +597,47 @@
}
/**
- * Displays a snackbar that indicates the system default alarm ringtone currently silent and
- * offers an action that displays the system alarm ringtone setting to adjust it.
+ * Shows/hides a snackbar as silencing settings are enabled/disabled.
*/
- private final class ShowSilentAlarmSnackbarRunnable implements Runnable {
+ private final class SilentSettingChangeWatcher implements OnSilentSettingsListener {
@Override
- public void run() {
- showSilentRingtoneSnackbar();
- }
- }
+ public void onSilentSettingsChange(SilentSetting before, SilentSetting after) {
+ if (mShowSilentSettingSnackbarRunnable != null) {
+ mSnackbarAnchor.removeCallbacks(mShowSilentSettingSnackbarRunnable);
+ mShowSilentSettingSnackbarRunnable = null;
+ }
- /**
- * Displays a snackbar that indicates the alarm volume is currently muted and offers an action
- * that displays the system volume control to adjust it.
- */
- private final class ShowMutedVolumeSnackbarRunnable implements Runnable {
- @Override
- public void run() {
- showAlarmVolumeMutedSnackbar();
- }
- }
-
- /**
- * Displays a snackbar that indicates the do-not-disturb setting is currently blocking alarms.
- */
- private final class ShowDNDBlockingSnackbarRunnable implements Runnable {
- @Override
- public void run() {
- showDoNotDisturbIsBlockingAlarmsSnackbar();
- }
- }
-
- /**
- * Observe changes to the system default alarm ringtone while the application is in the
- * foreground and show/hide the snackbar that warns when the ringtone is silent.
- */
- private final class AlarmRingtoneChangeObserver extends ContentObserver {
- private AlarmRingtoneChangeObserver() {
- super(new Handler());
- }
-
- @Override
- public void onChange(boolean selfChange) {
- if (isSystemAlarmRingtoneSilent()) {
- showSilentRingtoneSnackbar();
- } else {
+ if (after == null) {
SnackbarManager.dismiss();
+ } else {
+ mShowSilentSettingSnackbarRunnable = new ShowSilentSettingSnackbarRunnable(after);
+ mSnackbarAnchor.postDelayed(mShowSilentSettingSnackbarRunnable, SECOND_IN_MILLIS);
}
}
}
/**
- * Observe changes to the alarm stream volume while the application is in the foreground and
- * show/hide the snackbar that warns when the alarm volume is muted.
+ * Displays a snackbar that indicates a system setting is currently silencing alarms.
*/
- private final class AlarmVolumeChangeObserver extends ContentObserver {
- private AlarmVolumeChangeObserver() {
- super(new Handler());
+ private final class ShowSilentSettingSnackbarRunnable implements Runnable {
+
+ private final SilentSetting mSilentSetting;
+
+ private ShowSilentSettingSnackbarRunnable(SilentSetting silentSetting) {
+ mSilentSetting = silentSetting;
}
- @Override
- public void onChange(boolean selfChange) {
- if (isAlarmStreamMuted()) {
- showAlarmVolumeMutedSnackbar();
- } else {
- SnackbarManager.dismiss();
- }
- }
- }
+ public void run() {
+ // Create a snackbar with a message explaining the setting that is silencing alarms.
+ final Snackbar snackbar = createSnackbar(mSilentSetting.getLabelResId());
- /**
- * Observe changes to the do-not-disturb setting while the application is in the foreground
- * and show/hide the snackbar that warns when the setting is blocking alarms.
- */
- private final class DoNotDisturbChangeReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (isDoNotDisturbBlockingAlarms()) {
- showDoNotDisturbIsBlockingAlarmsSnackbar();
- } else {
- SnackbarManager.dismiss();
+ // Set the associated corrective action if one exists.
+ if (mSilentSetting.isActionEnabled(DeskClock.this)) {
+ final int actionResId = mSilentSetting.getActionResId();
+ snackbar.setAction(actionResId, mSilentSetting.getActionListener());
}
+
+ SnackbarManager.show(snackbar);
}
}
@@ -778,11 +646,10 @@
*/
private final class TabChangeWatcher implements TabListener {
@Override
- public void selectedTabChanged(Tab oldSelectedTab, Tab newSelectedTab) {
- final int index = newSelectedTab.ordinal();
-
+ public void selectedTabChanged(UiDataModel.Tab oldSelectedTab,
+ UiDataModel.Tab newSelectedTab) {
// Update the view pager and tab layout to agree with the model.
- updateCurrentTab(index);
+ updateCurrentTab();
// Avoid sending events for the initial tab selection on launch and re-selecting a tab
// after a configuration change.
@@ -810,47 +677,4 @@
}
}
}
-
- /**
- * This adapter produces the DeskClockFragments that are the contents of the tabs.
- */
- private static final class TabFragmentAdapter extends FragmentPagerAdapter {
-
- private final FragmentManager mFragmentManager;
- private final Context mContext;
-
- TabFragmentAdapter(AppCompatActivity activity) {
- super(activity.getFragmentManager());
- mContext = activity;
- mFragmentManager = activity.getFragmentManager();
- }
-
- @Override
- public Object instantiateItem(ViewGroup container, int position) {
- position = UiDataModel.getUiDataModel().getTabLayoutIndex(position);
- return super.instantiateItem(container, position);
- }
-
- @Override
- public DeskClockFragment getItem(int position) {
- final String tag = makeFragmentName(R.id.desk_clock_pager, position);
- Fragment fragment = mFragmentManager.findFragmentByTag(tag);
- if (fragment == null) {
- final Tab tab = UiDataModel.getUiDataModel().getTab(position);
- final String fragmentClassName = tab.getFragmentClassName();
- fragment = Fragment.instantiate(mContext, fragmentClassName);
- }
- return (DeskClockFragment) fragment;
- }
-
- @Override
- public int getCount() {
- return UiDataModel.getUiDataModel().getTabCount();
- }
-
- /** This implementation duplicated from {@link FragmentPagerAdapter#makeFragmentName}. */
- private String makeFragmentName(int viewId, long id) {
- return "android:switcher:" + viewId + ":" + id;
- }
- }
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockApplication.java b/src/com/android/deskclock/DeskClockApplication.java
index 5929032..395d385 100644
--- a/src/com/android/deskclock/DeskClockApplication.java
+++ b/src/com/android/deskclock/DeskClockApplication.java
@@ -16,12 +16,15 @@
package com.android.deskclock;
+import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.preference.PreferenceManager;
import com.android.deskclock.controller.Controller;
import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.Events;
import com.android.deskclock.events.LogEventTracker;
import com.android.deskclock.uidata.UiDataModel;
@@ -32,9 +35,31 @@
super.onCreate();
final Context applicationContext = getApplicationContext();
- DataModel.getDataModel().setContext(applicationContext);
- UiDataModel.getUiDataModel().setContext(applicationContext);
+ final SharedPreferences prefs = getDefaultSharedPreferences(applicationContext);
+
+ DataModel.getDataModel().init(applicationContext, prefs);
+ UiDataModel.getUiDataModel().init(applicationContext, prefs);
Controller.getController().setContext(applicationContext);
- Events.addEventTracker(new LogEventTracker(applicationContext));
+ Controller.getController().addEventTracker(new LogEventTracker(applicationContext));
+ }
+
+ /**
+ * Returns the default {@link SharedPreferences} instance from the underlying storage context.
+ */
+ @TargetApi(Build.VERSION_CODES.N)
+ private static SharedPreferences getDefaultSharedPreferences(Context context) {
+ final Context storageContext;
+ if (Utils.isNOrLater()) {
+ // All N devices have split storage areas. Migrate the existing preferences into the new
+ // device encrypted storage area if that has not yet occurred.
+ final String name = PreferenceManager.getDefaultSharedPreferencesName(context);
+ storageContext = context.createDeviceProtectedStorageContext();
+ if (!storageContext.moveSharedPreferencesFrom(context, name)) {
+ LogUtils.wtf("Failed to migrate shared preferences");
+ }
+ } else {
+ storageContext = context;
+ }
+ return PreferenceManager.getDefaultSharedPreferences(storageContext);
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockBackupAgent.java b/src/com/android/deskclock/DeskClockBackupAgent.java
index 96c64c8..791cb05 100644
--- a/src/com/android/deskclock/DeskClockBackupAgent.java
+++ b/src/com/android/deskclock/DeskClockBackupAgent.java
@@ -24,13 +24,12 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
-import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import com.android.deskclock.alarms.AlarmStateManager;
+import com.android.deskclock.data.DataModel;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
@@ -43,8 +42,6 @@
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DeskClockBackupAgent");
- private static final String KEY_RESTORE_FINISHED = "restore_finished";
-
public static final String ACTION_COMPLETE_RESTORE =
"com.android.deskclock.action.COMPLETE_RESTORE";
@@ -87,9 +84,8 @@
// the device-encrypted storage area
}
- // Write a preference to indicate a data restore has been completed.
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(this);
- prefs.edit().putBoolean(KEY_RESTORE_FINISHED, true).apply();
+ // Indicate a data restore has been completed.
+ DataModel.getDataModel().setRestoreBackupFinished(true);
// Create an Intent to send into DeskClock indicating restore is complete.
final PendingIntent restoreIntent = PendingIntent.getBroadcast(this, 0,
@@ -111,9 +107,8 @@
* @return {@code true} if restore data was processed; {@code false} otherwise.
*/
public static boolean processRestoredData(Context context) {
- // If the preference indicates data was not recently restored, there is nothing to do.
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
- if (!prefs.getBoolean(KEY_RESTORE_FINISHED, false)) {
+ // If data was not recently restored, there is nothing to do.
+ if (!DataModel.getDataModel().isRestoreBackupFinished()) {
return false;
}
@@ -143,7 +138,7 @@
}
// Remove the preference to avoid executing this logic multiple times.
- prefs.edit().remove(KEY_RESTORE_FINISHED).apply();
+ DataModel.getDataModel().setRestoreBackupFinished(false);
LOGGER.i("processRestoredData() completed");
return true;
diff --git a/src/com/android/deskclock/DeskClockFragment.java b/src/com/android/deskclock/DeskClockFragment.java
index 28825bf..68e2ee6 100644
--- a/src/com/android/deskclock/DeskClockFragment.java
+++ b/src/com/android/deskclock/DeskClockFragment.java
@@ -20,18 +20,20 @@
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.view.KeyEvent;
-import android.widget.ImageButton;
+import android.widget.Button;
+import android.widget.ImageView;
import com.android.deskclock.uidata.UiDataModel;
import com.android.deskclock.uidata.UiDataModel.Tab;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_IMMEDIATE;
-
public abstract class DeskClockFragment extends Fragment implements FabContainer, FabController {
/** The tab associated with this fragment. */
private final Tab mTab;
+ /** The container that houses the fab and its left and right buttons. */
+ private FabContainer mFabContainer;
+
public DeskClockFragment(Tab tab) {
mTab = tab;
}
@@ -52,12 +54,17 @@
}
@Override
- public void onLeftButtonClick(@NonNull ImageButton left) {
+ public void onLeftButtonClick(@NonNull Button left) {
// Do nothing here, only in derived classes
}
@Override
- public void onRightButtonClick(@NonNull ImageButton right) {
+ public void onRightButtonClick(@NonNull Button right) {
+ // Do nothing here, only in derived classes
+ }
+
+ @Override
+ public void onMorphFab(@NonNull ImageView fab) {
// Do nothing here, only in derived classes
}
@@ -69,22 +76,22 @@
}
/**
- * Requests that the parent activity update the fab and buttons.
- *
- * @param updateType the manner in which the fab container should be updated
+ * @param fabContainer the container that houses the fab and its left and right buttons
*/
- @Override
- public final void updateFab(FabContainer.UpdateType updateType) {
- final FabContainer parentFabContainer = (FabContainer) getActivity();
- if (parentFabContainer != null) {
- parentFabContainer.updateFab(updateType);
- }
+ public final void setFabContainer(FabContainer fabContainer) {
+ mFabContainer = fabContainer;
}
+ /**
+ * Requests that the parent activity update the fab and buttons.
+ *
+ * @param updateTypes the manner in which the fab container should be updated
+ */
@Override
- public void onMorphFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
- // Pass through to onUpdateFabButtons because there is no spec for morphing button icon.
- onUpdateFabButtons(left, right);
+ public final void updateFab(@UpdateFabFlag int updateTypes) {
+ if (mFabContainer != null) {
+ mFabContainer.updateFab(updateTypes);
+ }
}
/**
@@ -95,6 +102,13 @@
}
/**
+ * Select the tab that displays this fragment.
+ */
+ public final void selectTab() {
+ UiDataModel.getUiDataModel().setSelectedTab(mTab);
+ }
+
+ /**
* Updates the scrolling state in the {@link UiDataModel} for this tab.
*
* @param scrolledToTop {@code true} iff the vertical scroll position of this tab is at the top
diff --git a/src/com/android/deskclock/DropShadowController.java b/src/com/android/deskclock/DropShadowController.java
index 6284951..19e1b7a 100644
--- a/src/com/android/deskclock/DropShadowController.java
+++ b/src/com/android/deskclock/DropShadowController.java
@@ -48,6 +48,9 @@
/** The component that displays a drop shadow. */
private final View mDropShadowView;
+ /** Tab bar's hairline, which is hidden whenever the drop shadow is displayed. */
+ private View mHairlineView;
+
// Supported sources of scroll position include: ListView, RecyclerView and UiDataModel.
private RecyclerView mRecyclerView;
private UiDataModel mUiDataModel;
@@ -56,12 +59,15 @@
/**
* @param dropShadowView to be hidden/shown as {@code uiDataModel} reports scrolling changes
* @param uiDataModel models the vertical scrolling state of the application's selected tab
+ * @param hairlineView at the bottom of the tab bar to be hidden or shown when the drop shadow
+ * is displayed or hidden, respectively.
*/
- public DropShadowController(View dropShadowView, UiDataModel uiDataModel) {
+ public DropShadowController(View dropShadowView, UiDataModel uiDataModel, View hairlineView) {
this(dropShadowView);
mUiDataModel = uiDataModel;
mUiDataModel.addTabScrollListener(mScrollChangeWatcher);
- updateDropShadow(uiDataModel.isSelectedTabScrolledToTop());
+ mHairlineView = hairlineView;
+ updateDropShadow(!uiDataModel.isSelectedTabScrolledToTop());
}
/**
@@ -72,7 +78,7 @@
this(dropShadowView);
mListView = listView;
mListView.setOnScrollListener(mScrollChangeWatcher);
- updateDropShadow(Utils.isScrolledToTop(listView));
+ updateDropShadow(!Utils.isScrolledToTop(listView));
}
/**
@@ -83,7 +89,7 @@
this(dropShadowView);
mRecyclerView = recyclerView;
mRecyclerView.addOnScrollListener(mScrollChangeWatcher);
- updateDropShadow(Utils.isScrolledToTop(recyclerView));
+ updateDropShadow(!Utils.isScrolledToTop(recyclerView));
}
private DropShadowController(View dropShadowView) {
@@ -107,24 +113,30 @@
}
/**
- * @param scrolledToTop {@code true} indicates the drop shadow should be hidden;
- * {@code false} indicates the drop shadow should be displayed
+ * @param shouldShowDropShadow {@code true} indicates the drop shadow should be displayed;
+ * {@code false} indicates the drop shadow should be hidden
*/
- private void updateDropShadow(boolean scrolledToTop) {
- if (scrolledToTop && mDropShadowView.getAlpha() != 0f) {
+ private void updateDropShadow(boolean shouldShowDropShadow) {
+ if (!shouldShowDropShadow && mDropShadowView.getAlpha() != 0f) {
if (DataModel.getDataModel().isApplicationInForeground()) {
mDropShadowAnimator.reverse();
} else {
mDropShadowView.setAlpha(0f);
}
+ if (mHairlineView != null) {
+ mHairlineView.setVisibility(View.VISIBLE);
+ }
}
- if (!scrolledToTop && mDropShadowView.getAlpha() != 1f) {
+ if (shouldShowDropShadow && mDropShadowView.getAlpha() != 1f) {
if (DataModel.getDataModel().isApplicationInForeground()) {
mDropShadowAnimator.start();
} else {
mDropShadowView.setAlpha(1f);
}
+ if (mHairlineView != null) {
+ mHairlineView.setVisibility(View.INVISIBLE);
+ }
}
}
@@ -137,7 +149,7 @@
// RecyclerView scrolled.
@Override
public void onScrolled(RecyclerView view, int dx, int dy) {
- updateDropShadow(Utils.isScrolledToTop(view));
+ updateDropShadow(!Utils.isScrolledToTop(view));
}
// ListView scrolled.
@@ -147,12 +159,12 @@
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
- updateDropShadow(Utils.isScrolledToTop(view));
+ updateDropShadow(!Utils.isScrolledToTop(view));
}
// UiDataModel reports scroll change.
public void selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop) {
- updateDropShadow(scrolledToTop);
+ updateDropShadow(!scrolledToTop);
}
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabContainer.java b/src/com/android/deskclock/FabContainer.java
index 4e12e38..2f18061 100644
--- a/src/com/android/deskclock/FabContainer.java
+++ b/src/com/android/deskclock/FabContainer.java
@@ -1,5 +1,7 @@
package com.android.deskclock;
+import android.support.annotation.IntDef;
+
/**
* Implemented by containers that house the fab and its associated buttons. Also implemented by
* containers that know how to contact the <strong>true</strong> fab container to ferry through
@@ -7,32 +9,60 @@
*/
public interface FabContainer {
- enum UpdateType {
- /** Signals just the fab should be "animated away", updated, and "animated back". */
- FAB_ONLY_SHRINK_AND_EXPAND,
+ /** Bit field for updates */
- /** Signals the fab and buttons should be "animated away", updated, and "animated back". */
- FAB_AND_BUTTONS_SHRINK_AND_EXPAND,
+ /** Bit 0-1 */
+ int FAB_ANIMATION_MASK = 0b11;
+ /** Signals that the fab should be updated in place with no animation. */
+ int FAB_IMMEDIATE = 0b1;
+ /** Signals the fab should be "animated away", updated, and "animated back". */
+ int FAB_SHRINK_AND_EXPAND = 0b10;
+ /** Signals that the fab should morph into a new state in place. */
+ int FAB_MORPH = 0b11;
- /** Signals that the fab and buttons should be updated in place with no animation. */
- FAB_AND_BUTTONS_IMMEDIATE,
+ /** Bit 2 */
+ int FAB_REQUEST_FOCUS_MASK = 0b100;
+ /** Signals that the fab should request focus. */
+ int FAB_REQUEST_FOCUS = 0b100;
- /** Signals that the fab and buttons should morph into a new state in place. **/
- FAB_AND_BUTTONS_MORPH,
+ /** Bit 3-4 */
+ int BUTTONS_ANIMATION_MASK = 0b11000;
+ /** Signals that the buttons should be updated in place with no animation. */
+ int BUTTONS_IMMEDIATE = 0b1000;
+ /** Signals that the buttons should be "animated away", updated, and "animated back". */
+ int BUTTONS_SHRINK_AND_EXPAND = 0b10000;
- /** Disable the buttons of the fab so they do not respond to clicks. */
- DISABLE_BUTTONS,
+ /** Bit 5 */
+ int BUTTONS_DISABLE_MASK = 0b100000;
+ /** Disable the buttons of the fab so they do not respond to clicks. */
+ int BUTTONS_DISABLE = 0b100000;
- /** Signals that the fab should request focus. */
- FAB_REQUESTS_FOCUS
- }
+ /** Bit 6-7 */
+ int FAB_AND_BUTTONS_SHRINK_EXPAND_MASK = 0b11000000;
+ /** Signals the fab and buttons should be "animated away". */
+ int FAB_AND_BUTTONS_SHRINK = 0b10000000;
+ /** Signals the fab and buttons should be "animated back". */
+ int FAB_AND_BUTTONS_EXPAND = 0b01000000;
+
+ /** Convenience flags */
+ int FAB_AND_BUTTONS_IMMEDIATE = FAB_IMMEDIATE | BUTTONS_IMMEDIATE;
+ int FAB_AND_BUTTONS_SHRINK_AND_EXPAND = FAB_SHRINK_AND_EXPAND | BUTTONS_SHRINK_AND_EXPAND;
+
+ @IntDef(
+ flag = true,
+ value = { FAB_IMMEDIATE, FAB_SHRINK_AND_EXPAND, FAB_MORPH, FAB_REQUEST_FOCUS,
+ BUTTONS_IMMEDIATE, BUTTONS_SHRINK_AND_EXPAND, BUTTONS_DISABLE,
+ FAB_AND_BUTTONS_IMMEDIATE, FAB_AND_BUTTONS_SHRINK_AND_EXPAND,
+ FAB_AND_BUTTONS_SHRINK, FAB_AND_BUTTONS_EXPAND }
+ )
+ @interface UpdateFabFlag {}
/**
* Requests that this container update the fab and/or its buttons because their state has
* changed. The update may be immediate or it may be animated depending on the choice of
- * {@code updateType}.
+ * {@code updateTypes}.
*
- * @param updateType indicates the type of update to apply to the fab and its buttons
+ * @param updateTypes indicates the types of update to apply to the fab and its buttons
*/
- void updateFab(UpdateType updateType);
+ void updateFab(@UpdateFabFlag int updateTypes);
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabController.java b/src/com/android/deskclock/FabController.java
index 4735c33..13bb42b 100644
--- a/src/com/android/deskclock/FabController.java
+++ b/src/com/android/deskclock/FabController.java
@@ -2,7 +2,7 @@
import android.support.annotation.NonNull;
import android.view.View;
-import android.widget.ImageButton;
+import android.widget.Button;
import android.widget.ImageView;
/**
@@ -22,22 +22,20 @@
void onUpdateFab(@NonNull ImageView fab);
/**
+ * Called before onUpdateFab when the fab should be animated.
+ *
+ * @param fab the fab component to be configured based on current state
+ */
+ void onMorphFab(@NonNull ImageView fab);
+
+ /**
* Configures the display of the buttons to the left and right of the fab to match the current
* state of this controller.
*
* @param left button to the left of the fab to configure based on current state
* @param right button to the right of the fab to configure based on current state
*/
- void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right);
-
- /**
- * Animates the display of the buttons to the left and right of the fab to match the current
- * state of this controller.
- *
- * @param left button to the left of the fab to configure based on current state
- * @param right button to the right of the fab to configure based on current state
- */
- void onMorphFabButtons(@NonNull ImageButton left, @NonNull ImageButton right);
+ void onUpdateFabButtons(@NonNull Button left, @NonNull Button right);
/**
* Handles a click on the fab.
@@ -51,12 +49,12 @@
*
* @param left the button to the left of the fab component
*/
- void onLeftButtonClick(@NonNull ImageButton left);
+ void onLeftButtonClick(@NonNull Button left);
/**
* Handles a click on the button to the right of the fab component.
*
* @param right the button to the right of the fab component
*/
- void onRightButtonClick(@NonNull ImageButton right);
+ void onRightButtonClick(@NonNull Button right);
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FetchMatchingAlarmsAction.java b/src/com/android/deskclock/FetchMatchingAlarmsAction.java
index 9b797b4..5575348 100644
--- a/src/com/android/deskclock/FetchMatchingAlarmsAction.java
+++ b/src/com/android/deskclock/FetchMatchingAlarmsAction.java
@@ -24,6 +24,7 @@
import android.provider.AlarmClock;
import com.android.deskclock.alarms.AlarmStateManager;
+import com.android.deskclock.controller.Controller;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
@@ -57,10 +58,7 @@
@Override
public void run() {
- // only allow on background thread
- if (Looper.myLooper() == Looper.getMainLooper()) {
- throw new IllegalStateException("Must be called on a background thread");
- }
+ Utils.enforceNotMainLooper();
final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE);
// if search mode isn't specified show all alarms in the UI picker
@@ -175,6 +173,6 @@
private void notifyFailureAndLog(String reason, Activity activity) {
LogUtils.e(reason);
- Voice.notifyFailure(activity, reason);
+ Controller.getController().notifyVoiceFailure(activity, reason);
}
}
diff --git a/src/com/android/deskclock/FormattedTextUtils.java b/src/com/android/deskclock/FormattedTextUtils.java
new file mode 100644
index 0000000..80833ee
--- /dev/null
+++ b/src/com/android/deskclock/FormattedTextUtils.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.text.Spannable;
+import android.text.SpannableString;
+
+/**
+ * Utilities for formatting strings using spans.
+ */
+public class FormattedTextUtils {
+
+ private FormattedTextUtils() {
+ }
+
+ /**
+ * Applies a span over the length of the given text.
+ *
+ * @param text the {@link CharSequence} to be formatted
+ * @param span the span to apply
+ * @return the text with the span applied
+ */
+ public static CharSequence formatText(CharSequence text, Object span) {
+ if (text == null) {
+ return null;
+ }
+
+ final SpannableString formattedText = SpannableString.valueOf(text);
+ formattedText.setSpan(span, 0, formattedText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return formattedText;
+ }
+}
diff --git a/src/com/android/deskclock/FragmentTabPagerAdapter.java b/src/com/android/deskclock/FragmentTabPagerAdapter.java
new file mode 100644
index 0000000..3b154d5
--- /dev/null
+++ b/src/com/android/deskclock/FragmentTabPagerAdapter.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.support.v13.app.FragmentCompat;
+import android.support.v4.view.PagerAdapter;
+import android.util.ArrayMap;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.deskclock.uidata.UiDataModel;
+
+import java.util.Map;
+
+/**
+ * This adapter produces the DeskClockFragments that are the content of the DeskClock tabs. The
+ * adapter presents the tabs in LTR and RTL order depending on the text layout direction for the
+ * current locale. To prevent issues when switching between LTR and RTL, fragments are registered
+ * with the manager using position-independent tags, which is an important departure from
+ * FragmentPagerAdapter.
+ */
+final class FragmentTabPagerAdapter extends PagerAdapter {
+
+ private final DeskClock mDeskClock;
+
+ /** The manager into which fragments are added. */
+ private final FragmentManager mFragmentManager;
+
+ /** A fragment cache that can be accessed before {@link #instantiateItem} is called. */
+ private final Map<UiDataModel.Tab, DeskClockFragment> mFragmentCache;
+
+ /** The active fragment transaction if one exists. */
+ private FragmentTransaction mCurrentTransaction;
+
+ /** The current fragment displayed to the user. */
+ private Fragment mCurrentPrimaryItem;
+
+ FragmentTabPagerAdapter(DeskClock deskClock) {
+ mDeskClock = deskClock;
+ mFragmentCache = new ArrayMap<>(getCount());
+ mFragmentManager = deskClock.getFragmentManager();
+ }
+
+ @Override
+ public int getCount() {
+ return UiDataModel.getUiDataModel().getTabCount();
+ }
+
+ /**
+ * @param position the left-to-right index of the fragment to be returned
+ * @return the fragment displayed at the given {@code position}
+ */
+ DeskClockFragment getDeskClockFragment(int position) {
+ // Fetch the tab the UiDataModel reports for the position.
+ final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
+
+ // First check the local cache for the fragment.
+ DeskClockFragment fragment = mFragmentCache.get(tab);
+ if (fragment != null) {
+ return fragment;
+ }
+
+ // Next check the fragment manager; relevant when app is rebuilt after locale changes
+ // because this adapter will be new and mFragmentCache will be empty, but the fragment
+ // manager will retain the Fragments built on original application launch.
+ fragment = (DeskClockFragment) mFragmentManager.findFragmentByTag(tab.name());
+ if (fragment != null) {
+ fragment.setFabContainer(mDeskClock);
+ mFragmentCache.put(tab, fragment);
+ return fragment;
+ }
+
+ // Otherwise, build the fragment from scratch.
+ final String fragmentClassName = tab.getFragmentClassName();
+ fragment = (DeskClockFragment) Fragment.instantiate(mDeskClock, fragmentClassName);
+ fragment.setFabContainer(mDeskClock);
+ mFragmentCache.put(tab, fragment);
+ return fragment;
+ }
+
+ @Override
+ public void startUpdate(ViewGroup container) {
+ if (container.getId() == View.NO_ID) {
+ throw new IllegalStateException("ViewPager with adapter " + this + " has no id");
+ }
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ if (mCurrentTransaction == null) {
+ mCurrentTransaction = mFragmentManager.beginTransaction();
+ }
+
+ // Use the fragment located in the fragment manager if one exists.
+ final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
+ Fragment fragment = mFragmentManager.findFragmentByTag(tab.name());
+ if (fragment != null) {
+ mCurrentTransaction.attach(fragment);
+ } else {
+ fragment = getDeskClockFragment(position);
+ mCurrentTransaction.add(container.getId(), fragment, tab.name());
+ }
+
+ if (fragment != mCurrentPrimaryItem) {
+ FragmentCompat.setMenuVisibility(fragment, false);
+ FragmentCompat.setUserVisibleHint(fragment, false);
+ }
+
+ return fragment;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ if (mCurrentTransaction == null) {
+ mCurrentTransaction = mFragmentManager.beginTransaction();
+ }
+ final DeskClockFragment fragment = (DeskClockFragment) object;
+ fragment.setFabContainer(null);
+ mCurrentTransaction.detach(fragment);
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ final Fragment fragment = (Fragment) object;
+ if (fragment != mCurrentPrimaryItem) {
+ if (mCurrentPrimaryItem != null) {
+ FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false);
+ FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false);
+ }
+ if (fragment != null) {
+ FragmentCompat.setMenuVisibility(fragment, true);
+ FragmentCompat.setUserVisibleHint(fragment, true);
+ }
+ mCurrentPrimaryItem = fragment;
+ }
+ }
+
+ @Override
+ public void finishUpdate(ViewGroup container) {
+ if (mCurrentTransaction != null) {
+ mCurrentTransaction.commitAllowingStateLoss();
+ mCurrentTransaction = null;
+ mFragmentManager.executePendingTransactions();
+ }
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return ((Fragment) object).getView() == view;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/HandleApiCalls.java b/src/com/android/deskclock/HandleApiCalls.java
index 2256a37..a384cd1 100644
--- a/src/com/android/deskclock/HandleApiCalls.java
+++ b/src/com/android/deskclock/HandleApiCalls.java
@@ -29,12 +29,13 @@
import android.text.format.DateFormat;
import com.android.deskclock.alarms.AlarmStateManager;
+import com.android.deskclock.controller.Controller;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Timer;
+import com.android.deskclock.data.Weekdays;
import com.android.deskclock.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.provider.DaysOfWeek;
import com.android.deskclock.timer.TimerFragment;
import com.android.deskclock.timer.TimerService;
import com.android.deskclock.uidata.UiDataModel;
@@ -125,7 +126,7 @@
context.getContentResolver(), alarm.id);
if (instance == null) {
final String reason = context.getString(R.string.no_alarm_scheduled_for_this_time);
- Voice.notifyFailure(activity, reason);
+ Controller.getController().notifyVoiceFailure(activity, reason);
LOGGER.i("No alarm instance to dismiss");
return;
}
@@ -150,18 +151,17 @@
// Otherwise the alarm cannot be dismissed at this time.
final String reason = context.getString(
R.string.alarm_cant_be_dismissed_still_more_than_24_hours_away, time);
- Voice.notifyFailure(activity, reason);
+ Controller.getController().notifyVoiceFailure(activity, reason);
LOGGER.i("Can't dismiss alarm more than 24 hours in advance");
}
// Log the successful dismissal.
final String reason = context.getString(R.string.alarm_is_dismissed, time);
- Voice.notifySuccess(activity, reason);
+ Controller.getController().notifyVoiceSuccess(activity, reason);
LOGGER.i("Alarm dismissed: " + instance);
Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
}
-
private static class DismissAlarmAsync extends AsyncTask<Void, Void, Void> {
private final Context mContext;
@@ -180,7 +180,7 @@
final List<Alarm> alarms = getEnabledAlarms(mContext);
if (alarms.isEmpty()) {
final String reason = mContext.getString(R.string.no_scheduled_alarms);
- Voice.notifyFailure(mActivity, reason);
+ Controller.getController().notifyVoiceFailure(mActivity, reason);
LOGGER.i("No scheduled alarms");
return null;
}
@@ -203,7 +203,8 @@
.putExtra(EXTRA_ACTION, ACTION_DISMISS)
.putExtra(EXTRA_ALARMS, alarms.toArray(new Parcelable[alarms.size()]));
mContext.startActivity(pickSelectionIntent);
- Voice.notifySuccess(mActivity, mContext.getString(R.string.pick_alarm_to_dismiss));
+ final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
+ Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
return null;
}
@@ -222,7 +223,8 @@
.putExtra(EXTRA_ALARMS,
matchingAlarms.toArray(new Parcelable[matchingAlarms.size()]));
mContext.startActivity(pickSelectionIntent);
- Voice.notifySuccess(mActivity, mContext.getString(R.string.pick_alarm_to_dismiss));
+ final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
+ Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
return null;
}
@@ -264,7 +266,7 @@
cr, FIRED_STATE);
if (alarmInstances.isEmpty()) {
final String reason = mContext.getString(R.string.no_firing_alarms);
- Voice.notifyFailure(mActivity, reason);
+ Controller.getController().notifyVoiceFailure(mActivity, reason);
LOGGER.i("No firing alarms");
return null;
}
@@ -284,7 +286,7 @@
final String reason = context.getString(R.string.alarm_is_snoozed, time);
AlarmStateManager.setSnoozeState(context, alarmInstance, true);
- Voice.notifySuccess(activity, reason);
+ Controller.getController().notifyVoiceSuccess(activity, reason);
LOGGER.i("Alarm snoozed: " + alarmInstance);
Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
}
@@ -300,7 +302,8 @@
hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, hour);
if (hour < 0 || hour > 23) {
final int mins = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
- Voice.notifyFailure(this, getString(R.string.invalid_time, hour, mins, " "));
+ final String voiceMessage = getString(R.string.invalid_time, hour, mins, " ");
+ Controller.getController().notifyVoiceFailure(this, voiceMessage);
LOGGER.i("Illegal hour: " + hour);
return;
}
@@ -309,7 +312,8 @@
// Validate the minute, if one was given.
final int minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
if (minutes < 0 || minutes > 59) {
- Voice.notifyFailure(this, getString(R.string.invalid_time, hour, minutes, " "));
+ final String voiceMessage = getString(R.string.invalid_time, hour, minutes, " ");
+ Controller.getController().notifyVoiceFailure(this, voiceMessage);
LOGGER.i("Illegal minute: " + minutes);
return;
}
@@ -330,7 +334,8 @@
// Open DeskClock which is now positioned on the alarms tab.
startActivity(createAlarm);
- Voice.notifyFailure(this, getString(R.string.invalid_time, hour, minutes, " "));
+ final String voiceMessage = getString(R.string.invalid_time, hour, minutes, " ");
+ Controller.getController().notifyVoiceFailure(this, voiceMessage);
LOGGER.i("Missing alarm time; opening UI");
return;
}
@@ -369,12 +374,13 @@
}
// Schedule the next instance.
- final AlarmInstance alarmInstance = alarm.createInstanceAfter(Calendar.getInstance());
+ final Calendar now = DataModel.getDataModel().getCalendar();
+ final AlarmInstance alarmInstance = alarm.createInstanceAfter(now);
setupInstance(alarmInstance, skipUi);
final String time = DateFormat.getTimeFormat(this)
.format(alarmInstance.getAlarmTime().getTime());
- Voice.notifySuccess(this, getString(R.string.alarm_is_set, time));
+ Controller.getController().notifyVoiceSuccess(this, getString(R.string.alarm_is_set, time));
}
private void handleShowAlarms(Intent intent) {
@@ -415,8 +421,9 @@
// Verify that the timer length is between one second and one day.
final long lengthMillis = SECOND_IN_MILLIS * intent.getIntExtra(AlarmClock.EXTRA_LENGTH, 0);
- if (lengthMillis < Timer.MIN_LENGTH || lengthMillis > Timer.MAX_LENGTH) {
- Voice.notifyFailure(this, getString(R.string.invalid_timer_length));
+ if (lengthMillis < Timer.MIN_LENGTH) {
+ final String voiceMessage = getString(R.string.invalid_timer_length);
+ Controller.getController().notifyVoiceFailure(this, voiceMessage);
LOGGER.i("Invalid timer length requested: " + lengthMillis);
return;
}
@@ -444,7 +451,7 @@
// Start the selected timer.
DataModel.getDataModel().startTimer(timer);
Events.sendTimerEvent(R.string.action_start, R.string.label_intent);
- Voice.notifySuccess(this, getString(R.string.timer_created));
+ Controller.getController().notifyVoiceSuccess(this, getString(R.string.timer_created));
// If not instructed to skip the UI, display the running timer.
if (!skipUi) {
@@ -484,7 +491,7 @@
alarm.vibrate = intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, alarm.vibrate);
alarm.alert = getAlertFromIntent(intent, alarm.alert);
alarm.label = getLabelFromIntent(intent, alarm.label);
- alarm.daysOfWeek = getDaysFromIntent(intent, alarm.daysOfWeek.getBitSet());
+ alarm.daysOfWeek = getDaysFromIntent(intent, alarm.daysOfWeek);
}
private static String getLabelFromIntent(Intent intent, String defaultLabel) {
@@ -492,27 +499,26 @@
return message == null ? "" : message;
}
- private static DaysOfWeek getDaysFromIntent(Intent intent, int defaultDaysBitset) {
+ private static Weekdays getDaysFromIntent(Intent intent, Weekdays defaultWeekdays) {
if (!intent.hasExtra(AlarmClock.EXTRA_DAYS)) {
- return new DaysOfWeek(defaultDaysBitset);
+ return defaultWeekdays;
}
- final DaysOfWeek daysOfWeek = new DaysOfWeek(0);
- final ArrayList<Integer> days = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS);
+ final List<Integer> days = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS);
if (days != null) {
final int[] daysArray = new int[days.size()];
for (int i = 0; i < days.size(); i++) {
daysArray[i] = days.get(i);
}
- daysOfWeek.setDaysOfWeek(true, daysArray);
+ return Weekdays.fromCalendarDays(daysArray);
} else {
// API says to use an ArrayList<Integer> but we allow the user to use a int[] too.
final int[] daysArray = intent.getIntArrayExtra(AlarmClock.EXTRA_DAYS);
if (daysArray != null) {
- daysOfWeek.setDaysOfWeek(true, daysArray);
+ return Weekdays.fromCalendarDays(daysArray);
}
}
- return daysOfWeek;
+ return defaultWeekdays;
}
private static Uri getAlertFromIntent(Intent intent, Uri defaultUri) {
@@ -563,7 +569,7 @@
// Days is treated differently than other fields because if days is not specified, it
// explicitly means "not recurring".
selection.append(" AND ").append(Alarm.DAYS_OF_WEEK).append("=?");
- args.add(String.valueOf(getDaysFromIntent(intent, DaysOfWeek.NO_DAYS_SET).getBitSet()));
+ args.add(String.valueOf(getDaysFromIntent(intent, Weekdays.NONE).getBits()));
if (intent.hasExtra(AlarmClock.EXTRA_VIBRATE)) {
selection.append(" AND ").append(Alarm.VIBRATE).append("=?");
diff --git a/src/com/android/deskclock/HandleShortcuts.java b/src/com/android/deskclock/HandleShortcuts.java
index 00094d8..68bc42e 100644
--- a/src/com/android/deskclock/HandleShortcuts.java
+++ b/src/com/android/deskclock/HandleShortcuts.java
@@ -20,7 +20,6 @@
import android.content.Intent;
import android.os.Bundle;
-import com.android.deskclock.data.DataModel;
import com.android.deskclock.events.Events;
import com.android.deskclock.stopwatch.StopwatchService;
import com.android.deskclock.uidata.UiDataModel;
@@ -42,20 +41,20 @@
switch (action) {
case StopwatchService.ACTION_PAUSE_STOPWATCH:
Events.sendStopwatchEvent(R.string.action_pause, R.string.label_shortcut);
- DataModel.getDataModel().pauseStopwatch();
// Open DeskClock positioned on the stopwatch tab.
UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
- startActivity(new Intent(this, DeskClock.class));
+ startActivity(new Intent(this, DeskClock.class)
+ .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH));
setResult(RESULT_OK);
break;
case StopwatchService.ACTION_START_STOPWATCH:
Events.sendStopwatchEvent(R.string.action_start, R.string.label_shortcut);
- DataModel.getDataModel().startStopwatch();
// Open DeskClock positioned on the stopwatch tab.
UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
- startActivity(new Intent(this, DeskClock.class));
+ startActivity(new Intent(this, DeskClock.class)
+ .setAction(StopwatchService.ACTION_START_STOPWATCH));
setResult(RESULT_OK);
break;
default:
diff --git a/src/com/android/deskclock/ItemAdapter.java b/src/com/android/deskclock/ItemAdapter.java
index 084e836..e1cd4d3 100644
--- a/src/com/android/deskclock/ItemAdapter.java
+++ b/src/com/android/deskclock/ItemAdapter.java
@@ -17,6 +17,7 @@
package com.android.deskclock;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.View;
@@ -25,6 +26,8 @@
import java.util.ArrayList;
import java.util.List;
+import static android.support.v7.widget.RecyclerView.NO_ID;
+
/**
* Base adapter class for displaying a collection of items. Provides functionality for handling
* changing items, persistent item state, item click events, and re-usable item views.
@@ -33,7 +36,9 @@
extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
/**
- * Finds the position of the changed item holder and invokes {@link #notifyItemChanged(int)}.
+ * Finds the position of the changed item holder and invokes {@link #notifyItemChanged(int)} or
+ * {@link #notifyItemChanged(int, Object)} if payloads are present (in order to do in-place
+ * change animations).
*/
private final OnItemChangedListener mItemChangedNotifier = new OnItemChangedListener() {
@Override
@@ -46,6 +51,17 @@
notifyItemChanged(position);
}
}
+
+ @Override
+ public void onItemChanged(ItemHolder<?> itemHolder, Object payload) {
+ if (mOnItemChangedListener != null) {
+ mOnItemChangedListener.onItemChanged(itemHolder, payload);
+ }
+ final int position = mItemHolders.indexOf(itemHolder);
+ if (position != RecyclerView.NO_POSITION) {
+ notifyItemChanged(position, payload);
+ }
+ }
};
/**
@@ -177,6 +193,40 @@
}
/**
+ * Inserts the specified item holder at the specified position. Invokes
+ * {@link #notifyItemInserted} to update the UI.
+ *
+ * @param position the index to which to add the item holder
+ * @param itemHolder the item holder to add
+ * @return this object, allowing calls to methods in this class to be chained
+ */
+ public ItemAdapter addItem(int position, @NonNull T itemHolder) {
+ itemHolder.addOnItemChangedListener(mItemChangedNotifier);
+ position = Math.min(position, mItemHolders.size());
+ mItemHolders.add(position, itemHolder);
+ notifyItemInserted(position);
+ return this;
+ }
+
+ /**
+ * Removes the first occurrence of the specified element from this list, if it is present
+ * (optional operation). If this list does not contain the element, it is unchanged. Invokes
+ * {@link #notifyItemRemoved} to update the UI.
+ *
+ * @param itemHolder the item holder to remove
+ * @return this object, allowing calls to methods in this class to be chained
+ */
+ public ItemAdapter removeItem(@NonNull T itemHolder) {
+ final int index = mItemHolders.indexOf(itemHolder);
+ if (index >= 0) {
+ itemHolder = mItemHolders.remove(index);
+ itemHolder.removeOnItemChangedListener(mItemChangedNotifier);
+ notifyItemRemoved(index);
+ }
+ return this;
+ }
+
+ /**
* Sets the listener to be invoked whenever any item changes.
*/
public void setOnItemChangedListener(OnItemChangedListener listener) {
@@ -190,7 +240,7 @@
@Override
public long getItemId(int position) {
- return mItemHolders.get(position).itemId;
+ return hasStableIds() ? mItemHolders.get(position).itemId : NO_ID;
}
public T findItemById(long id) {
@@ -312,6 +362,16 @@
}
/**
+ * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder, Object)} for all
+ * listeners added via {@link #addOnItemChangedListener(OnItemChangedListener)}.
+ */
+ public final void notifyItemChanged(Object payload) {
+ for (OnItemChangedListener listener : mOnItemChangedListeners) {
+ listener.onItemChanged(this, payload);
+ }
+ }
+
+ /**
* Called to retrieve per-instance state when the item may disappear or change so that
* state can be restored in {@link #onRestoreInstanceState(Bundle)}.
* <p/>
@@ -457,7 +517,16 @@
*
* @param itemHolder the item holder that has changed
*/
- public void onItemChanged(ItemHolder<?> itemHolder);
+ void onItemChanged(ItemHolder<?> itemHolder);
+
+
+ /**
+ * Invoked by {@link ItemHolder#notifyItemChanged(Object payload)}.
+ *
+ * @param itemHolder the item holder that has changed
+ * @param payload the payload object
+ */
+ void onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload);
}
/**
@@ -470,6 +539,6 @@
* @param viewHolder the {@link ItemViewHolder} containing the view that was clicked
* @param id the unique identifier for the click action that has occurred
*/
- public void onItemClicked(ItemViewHolder<?> viewHolder, int id);
+ void onItemClicked(ItemViewHolder<?> viewHolder, int id);
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAnimator.java b/src/com/android/deskclock/ItemAnimator.java
new file mode 100644
index 0000000..3b65d56
--- /dev/null
+++ b/src/com/android/deskclock/ItemAnimator.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.support.annotation.NonNull;
+import android.support.v4.util.ArrayMap;
+import android.support.v7.widget.RecyclerView.State;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.support.v7.widget.SimpleItemAnimator;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static android.view.View.TRANSLATION_Y;
+import static android.view.View.TRANSLATION_X;
+
+public class ItemAnimator extends SimpleItemAnimator {
+
+ private final List<Animator> mAddAnimatorsList = new ArrayList<>();
+ private final List<Animator> mRemoveAnimatorsList = new ArrayList<>();
+ private final List<Animator> mChangeAnimatorsList = new ArrayList<>();
+ private final List<Animator> mMoveAnimatorsList = new ArrayList<>();
+
+ private final Map<ViewHolder, Animator> mAnimators = new ArrayMap<>();
+
+ @Override
+ public boolean animateRemove(final ViewHolder holder) {
+ endAnimation(holder);
+
+ final float prevAlpha = holder.itemView.getAlpha();
+
+ final Animator removeAnimator = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 0f);
+ removeAnimator.setDuration(getRemoveDuration());
+ removeAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchRemoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animator.removeAllListeners();
+ mAnimators.remove(holder);
+ holder.itemView.setAlpha(prevAlpha);
+ dispatchRemoveFinished(holder);
+ }
+ });
+ mRemoveAnimatorsList.add(removeAnimator);
+ mAnimators.put(holder, removeAnimator);
+ return true;
+ }
+
+ @Override
+ public boolean animateAdd(final ViewHolder holder) {
+ endAnimation(holder);
+
+ final float prevAlpha = holder.itemView.getAlpha();
+ holder.itemView.setAlpha(0f);
+
+ final Animator addAnimator = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 1f)
+ .setDuration(getAddDuration());
+ addAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchAddStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animator.removeAllListeners();
+ mAnimators.remove(holder);
+ holder.itemView.setAlpha(prevAlpha);
+ dispatchAddFinished(holder);
+ }
+ });
+ mAddAnimatorsList.add(addAnimator);
+ mAnimators.put(holder, addAnimator);
+ return true;
+ }
+
+ @Override
+ public boolean animateMove(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ endAnimation(holder);
+
+ final int deltaX = toX - fromX;
+ final int deltaY = toY - fromY;
+ final long moveDuration = getMoveDuration();
+
+ if (deltaX == 0 && deltaY == 0) {
+ dispatchMoveFinished(holder);
+ return false;
+ }
+
+ final View view = holder.itemView;
+ final float prevTranslationX = view.getTranslationX();
+ final float prevTranslationY = view.getTranslationY();
+ view.setTranslationX(-deltaX);
+ view.setTranslationY(-deltaY);
+
+ final ObjectAnimator moveAnimator;
+ if (deltaX != 0 && deltaY != 0) {
+ final PropertyValuesHolder moveX = PropertyValuesHolder.ofFloat(TRANSLATION_X, 0f);
+ final PropertyValuesHolder moveY = PropertyValuesHolder.ofFloat(TRANSLATION_Y, 0f);
+ moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX, moveY);
+ } else if (deltaX != 0) {
+ final PropertyValuesHolder moveX = PropertyValuesHolder.ofFloat(TRANSLATION_X, 0f);
+ moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX);
+ } else {
+ final PropertyValuesHolder moveY = PropertyValuesHolder.ofFloat(TRANSLATION_Y, 0f);
+ moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveY);
+ }
+
+ moveAnimator.setDuration(moveDuration);
+ moveAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+ moveAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchMoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animator.removeAllListeners();
+ mAnimators.remove(holder);
+ view.setTranslationX(prevTranslationX);
+ view.setTranslationY(prevTranslationY);
+ dispatchMoveFinished(holder);
+ }
+ });
+ mMoveAnimatorsList.add(moveAnimator);
+ mAnimators.put(holder, moveAnimator);
+
+ return true;
+ }
+
+ @Override
+ public boolean animateChange(@NonNull final ViewHolder oldHolder,
+ @NonNull final ViewHolder newHolder, @NonNull ItemHolderInfo preInfo,
+ @NonNull ItemHolderInfo postInfo) {
+ endAnimation(oldHolder);
+ endAnimation(newHolder);
+
+ final long changeDuration = getChangeDuration();
+ List<Object> payloads = preInfo instanceof PayloadItemHolderInfo
+ ? ((PayloadItemHolderInfo) preInfo).getPayloads() : null;
+
+ if (oldHolder == newHolder) {
+ final Animator animator = ((OnAnimateChangeListener) newHolder)
+ .onAnimateChange(payloads, preInfo.left, preInfo.top, preInfo.right,
+ preInfo.bottom, changeDuration);
+ if (animator == null) {
+ dispatchChangeFinished(newHolder, false);
+ return false;
+ }
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(newHolder, false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animator.removeAllListeners();
+ mAnimators.remove(newHolder);
+ dispatchChangeFinished(newHolder, false);
+ }
+ });
+ mChangeAnimatorsList.add(animator);
+ mAnimators.put(newHolder, animator);
+ return true;
+ } else if (!(oldHolder instanceof OnAnimateChangeListener) ||
+ !(newHolder instanceof OnAnimateChangeListener)) {
+ // Both holders must implement OnAnimateChangeListener in order to animate.
+ dispatchChangeFinished(oldHolder, true);
+ dispatchChangeFinished(newHolder, true);
+ return false;
+ }
+
+ final Animator oldChangeAnimator = ((OnAnimateChangeListener) oldHolder)
+ .onAnimateChange(oldHolder, newHolder, changeDuration);
+ if (oldChangeAnimator != null) {
+ oldChangeAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(oldHolder, true);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animator.removeAllListeners();
+ mAnimators.remove(oldHolder);
+ dispatchChangeFinished(oldHolder, true);
+ }
+ });
+ mAnimators.put(oldHolder, oldChangeAnimator);
+ mChangeAnimatorsList.add(oldChangeAnimator);
+ } else {
+ dispatchChangeFinished(oldHolder, true);
+ }
+
+ final Animator newChangeAnimator = ((OnAnimateChangeListener) newHolder)
+ .onAnimateChange(oldHolder, newHolder, changeDuration);
+ if (newChangeAnimator != null) {
+ newChangeAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(newHolder, false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animator.removeAllListeners();
+ mAnimators.remove(newHolder);
+ dispatchChangeFinished(newHolder, false);
+ }
+ });
+ mAnimators.put(newHolder, newChangeAnimator);
+ mChangeAnimatorsList.add(newChangeAnimator);
+ } else {
+ dispatchChangeFinished(newHolder, false);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean animateChange(ViewHolder oldHolder,
+ ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
+ /* Unused */
+ throw new IllegalStateException("This method should not be used");
+ }
+
+ @Override
+ public void runPendingAnimations() {
+ final AnimatorSet removeAnimatorSet = new AnimatorSet();
+ removeAnimatorSet.playTogether(mRemoveAnimatorsList);
+ mRemoveAnimatorsList.clear();
+
+ final AnimatorSet addAnimatorSet = new AnimatorSet();
+ addAnimatorSet.playTogether(mAddAnimatorsList);
+ mAddAnimatorsList.clear();
+
+ final AnimatorSet changeAnimatorSet = new AnimatorSet();
+ changeAnimatorSet.playTogether(mChangeAnimatorsList);
+ mChangeAnimatorsList.clear();
+
+ final AnimatorSet moveAnimatorSet = new AnimatorSet();
+ moveAnimatorSet.playTogether(mMoveAnimatorsList);
+ mMoveAnimatorsList.clear();
+
+ final AnimatorSet pendingAnimatorSet = new AnimatorSet();
+ pendingAnimatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animator.removeAllListeners();
+ dispatchFinishedWhenDone();
+ }
+ });
+ // Required order: removes, then changes & moves simultaneously, then additions. There are
+ // redundant edges because changes or moves may be empty, causing the removes to incorrectly
+ // play immediately.
+ pendingAnimatorSet.play(removeAnimatorSet).before(changeAnimatorSet);
+ pendingAnimatorSet.play(removeAnimatorSet).before(moveAnimatorSet);
+ pendingAnimatorSet.play(changeAnimatorSet).with(moveAnimatorSet);
+ pendingAnimatorSet.play(addAnimatorSet).after(changeAnimatorSet);
+ pendingAnimatorSet.play(addAnimatorSet).after(moveAnimatorSet);
+ pendingAnimatorSet.start();
+ }
+
+ @Override
+ public void endAnimation(ViewHolder holder) {
+ final Animator animator = mAnimators.get(holder);
+
+ mAnimators.remove(holder);
+ mAddAnimatorsList.remove(animator);
+ mRemoveAnimatorsList.remove(animator);
+ mChangeAnimatorsList.remove(animator);
+ mMoveAnimatorsList.remove(animator);
+
+ if (animator != null) {
+ animator.end();
+ }
+
+ dispatchFinishedWhenDone();
+ }
+
+ @Override
+ public void endAnimations() {
+ final List<Animator> animatorList = new ArrayList<>(mAnimators.values());
+ for (Animator animator : animatorList) {
+ animator.end();
+ }
+ dispatchFinishedWhenDone();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return !mAnimators.isEmpty();
+ }
+
+ private void dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished();
+ }
+ }
+
+ @Override
+ public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
+ @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
+ @NonNull List<Object> payloads) {
+ final ItemHolderInfo itemHolderInfo = super.recordPreLayoutInformation(state, viewHolder,
+ changeFlags, payloads);
+ if (itemHolderInfo instanceof PayloadItemHolderInfo) {
+ ((PayloadItemHolderInfo) itemHolderInfo).setPayloads(payloads);
+ }
+ return itemHolderInfo;
+ }
+
+ @Override
+ public ItemHolderInfo obtainHolderInfo() {
+ return new PayloadItemHolderInfo();
+ }
+
+ @Override
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+ @NonNull List<Object> payloads) {
+ final boolean defaultReusePolicy = super.canReuseUpdatedViewHolder(viewHolder, payloads);
+ // Whenever we have a payload, this is an in-place animation.
+ return !payloads.isEmpty() || defaultReusePolicy;
+ }
+
+ private static final class PayloadItemHolderInfo extends ItemHolderInfo {
+ private final List<Object> mPayloads = new ArrayList<>();
+
+ void setPayloads(List<Object> payloads) {
+ mPayloads.clear();
+ mPayloads.addAll(payloads);
+ }
+
+ List<Object> getPayloads() {
+ return mPayloads;
+ }
+ }
+
+ public interface OnAnimateChangeListener {
+ Animator onAnimateChange(ViewHolder oldHolder, ViewHolder newHolder, long duration);
+ Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
+ int fromBottom, long duration);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/LabelDialogFragment.java b/src/com/android/deskclock/LabelDialogFragment.java
index d301b10..63a1ee4 100644
--- a/src/com/android/deskclock/LabelDialogFragment.java
+++ b/src/com/android/deskclock/LabelDialogFragment.java
@@ -18,9 +18,13 @@
import android.app.Dialog;
import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
+import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
@@ -30,6 +34,7 @@
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
+import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
@@ -37,8 +42,6 @@
import com.android.deskclock.data.Timer;
import com.android.deskclock.provider.Alarm;
-import static android.graphics.Color.RED;
-import static android.graphics.Color.WHITE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
/**
@@ -46,10 +49,15 @@
*/
public class LabelDialogFragment extends DialogFragment {
- private static final String KEY_LABEL = "label";
- private static final String KEY_ALARM = "alarm";
- private static final String KEY_TIMER_ID = "timer_id";
- private static final String KEY_TAG = "tag";
+ /**
+ * The tag that identifies instances of LabelDialogFragment in the fragment manager.
+ */
+ private static final String TAG = "label_dialog";
+
+ private static final String ARG_LABEL = "arg_label";
+ private static final String ARG_ALARM = "arg_alarm";
+ private static final String ARG_TIMER_ID = "arg_timer_id";
+ private static final String ARG_TAG = "arg_tag";
private AppCompatEditText mLabelBox;
private Alarm mAlarm;
@@ -58,9 +66,9 @@
public static LabelDialogFragment newInstance(Alarm alarm, String label, String tag) {
final Bundle args = new Bundle();
- args.putString(KEY_LABEL, label);
- args.putParcelable(KEY_ALARM, alarm);
- args.putString(KEY_TAG, tag);
+ args.putString(ARG_LABEL, label);
+ args.putParcelable(ARG_ALARM, alarm);
+ args.putString(ARG_TAG, tag);
final LabelDialogFragment frag = new LabelDialogFragment();
frag.setArguments(args);
@@ -69,53 +77,100 @@
public static LabelDialogFragment newInstance(Timer timer) {
final Bundle args = new Bundle();
- args.putString(KEY_LABEL, timer.getLabel());
- args.putInt(KEY_TIMER_ID, timer.getId());
+ args.putString(ARG_LABEL, timer.getLabel());
+ args.putInt(ARG_TIMER_ID, timer.getId());
final LabelDialogFragment frag = new LabelDialogFragment();
frag.setArguments(args);
return frag;
}
+ /**
+ * Replaces any existing LabelDialogFragment with the given {@code fragment}.
+ */
+ public static void show(FragmentManager manager, LabelDialogFragment fragment) {
+ if (manager == null || manager.isDestroyed()) {
+ return;
+ }
+
+ // Finish any outstanding fragment work.
+ manager.executePendingTransactions();
+
+ final FragmentTransaction tx = manager.beginTransaction();
+
+ // Remove existing instance of LabelDialogFragment if necessary.
+ final Fragment existing = manager.findFragmentByTag(TAG);
+ if (existing != null) {
+ tx.remove(existing);
+ }
+ tx.addToBackStack(null);
+
+ fragment.show(tx, TAG);
+ }
+
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
// As long as the label box exists, save its state.
if (mLabelBox != null) {
- outState.putString(KEY_LABEL, mLabelBox.getText().toString());
+ outState.putString(ARG_LABEL, mLabelBox.getText().toString());
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- final Bundle bundle = getArguments();
- mAlarm = bundle.getParcelable(KEY_ALARM);
- mTimerId = bundle.getInt(KEY_TIMER_ID, -1);
- mTag = bundle.getString(KEY_TAG);
+ final Bundle args = getArguments() == null ? Bundle.EMPTY : getArguments();
+ mAlarm = args.getParcelable(ARG_ALARM);
+ mTimerId = args.getInt(ARG_TIMER_ID, -1);
+ mTag = args.getString(ARG_TAG);
- final String label = savedInstanceState != null ?
- savedInstanceState.getString(KEY_LABEL) : bundle.getString(KEY_LABEL);
+ String label = args.getString(ARG_LABEL);
+ if (savedInstanceState != null) {
+ label = savedInstanceState.getString(ARG_LABEL, label);
+ }
- final Context context = getActivity();
+ final AlertDialog dialog = new AlertDialog.Builder(getActivity())
+ .setPositiveButton(android.R.string.ok, new OkListener())
+ .setNegativeButton(android.R.string.cancel, null /* listener */)
+ .setMessage(R.string.label)
+ .create();
+ final Context context = dialog.getContext();
+
+ final int colorControlActivated =
+ ThemeUtils.resolveColor(context, R.attr.colorControlActivated);
+ final int colorControlNormal =
+ ThemeUtils.resolveColor(context, R.attr.colorControlNormal);
mLabelBox = new AppCompatEditText(context);
+ mLabelBox.setSupportBackgroundTintList(new ColorStateList(
+ new int[][] { { android.R.attr.state_activated }, {} },
+ new int[] { colorControlActivated, colorControlNormal }));
mLabelBox.setOnEditorActionListener(new ImeDoneListener());
- mLabelBox.addTextChangedListener(new TextChangeListener(context));
+ mLabelBox.addTextChangedListener(new TextChangeListener());
mLabelBox.setSingleLine();
mLabelBox.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
mLabelBox.setText(label);
mLabelBox.selectAll();
- final int padding = getResources().getDimensionPixelSize(R.dimen.label_edittext_padding);
- final AlertDialog alertDialog = new AlertDialog.Builder(context)
- .setView(mLabelBox, padding, 0, padding, 0)
- .setPositiveButton(R.string.time_picker_set, new OkListener())
- .setNegativeButton(R.string.time_picker_cancel, new CancelListener())
- .setMessage(R.string.label)
- .create();
+ // The line at the bottom of EditText is part of its background therefore the padding
+ // must be added to its container.
+ final int padding = context.getResources()
+ .getDimensionPixelSize(R.dimen.label_edittext_padding);
+ dialog.setView(mLabelBox, padding, 0, padding, 0);
- alertDialog.getWindow().setSoftInputMode(SOFT_INPUT_STATE_VISIBLE);
- return alertDialog;
+ final Window alertDialogWindow = dialog.getWindow();
+ if (alertDialogWindow != null) {
+ alertDialogWindow.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE);
+ }
+ return dialog;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+
+ // Stop callbacks from the IME since there is no view to process them.
+ mLabelBox.setOnEditorActionListener(null);
}
/**
@@ -138,7 +193,7 @@
}
}
- interface AlarmLabelDialogHandler {
+ public interface AlarmLabelDialogHandler {
void onDialogLabelSet(Alarm alarm, String label, String tag);
}
@@ -146,26 +201,18 @@
* Alters the UI to indicate when input is valid or invalid.
*/
private class TextChangeListener implements TextWatcher {
-
- private final int colorAccent;
- private final int colorControlNormal;
-
- public TextChangeListener(Context context) {
- colorAccent = Utils.obtainStyledColor(context, R.attr.colorAccent, RED);
- colorControlNormal = Utils.obtainStyledColor(context, R.attr.colorControlNormal, WHITE);
- }
-
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
- final int color = TextUtils.isEmpty(s) ? colorControlNormal : colorAccent;
- mLabelBox.setSupportBackgroundTintList(ColorStateList.valueOf(color));
+ mLabelBox.setActivated(!TextUtils.isEmpty(s));
}
@Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
@Override
- public void afterTextChanged(Editable editable) {}
+ public void afterTextChanged(Editable editable) {
+ }
}
/**
@@ -176,7 +223,7 @@
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
setLabel();
- dismiss();
+ dismissAllowingStateLoss();
return true;
}
return false;
@@ -193,14 +240,4 @@
dismiss();
}
}
-
- /**
- * Handles discarding the label edit from the Cancel button of the dialog.
- */
- private class CancelListener implements DialogInterface.OnClickListener {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- dismiss();
- }
- }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/deskclock/NumberPickerCompat.java b/src/com/android/deskclock/NumberPickerCompat.java
deleted file mode 100644
index 2fb5bed..0000000
--- a/src/com/android/deskclock/NumberPickerCompat.java
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * 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;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.util.AttributeSet;
-import android.widget.NumberPicker;
-
-import java.lang.reflect.Field;
-
-/**
- * Subclass of NumberPicker that allows customizing divider color and saves/restores its value
- * across device rotations.
- */
-public class NumberPickerCompat extends NumberPicker implements NumberPicker.OnValueChangeListener {
-
- private static Field sSelectionDivider;
- private static boolean sTrySelectionDivider = true;
-
- private final Runnable mAnnounceValueRunnable = new Runnable() {
- @Override
- public void run() {
- if (mOnAnnounceValueChangedListener != null) {
- final int value = getValue();
- final String[] displayedValues = getDisplayedValues();
- final String displayedValue =
- displayedValues == null ? null : displayedValues[value];
- mOnAnnounceValueChangedListener.onAnnounceValueChanged(
- NumberPickerCompat.this, value, displayedValue);
- }
- }
- };
- private OnValueChangeListener mOnValueChangedListener;
- private OnAnnounceValueChangedListener mOnAnnounceValueChangedListener;
-
- public NumberPickerCompat(Context context) {
- this(context, null /* attrs */);
- }
-
- public NumberPickerCompat(Context context, AttributeSet attrs) {
- super(context, attrs);
- tintSelectionDivider(context);
- super.setOnValueChangedListener(this);
- }
-
- public NumberPickerCompat(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- tintSelectionDivider(context);
- super.setOnValueChangedListener(this);
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private void tintSelectionDivider(Context context) {
- // Accent color in KK will stay system blue, so leave divider color matching.
- // The divider is correctly tinted to controlColorNormal in M.
-
- if (Utils.isLOrLMR1() && sTrySelectionDivider) {
- final TypedArray a = context.obtainStyledAttributes(
- new int[] { android.R.attr.colorControlNormal });
- // White is default color if colorControlNormal is not defined.
- final int color = a.getColor(0, Color.WHITE);
- a.recycle();
-
- try {
- if (sSelectionDivider == null) {
- sSelectionDivider = NumberPicker.class.getDeclaredField("mSelectionDivider");
- sSelectionDivider.setAccessible(true);
- }
- final Drawable selectionDivider = (Drawable) sSelectionDivider.get(this);
- if (selectionDivider != null) {
- // setTint is API21+, but this will only be called in API21
- selectionDivider.setTint(color);
- }
- } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
- LogUtils.e("Unable to set selection divider", e);
- sTrySelectionDivider = false;
- }
- }
- }
-
- /**
- * @return the state of this NumberPicker including the currently selected value
- */
- @Override
- protected Parcelable onSaveInstanceState() {
- return new State(super.onSaveInstanceState(), getValue());
- }
-
- /**
- * @param state the state of this NumberPicker including the value to select
- */
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- final State instanceState = (State) state;
- super.onRestoreInstanceState(instanceState.getSuperState());
- setValue(instanceState.mValue);
- }
-
- @Override
- public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
- mOnValueChangedListener = onValueChangedListener;
- }
-
- @Override
- public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
- if (mOnValueChangedListener != null) {
- mOnValueChangedListener.onValueChange(picker, oldVal, newVal);
- }
-
- // Wait till we reach a value to prevent TalkBack from announcing every intermediate value
- // when scrolling fast.
- removeCallbacks(mAnnounceValueRunnable);
- postDelayed(mAnnounceValueRunnable, 200L);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- removeCallbacks(mAnnounceValueRunnable);
- }
-
- /**
- * Register a callback to be invoked whenever a value change should be announced.
- */
- public void setOnAnnounceValueChangedListener(OnAnnounceValueChangedListener listener) {
- mOnAnnounceValueChangedListener = listener;
- }
-
- /**
- * The state of this NumberPicker including the selected value. Used to preserve values across
- * device rotation.
- */
- private static final class State extends BaseSavedState {
-
- private final int mValue;
-
- public State(Parcel source) {
- super(source);
- mValue = source.readInt();
- }
-
- public State(Parcelable superState, int value) {
- super(superState);
- mValue = value;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- dest.writeInt(mValue);
- }
-
- public static final Parcelable.Creator<State> CREATOR =
- new Parcelable.Creator<State>() {
- public State createFromParcel(Parcel in) { return new State(in); }
- public State[] newArray(int size) { return new State[size]; }
- };
- }
-
- /**
- * Interface for a callback to be invoked when a value change should be announced for
- * accessibility.
- */
- public interface OnAnnounceValueChangedListener {
- /**
- * Called when a value change should be announced.
- * @param picker The number picker whose value changed.
- * @param value The new value.
- * @param displayedValue The text displayed for the value, or null if the value itself
- * is displayed.
- */
- void onAnnounceValueChanged(NumberPicker picker, int value, String displayedValue);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Predicate.java b/src/com/android/deskclock/Predicate.java
new file mode 100644
index 0000000..9d9cebc
--- /dev/null
+++ b/src/com/android/deskclock/Predicate.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * A Predicate can determine a true or false value for any input of its
+ * parameterized type. For example, a {@code RegexPredicate} might implement
+ * {@code Predicate<String>}, and return true for any String that matches its
+ * given regular expression.
+ * <p/>
+ * <p/>
+ * Implementors of Predicate which may cause side effects upon evaluation are
+ * strongly encouraged to state this fact clearly in their API documentation.
+ */
+public interface Predicate<T> {
+
+ boolean apply(T t);
+
+ /**
+ * An implementation of the predicate interface that always returns true.
+ */
+ Predicate TRUE = new Predicate() {
+ @Override
+ public boolean apply(Object o) {
+ return true;
+ }
+ };
+
+ /**
+ * An implementation of the predicate interface that always returns false.
+ */
+ Predicate FALSE = new Predicate() {
+ @Override
+ public boolean apply(Object o) {
+ return false;
+ }
+ };
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/RingtonePickerDialogFragment.java b/src/com/android/deskclock/RingtonePickerDialogFragment.java
deleted file mode 100644
index 826566e..0000000
--- a/src/com/android/deskclock/RingtonePickerDialogFragment.java
+++ /dev/null
@@ -1,503 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.DialogFragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.app.LoaderManager;
-import android.content.AsyncTaskLoader;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Loader;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.media.AudioManager;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.annotation.StringRes;
-import android.support.v13.app.FragmentCompat;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.app.AlertDialog;
-import android.util.Pair;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
-
-/**
- * This ringtone picker offers some flexibility over the system ringtone picker. It can be themed
- * and it allows control of the ringtones that are displayed and their labels.
- */
-public final class RingtonePickerDialogFragment extends DialogFragment implements
- DialogInterface.OnClickListener,
- FragmentCompat.OnRequestPermissionsResultCallback,
- LoaderManager.LoaderCallbacks<RingtoneManager> {
-
- private static final String ARGS_KEY_TITLE = "title";
- private static final String ARGS_KEY_DEFAULT_RINGTONE_TITLE = "default_ringtone_title";
- private static final String ARGS_KEY_DEFAULT_RINGTONE_URI = "default_ringtone_uri";
- private static final String ARGS_KEY_EXISTING_RINGTONE_URI = "existing_ringtone_uri";
-
- private static final String STATE_KEY_REQUESTING_PERMISSION = "requesting_permission";
- private static final String STATE_KEY_SELECTED_RINGTONE_URI = "selected_ringtone_uri";
-
- private boolean mRequestingPermission;
- private Uri mSelectedRingtoneUri;
-
- private RingtoneAdapter mRingtoneAdapter;
- private AlertDialog mDialog;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Restore saved instance state.
- if (savedInstanceState != null) {
- mRequestingPermission = savedInstanceState.getBoolean(STATE_KEY_REQUESTING_PERMISSION);
- mSelectedRingtoneUri =
- savedInstanceState.getParcelable(STATE_KEY_SELECTED_RINGTONE_URI);
- } else {
- // Initialize selection to the existing ringtone.
- mSelectedRingtoneUri = getArguments().getParcelable(ARGS_KEY_EXISTING_RINGTONE_URI);
- }
- }
-
- @Override
- public AlertDialog onCreateDialog(Bundle savedInstanceState) {
- final AlertDialog.Builder builder =
- new AlertDialog.Builder(getActivity(), R.style.DialogTheme);
- final Bundle args = getArguments();
-
- mRingtoneAdapter = new RingtoneAdapter(builder.getContext())
- .addStaticRingtone(R.string.silent_ringtone_title, Utils.RINGTONE_SILENT)
- .addStaticRingtone(args.getInt(ARGS_KEY_DEFAULT_RINGTONE_TITLE),
- (Uri) args.getParcelable(ARGS_KEY_DEFAULT_RINGTONE_URI));
- mDialog = builder.setTitle(args.getInt(ARGS_KEY_TITLE))
- .setSingleChoiceItems(mRingtoneAdapter, -1, this /* listener */)
- .setPositiveButton(android.R.string.ok, this /* listener */)
- .setNegativeButton(android.R.string.cancel, null /* listener */)
- .create();
-
- return mDialog;
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- if (savedInstanceState == null
- && ContextCompat.checkSelfPermission(getActivity(), READ_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED) {
- mRequestingPermission = true;
- FragmentCompat.requestPermissions(this, new String[] { READ_EXTERNAL_STORAGE },
- 0 /* requestCode */);
- } else if (!mRequestingPermission) {
- getLoaderManager().initLoader(0 /* id */, null /* args */, this /* callback */);
- }
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
- @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-
- for (final String permission : permissions) {
- if (READ_EXTERNAL_STORAGE.equals(permission)) {
- mRequestingPermission = false;
-
- // Show the dialog now that we've prompted the user for permissions.
- mDialog.show();
-
- getLoaderManager().initLoader(0 /* id */, null /* args */, this /* callback */);
- break;
- }
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- // Disable the positive button until we have a valid selection (Note: this is the first
- // point in the fragment's lifecycle that the dialog *should* have all its views).
- final View positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
- if (positiveButton != null) {
- positiveButton.setEnabled(!mRingtoneAdapter.isEmpty() && mSelectedRingtoneUri != null);
- }
-
- // Hide the dialog if we are currently requesting permissions.
- if (mRequestingPermission) {
- mDialog.hide();
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- // Allow the volume rocker to control the alarm stream volume while the picker is showing.
- mDialog.setVolumeControlStream(AudioManager.STREAM_ALARM);
- }
-
- @Override
- public void onSaveInstanceState(@NonNull Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putBoolean(STATE_KEY_REQUESTING_PERMISSION, mRequestingPermission);
- outState.putParcelable(STATE_KEY_SELECTED_RINGTONE_URI, mSelectedRingtoneUri);
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // Stop playing the preview unless we are currently undergoing a configuration change
- // (e.g. orientation).
- final Activity activity = getActivity();
- if (activity != null && !activity.isChangingConfigurations()) {
- RingtonePreviewKlaxon.stop(activity);
- }
- }
-
- @Override
- public Loader<RingtoneManager> onCreateLoader(int id, Bundle args) {
- return new RingtoneManagerLoader(getActivity());
- }
-
- @Override
- public void onLoadFinished(Loader<RingtoneManager> loader, RingtoneManager ringtoneManager) {
- // Swap in the new ringtone manager.
- mRingtoneAdapter.setRingtoneManager(ringtoneManager);
-
- // Preserve the selected ringtone.
- final ListView listView = mDialog.getListView();
- final int checkedPosition = mRingtoneAdapter.getRingtonePosition(mSelectedRingtoneUri);
- if (checkedPosition != ListView.INVALID_POSITION) {
- listView.setItemChecked(checkedPosition, true);
-
- // Also scroll the list to the selected ringtone (this method is poorly named).
- listView.setSelection(checkedPosition);
- } else {
- // Can't find the selected ringtone, clear the current selection.
- mSelectedRingtoneUri = null;
- listView.clearChoices();
- }
-
- // Enable the positive button if we have a valid selection (Note: the positive button may
- // be null if this callback returns before onStart).
- final View positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
- if (positiveButton != null) {
- positiveButton.setEnabled(mSelectedRingtoneUri != null);
- }
-
- // On M devices the checked view's drawable state isn't updated properly when it is first
- // bound, so we must use a blunt approach to force it to refresh correctly.
- if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
- listView.post(new Runnable() {
- @Override
- public void run() {
- for (int i = listView.getChildCount() - 1; i >= 0; --i) {
- listView.getChildAt(i).refreshDrawableState();
- }
- }
- });
- }
- }
-
- @Override
- public void onLoaderReset(Loader<RingtoneManager> loader) {
- mRingtoneAdapter.setRingtoneManager(null);
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- if (mSelectedRingtoneUri != null) {
- OnRingtoneSelectedListener listener = null;
- if (getParentFragment() instanceof OnRingtoneSelectedListener) {
- listener = (OnRingtoneSelectedListener) getParentFragment();
- } else if (getActivity() instanceof OnRingtoneSelectedListener) {
- listener = (OnRingtoneSelectedListener) getActivity();
- }
-
- if (listener != null) {
- listener.onRingtoneSelected(getTag(), mSelectedRingtoneUri);
- }
- }
- } else if (which >= 0) {
- // Update the selected ringtone, enabling the positive button if valid.
- mSelectedRingtoneUri = mRingtoneAdapter.getItem(which);
-
- // Enable the positive button if we have a valid selection.
- final View positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
- positiveButton.setEnabled(mSelectedRingtoneUri != null);
-
- // Play the preview for the clicked ringtone.
- if (mSelectedRingtoneUri == null
- || mSelectedRingtoneUri.equals(Utils.RINGTONE_SILENT)) {
- RingtonePreviewKlaxon.stop(getActivity());
- } else {
- RingtonePreviewKlaxon.start(getActivity(), mSelectedRingtoneUri);
- }
- }
- }
-
- /**
- * Callback interface for when a ringtone is selected via a picker. Typically implemented by
- * the activity or fragment which launches the ringtone picker.
- */
- public interface OnRingtoneSelectedListener {
- /**
- * Called when the ringtone picker dialog is confirmed and dismissed.
- *
- * @param tag the tag of the ringtone picker dialog fragment
- * @param ringtoneUri the uri of the ringtone that was picked
- */
- void onRingtoneSelected(String tag, Uri ringtoneUri);
- }
-
- public static final class Builder {
-
- private final Bundle mArgs = new Bundle();
-
- public Builder setTitle(@StringRes int titleId) {
- mArgs.putInt(ARGS_KEY_TITLE, titleId);
- return this;
- }
-
- public Builder setDefaultRingtoneTitle(@StringRes int titleId) {
- mArgs.putInt(ARGS_KEY_DEFAULT_RINGTONE_TITLE, titleId);
- return this;
- }
-
- public Builder setDefaultRingtoneUri(Uri ringtoneUri) {
- mArgs.putParcelable(ARGS_KEY_DEFAULT_RINGTONE_URI, ringtoneUri);
- return this;
- }
-
- public Builder setExistingRingtoneUri(Uri ringtoneUri) {
- mArgs.putParcelable(ARGS_KEY_EXISTING_RINGTONE_URI, ringtoneUri);
- return this;
- }
-
- public void show(FragmentManager fragmentManager, String tag) {
- final DialogFragment fragment = new RingtonePickerDialogFragment();
- fragment.setArguments(mArgs);
- fragment.show(fragmentManager, tag);
- }
-
- public void show(FragmentTransaction fragmentTransaction, String tag) {
- final DialogFragment fragment = new RingtonePickerDialogFragment();
- fragment.setArguments(mArgs);
- fragment.show(fragmentTransaction, tag);
- }
- }
-
- private static class RingtoneManagerLoader extends AsyncTaskLoader<RingtoneManager> {
-
- private RingtoneManager mRingtoneManager;
- private Cursor mRingtoneCursor;
-
- public RingtoneManagerLoader(Context context) {
- super(context);
- }
-
- @Override
- public RingtoneManager loadInBackground() {
- final RingtoneManager ringtoneManager = new RingtoneManager(getContext());
- ringtoneManager.setType(AudioManager.STREAM_ALARM);
-
- // Force the ringtone manager to load its ringtones. The cursor will be cached
- // internally by the ringtone manager.
- try {
- ringtoneManager.getCursor();
- } catch (Exception e) {
- LogUtils.e("Error getting Ringtone Manager cursor", e);
- }
-
- return ringtoneManager;
- }
-
- @Override
- public void deliverResult(RingtoneManager ringtoneManager) {
- if (mRingtoneManager != ringtoneManager) {
- if (mRingtoneCursor != null && !mRingtoneCursor.isClosed()) {
- mRingtoneCursor.close();
- }
- mRingtoneManager = ringtoneManager;
- try {
- mRingtoneCursor = mRingtoneManager.getCursor();
- } catch (Exception e) {
- LogUtils.e("Error getting Ringtone Manager cursor", e);
- }
- }
- super.deliverResult(ringtoneManager);
- }
-
- @Override
- protected void onReset() {
- super.onReset();
-
- if (mRingtoneCursor != null && !mRingtoneCursor.isClosed()) {
- mRingtoneCursor.close();
- mRingtoneCursor = null;
- }
- mRingtoneManager = null;
- }
-
- @Override
- protected void onStartLoading() {
- super.onStartLoading();
-
- if (mRingtoneManager != null) {
- deliverResult(mRingtoneManager);
- } else {
- forceLoad();
- }
- }
- }
-
- private static class RingtoneAdapter extends BaseAdapter {
-
- private final List<Pair<Integer, Uri>> mStaticRingtones;
- private final LayoutInflater mLayoutInflater;
-
- private RingtoneManager mRingtoneManager;
- private Cursor mRingtoneCursor;
-
- public RingtoneAdapter(Context context) {
- mStaticRingtones = new ArrayList<>(2 /* magic */);
- mLayoutInflater = LayoutInflater.from(context);
- }
-
- /**
- * Add a static ringtone item to display before the system ones.
- *
- * @param title the title to display for the ringtone
- * @param ringtoneUri the {@link Uri} for the ringtone
- * @return this object so method calls may be chained
- */
- public RingtoneAdapter addStaticRingtone(@StringRes int title, Uri ringtoneUri) {
- if (title != 0 && ringtoneUri != null) {
- mStaticRingtones.add(Pair.create(title, ringtoneUri));
- notifyDataSetChanged();
- }
-
- return this;
- }
-
- /**
- * Set the {@link RingtoneManager} to query for system ringtones.
- *
- * @param ringtoneManager the {@link RingtoneManager} to query for system ringtones
- * @return this object so method calls may be chained
- */
- public RingtoneAdapter setRingtoneManager(RingtoneManager ringtoneManager) {
- mRingtoneManager = ringtoneManager;
- try {
- mRingtoneCursor = ringtoneManager == null ? null : ringtoneManager.getCursor();
- } catch (Exception e) {
- LogUtils.e("Error getting Ringtone Manager cursor", e);
- }
- notifyDataSetChanged();
-
- return this;
- }
-
- /**
- * Returns the position of the given ringtone uri.
- *
- * @param ringtoneUri the {@link Uri} to retrieve the position of
- * @return the ringtones position in the adapter
- */
- public int getRingtonePosition(Uri ringtoneUri) {
- if (ringtoneUri == null) {
- return ListView.INVALID_POSITION;
- }
-
- final int staticRingtoneCount = mStaticRingtones.size();
- for (int position = 0; position < staticRingtoneCount; ++position) {
- if (ringtoneUri.equals(mStaticRingtones.get(position).second)) {
- return position;
- }
- }
-
- final int position = mRingtoneManager.getRingtonePosition(ringtoneUri);
- if (position != -1) {
- return position + staticRingtoneCount;
- }
- return ListView.INVALID_POSITION;
- }
-
- @Override
- public int getCount() {
- if (mRingtoneCursor == null) {
- return 0;
- }
- return mStaticRingtones.size() + mRingtoneCursor.getCount();
- }
-
- @Override
- public Uri getItem(int position) {
- final int staticRingtoneCount = mStaticRingtones.size();
- if (position < staticRingtoneCount) {
- return mStaticRingtones.get(position).second;
- }
- return mRingtoneManager.getRingtoneUri(position - staticRingtoneCount);
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- @SuppressLint("PrivateResource")
- public View getView(int position, View view, ViewGroup parent) {
- if (view == null) {
- // Use AlertDialog's singleChoiceItemLayout directly here, if this breaks in the
- // future just copy the layout to DeskClock's res/.
- view = mLayoutInflater.inflate(R.layout.select_dialog_singlechoice_material,
- parent, false /* attachToRoot */);
- }
-
- final TextView textView = (TextView) view.findViewById(android.R.id.text1);
- final int staticRingtoneCount = mStaticRingtones.size();
- if (position < staticRingtoneCount) {
- textView.setText(mStaticRingtones.get(position).first);
- } else {
- mRingtoneCursor.moveToPosition(position - staticRingtoneCount);
- textView.setText(mRingtoneCursor.getString(RingtoneManager.TITLE_COLUMN_INDEX));
- }
-
- return view;
- }
- }
-}
diff --git a/src/com/android/deskclock/RingtonePreviewKlaxon.java b/src/com/android/deskclock/RingtonePreviewKlaxon.java
index d11ed7e..eea5fe3 100644
--- a/src/com/android/deskclock/RingtonePreviewKlaxon.java
+++ b/src/com/android/deskclock/RingtonePreviewKlaxon.java
@@ -34,14 +34,14 @@
public static void start(Context context, Uri uri) {
stop(context);
LogUtils.i("RingtonePreviewKlaxon.start()");
- getAsyncRingtonePlayer(context).play(uri);
+ getAsyncRingtonePlayer(context).play(uri, 0);
}
private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
if (sAsyncRingtonePlayer == null) {
- sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext(), null);
+ sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
}
return sAsyncRingtonePlayer;
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Screensaver.java b/src/com/android/deskclock/Screensaver.java
index 673ecca..427885e 100644
--- a/src/com/android/deskclock/Screensaver.java
+++ b/src/com/android/deskclock/Screensaver.java
@@ -1,11 +1,11 @@
/*
- * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2016 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
+ * 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,
@@ -41,20 +41,22 @@
private final OnPreDrawListener mStartPositionUpdater = new StartPositionUpdater();
private MoveScreensaverRunnable mPositionUpdater;
- private View mContentView, mSaverView;
- private View mAnalogClock, mDigitalClock;
private String mDateFormat;
private String mDateFormatForAccessibility;
+ private View mContentView;
+ private View mMainClockView;
+ private TextClock mDigitalClock;
+ private AnalogClock mAnalogClock;
+
/* Register ContentObserver to see alarm changes for pre-L */
- private final ContentObserver mSettingsContentObserver = Utils.isPreL()
- ? new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- Utils.refreshAlarm(Screensaver.this, mContentView);
- }
- }
- : null;
+ private final ContentObserver mSettingsContentObserver =
+ Utils.isLOrLater() ? null : new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ Utils.refreshAlarm(Screensaver.this, mContentView);
+ }
+ };
// Runs every midnight or when the time changes and refreshes the date.
private final Runnable mMidnightUpdater = new Runnable() {
@@ -77,9 +79,9 @@
@Override
public void onCreate() {
LOGGER.v("Screensaver created");
- super.onCreate();
- setTheme(R.style.ScreensaverActivityTheme);
+ setTheme(R.style.Theme_DeskClock);
+ super.onCreate();
mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
@@ -91,24 +93,34 @@
super.onAttachedToWindow();
setContentView(R.layout.desk_clock_saver);
- mDigitalClock = findViewById(R.id.digital_clock);
- mAnalogClock = findViewById(R.id.analog_clock);
- mSaverView = findViewById(R.id.main_clock);
- mContentView = (View) mSaverView.getParent();
+
+ mContentView = findViewById(R.id.saver_container);
+ mMainClockView = mContentView.findViewById(R.id.main_clock);
+ mDigitalClock = (TextClock) mMainClockView.findViewById(R.id.digital_clock);
+ mAnalogClock = (AnalogClock) mMainClockView.findViewById(R.id.analog_clock);
setClockStyle();
- Utils.setTimeFormat((TextClock) mDigitalClock);
+ Utils.setClockIconTypeface(mContentView);
+ Utils.setTimeFormat(mDigitalClock, false);
+ mAnalogClock.enableSeconds(false);
- mPositionUpdater = new MoveScreensaverRunnable(mContentView, mSaverView);
+ mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
+ | View.SYSTEM_UI_FLAG_IMMERSIVE
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+
+ mPositionUpdater = new MoveScreensaverRunnable(mContentView, mMainClockView);
// We want the screen saver to exit upon user interaction.
setInteractive(false);
-
setFullscreen(true);
// Setup handlers for time reference changes and date updates.
- final IntentFilter filter = new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
- registerReceiver(mAlarmChangedReceiver, filter);
+ if (Utils.isLOrLater()) {
+ registerReceiver(mAlarmChangedReceiver,
+ new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED));
+ }
if (mSettingsContentObserver != null) {
@SuppressWarnings("deprecation")
@@ -117,7 +129,7 @@
}
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
- Utils.refreshAlarm(Screensaver.this, mContentView);
+ Utils.refreshAlarm(this, mContentView);
startPositionUpdater();
UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
@@ -136,7 +148,9 @@
stopPositionUpdater();
// Tear down handlers for time reference changes and date updates.
- unregisterReceiver(mAlarmChangedReceiver);
+ if (Utils.isLOrLater()) {
+ unregisterReceiver(mAlarmChangedReceiver);
+ }
}
@Override
@@ -144,17 +158,13 @@
LOGGER.v("Screensaver configuration changed");
super.onConfigurationChanged(newConfig);
- // Ignore the configuration change if no window exists.
- if (getWindow() != null) {
- // Restart the position updater via a PreDrawListener after layout is complete.
- startPositionUpdater();
- }
+ startPositionUpdater();
}
private void setClockStyle() {
Utils.setScreensaverClockStyle(mDigitalClock, mAnalogClock);
final boolean dimNightMode = DataModel.getDataModel().getScreensaverNightModeOn();
- Utils.dimClockView(dimNightMode, mSaverView);
+ Utils.dimClockView(dimNightMode, mMainClockView);
setScreenBright(!dimNightMode);
}
@@ -164,21 +174,25 @@
* schedule future callbacks to move the time display each minute.
*/
private void startPositionUpdater() {
- mContentView.getViewTreeObserver().addOnPreDrawListener(mStartPositionUpdater);
+ if (mContentView != null) {
+ mContentView.getViewTreeObserver().addOnPreDrawListener(mStartPositionUpdater);
+ }
}
/**
* This activity is no longer in the foreground; position callbacks should be removed.
*/
private void stopPositionUpdater() {
- mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
+ if (mContentView != null) {
+ mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
+ }
mPositionUpdater.stop();
}
private final class StartPositionUpdater implements OnPreDrawListener {
/**
* This callback occurs after initial layout has completed. It is an appropriate place to
- * select a random position for {@link #mSaverView} and schedule future callbacks to update
+ * select a random position for {@link #mMainClockView} and schedule future callbacks to update
* its position.
*
* @return {@code true} to continue with the drawing pass
diff --git a/src/com/android/deskclock/ScreensaverActivity.java b/src/com/android/deskclock/ScreensaverActivity.java
index 8cdf3b8..656cfc7 100644
--- a/src/com/android/deskclock/ScreensaverActivity.java
+++ b/src/com/android/deskclock/ScreensaverActivity.java
@@ -26,7 +26,6 @@
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
-import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.Window;
@@ -39,24 +38,17 @@
import static android.content.Intent.ACTION_BATTERY_CHANGED;
import static android.os.BatteryManager.EXTRA_PLUGGED;
-public class ScreensaverActivity extends AppCompatActivity {
+public class ScreensaverActivity extends BaseActivity {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("ScreensaverActivity");
/** These flags keep the screen on if the device is plugged in. */
- private static final int sWindowFlags = WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ private static final int WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
private final OnPreDrawListener mStartPositionUpdater = new StartPositionUpdater();
- private MoveScreensaverRunnable mPositionUpdater;
-
- private View mContentView, mSaverView;
- private View mAnalogClock, mDigitalClock;
-
- private String mDateFormat;
- private String mDateFormatForAccessibility;
private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
@Override
@@ -98,24 +90,43 @@
}
};
+ private String mDateFormat;
+ private String mDateFormatForAccessibility;
+
+ private View mContentView;
+ private View mMainClockView;
+
+ private MoveScreensaverRunnable mPositionUpdater;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
+ mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
+
setContentView(R.layout.desk_clock_saver);
- mDigitalClock = findViewById(R.id.digital_clock);
- mAnalogClock = findViewById(R.id.analog_clock);
- mSaverView = findViewById(R.id.main_clock);
mContentView = findViewById(R.id.saver_container);
+ mMainClockView = mContentView.findViewById(R.id.main_clock);
+
+ final View digitalClock = mMainClockView.findViewById(R.id.digital_clock);
+ final AnalogClock analogClock =
+ (AnalogClock) mMainClockView.findViewById(R.id.analog_clock);
+
+ Utils.setClockIconTypeface(mMainClockView);
+ Utils.setTimeFormat((TextClock) digitalClock, false);
+ Utils.setClockStyle(digitalClock, analogClock);
+ Utils.dimClockView(true, mMainClockView);
+ analogClock.enableSeconds(false);
+
mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
+ | View.SYSTEM_UI_FLAG_IMMERSIVE
| View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ mContentView.setOnSystemUiVisibilityChangeListener(new InteractionListener());
- Utils.setTimeFormat((TextClock) mDigitalClock);
- Utils.setClockStyle(mDigitalClock, mAnalogClock);
- Utils.dimClockView(true, mSaverView);
-
- mPositionUpdater = new MoveScreensaverRunnable(mContentView, mSaverView);
+ mPositionUpdater = new MoveScreensaverRunnable(mContentView, mMainClockView);
final Intent intent = getIntent();
if (intent != null) {
@@ -132,7 +143,9 @@
filter.addAction(Intent.ACTION_POWER_CONNECTED);
filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
filter.addAction(Intent.ACTION_USER_PRESENT);
- filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+ if (Utils.isLOrLater()) {
+ filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+ }
registerReceiver(mIntentReceiver, filter);
if (mSettingsContentObserver != null) {
@@ -146,9 +159,6 @@
public void onResume() {
super.onResume();
- mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
- mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
-
Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
@@ -156,8 +166,7 @@
UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
final Intent intent = registerReceiver(null, new IntentFilter(ACTION_BATTERY_CHANGED));
- final int plugged = intent.getIntExtra(EXTRA_PLUGGED, 0);
- final boolean pluggedIn = plugged != 0;
+ final boolean pluggedIn = intent != null && intent.getIntExtra(EXTRA_PLUGGED, 0) != 0;
updateWakeLock(pluggedIn);
}
@@ -191,9 +200,9 @@
final WindowManager.LayoutParams winParams = win.getAttributes();
winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
if (pluggedIn) {
- winParams.flags |= sWindowFlags;
+ winParams.flags |= WINDOW_FLAGS;
} else {
- winParams.flags &= (~sWindowFlags);
+ winParams.flags &= (~WINDOW_FLAGS);
}
win.setAttributes(winParams);
}
@@ -215,10 +224,10 @@
mPositionUpdater.stop();
}
- private class StartPositionUpdater implements OnPreDrawListener {
+ private final class StartPositionUpdater implements OnPreDrawListener {
/**
* This callback occurs after initial layout has completed. It is an appropriate place to
- * select a random position for {@link #mSaverView} and schedule future callbacks to update
+ * select a random position for {@link #mMainClockView} and schedule future callbacks to update
* its position.
*
* @return {@code true} to continue with the drawing pass
@@ -235,4 +244,15 @@
return true;
}
}
+
+ private final class InteractionListener implements View.OnSystemUiVisibilityChangeListener {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ // When the user interacts with the screen, the navigation bar reappears
+ if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
+ // We want the screen saver to exit upon user interaction.
+ finish();
+ }
+ }
+ }
}
diff --git a/src/com/android/deskclock/StopwatchTextController.java b/src/com/android/deskclock/StopwatchTextController.java
new file mode 100644
index 0000000..4b94dbd
--- /dev/null
+++ b/src/com/android/deskclock/StopwatchTextController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.content.Context;
+import android.widget.TextView;
+
+import com.android.deskclock.uidata.UiDataModel;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+/**
+ * A controller which will format a provided time in millis to display as a stopwatch.
+ */
+public final class StopwatchTextController {
+
+ private final TextView mMainTextView;
+ private final TextView mHundredthsTextView;
+
+ private long mLastTime = Long.MIN_VALUE;
+
+ public StopwatchTextController(TextView mainTextView, TextView hundredthsTextView) {
+ mMainTextView = mainTextView;
+ mHundredthsTextView = hundredthsTextView;
+ }
+
+ public void setTimeString(long accumulatedTime) {
+ // Since time is only displayed to centiseconds, if there is a change at the milliseconds
+ // level but not the centiseconds level, we can avoid unnecessary work.
+ if ((mLastTime / 10) == (accumulatedTime / 10)) {
+ return;
+ }
+
+ final int hours = (int) (accumulatedTime / HOUR_IN_MILLIS);
+ int remainder = (int) (accumulatedTime % HOUR_IN_MILLIS);
+
+ final int minutes = (int) (remainder / MINUTE_IN_MILLIS);
+ remainder = (int) (remainder % MINUTE_IN_MILLIS);
+
+ final int seconds = (int) (remainder / SECOND_IN_MILLIS);
+ remainder = (int) (remainder % SECOND_IN_MILLIS);
+
+ mHundredthsTextView.setText(UiDataModel.getUiDataModel().getFormattedNumber(
+ remainder / 10, 2));
+
+ // Avoid unnecessary computations and garbage creation if seconds have not changed since
+ // last layout pass.
+ if ((mLastTime / SECOND_IN_MILLIS) != (accumulatedTime / SECOND_IN_MILLIS)) {
+ final Context context = mMainTextView.getContext();
+ final String time = Utils.getTimeString(context, hours, minutes, seconds);
+ mMainTextView.setText(time);
+ }
+ mLastTime = accumulatedTime;
+ }
+}
diff --git a/src/com/android/deskclock/ThemeUtils.java b/src/com/android/deskclock/ThemeUtils.java
new file mode 100644
index 0000000..08ee012
--- /dev/null
+++ b/src/com/android/deskclock/ThemeUtils.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.AttrRes;
+import android.support.annotation.ColorInt;
+
+public final class ThemeUtils {
+
+ /** Temporary array used internally to resolve attributes. */
+ private static final int[] TEMP_ATTR = new int[1];
+
+ private ThemeUtils() {
+ // Prevent instantiation.
+ }
+
+ /**
+ * Convenience method for retrieving a themed color value.
+ *
+ * @param context the {@link Context} to resolve the theme attribute against
+ * @param attr the attribute corresponding to the color to resolve
+ * @return the color value of the resolved attribute
+ */
+ @ColorInt
+ public static int resolveColor(Context context, @AttrRes int attr) {
+ return resolveColor(context, attr, null /* stateSet */);
+ }
+
+ /**
+ * Convenience method for retrieving a themed color value.
+ *
+ * @param context the {@link Context} to resolve the theme attribute against
+ * @param attr the attribute corresponding to the color to resolve
+ * @param stateSet an array of {@link android.view.View} states
+ * @return the color value of the resolved attribute
+ */
+ @ColorInt
+ public static int resolveColor(Context context, @AttrRes int attr, @AttrRes int[] stateSet) {
+ final TypedArray a;
+ synchronized (TEMP_ATTR) {
+ TEMP_ATTR[0] = attr;
+ a = context.obtainStyledAttributes(TEMP_ATTR);
+ }
+
+ try {
+ if (stateSet == null) {
+ return a.getColor(0, Color.RED);
+ }
+
+ final ColorStateList colorStateList = a.getColorStateList(0);
+ if (colorStateList != null) {
+ return colorStateList.getColorForState(stateSet, Color.RED);
+ }
+ return Color.RED;
+ } finally {
+ a.recycle();
+ }
+ }
+
+ /**
+ * Convenience method for retrieving a themed drawable.
+ *
+ * @param context the {@link Context} to resolve the theme attribute against
+ * @param attr the attribute corresponding to the drawable to resolve
+ * @return the drawable of the resolved attribute
+ */
+ public static Drawable resolveDrawable(Context context, @AttrRes int attr) {
+ final TypedArray a;
+ synchronized (TEMP_ATTR) {
+ TEMP_ATTR[0] = attr;
+ a = context.obtainStyledAttributes(TEMP_ATTR);
+ }
+
+ try {
+ return a.getDrawable(0);
+ } finally {
+ a.recycle();
+ }
+ }
+}
+
diff --git a/src/com/android/deskclock/TimerCircleFrameLayout.java b/src/com/android/deskclock/TimerCircleFrameLayout.java
index 45bd546..e0cf320 100644
--- a/src/com/android/deskclock/TimerCircleFrameLayout.java
+++ b/src/com/android/deskclock/TimerCircleFrameLayout.java
@@ -48,9 +48,15 @@
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+
+ final int paddingTop = getPaddingTop();
+ final int paddingBottom = getPaddingBottom();
+
// Fetch the exact sizes imposed by the parent container.
- final int width = MeasureSpec.getSize(widthMeasureSpec);
- final int height = MeasureSpec.getSize(heightMeasureSpec);
+ final int width = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight;
+ final int height = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom;
final int smallestDimension = Math.min(width, height);
// Fetch the absolute maximum circle size allowed.
@@ -58,9 +64,11 @@
final int size = Math.min(smallestDimension, maxSize);
// Set the size of this container.
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingLeft + paddingRight,
+ MeasureSpec.EXACTLY);
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingTop + paddingBottom,
+ MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/deskclock/TimerTextController.java b/src/com/android/deskclock/TimerTextController.java
new file mode 100644
index 0000000..1744a7b
--- /dev/null
+++ b/src/com/android/deskclock/TimerTextController.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.widget.TextView;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+/**
+ * A controller which will format a provided time in millis to display as a timer.
+ */
+public final class TimerTextController {
+
+ private final TextView mTextView;
+
+ public TimerTextController(TextView textView) {
+ mTextView = textView;
+ }
+
+ public void setTimeString(long remainingTime) {
+ boolean isNegative = false;
+ if (remainingTime < 0) {
+ remainingTime = -remainingTime;
+ isNegative = true;
+ }
+
+ int hours = (int) (remainingTime / HOUR_IN_MILLIS);
+ int remainder = (int) (remainingTime % HOUR_IN_MILLIS);
+
+ int minutes = (int) (remainder / MINUTE_IN_MILLIS);
+ remainder = (int) (remainder % MINUTE_IN_MILLIS);
+
+ int seconds = (int) (remainder / SECOND_IN_MILLIS);
+ remainder = (int) (remainder % SECOND_IN_MILLIS);
+
+ // Round up to the next second
+ if (!isNegative && remainder != 0) {
+ seconds++;
+ if (seconds == 60) {
+ seconds = 0;
+ minutes++;
+ if (minutes == 60) {
+ minutes = 0;
+ hours++;
+ }
+ }
+ }
+
+ String time = Utils.getTimeString(mTextView.getContext(), hours, minutes, seconds);
+ if (isNegative && !(hours == 0 && minutes == 0 && seconds == 0)) {
+ time = "\u2212" + time;
+ }
+
+ mTextView.setText(time);
+ }
+}
diff --git a/src/com/android/deskclock/Utils.java b/src/com/android/deskclock/Utils.java
index 0f60da9..6f09a43 100644
--- a/src/com/android/deskclock/Utils.java
+++ b/src/com/android/deskclock/Utils.java
@@ -19,14 +19,12 @@
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.AlarmManager;
+import android.app.AlarmManager.AlarmClockInfo;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -38,13 +36,15 @@
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
-import android.os.SystemClock;
-import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.AnyRes;
import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.os.BuildCompat;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
@@ -60,8 +60,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.uidata.UiDataModel;
import java.text.NumberFormat;
@@ -69,7 +67,6 @@
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
-import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
@@ -87,23 +84,6 @@
*/
public static final Uri RINGTONE_SILENT = Uri.EMPTY;
- // Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
- private static String[] sShortWeekdays = null;
- private static final String DATE_FORMAT_SHORT = "ccccc";
-
- // Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc
- private static String[] sLongWeekdays = null;
- private static final String DATE_FORMAT_LONG = "EEEE";
-
- public static final int DEFAULT_WEEK_START = Calendar.getInstance().getFirstDayOfWeek();
-
- private static Locale sLocaleUsedForWeekdays;
-
- /**
- * Temporary array used by {@link #obtainStyledColor(Context, int, int)}.
- */
- private static final int[] TEMP_ARRAY = new int[1];
-
public static void enforceMainLooper() {
if (Looper.getMainLooper() != Looper.myLooper()) {
throw new IllegalAccessError("May only call from main thread.");
@@ -116,6 +96,15 @@
}
}
+ public static int indexOf(Object[] array, Object item) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i].equals(item)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
/**
* @return {@code true} if the device is prior to {@link Build.VERSION_CODES#LOLLIPOP}
*/
@@ -154,14 +143,14 @@
}
/**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later
- */
+ * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later
+ */
public static boolean isNOrLater() {
- return BuildCompat.isAtLeastN();
+ return BuildCompat.isAtLeastN();
}
/**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#NMR1} or later
+ * @return {@code true} if the device is {@link Build.VERSION_CODES#N_MR1} or later
*/
public static boolean isNMR1OrLater() {
return BuildCompat.isAtLeastNMR1();
@@ -197,22 +186,28 @@
}
/**
- * Uses {@link Utils#calculateRadiusOffset(float, float, float)} after fetching the values
- * from the resources.
+ * Configure the clock that is visible to display seconds. The clock that is not visible never
+ * displays seconds to avoid it scheduling unnecessary ticking runnables.
*/
- public static float calculateRadiusOffset(Resources resources) {
- if (resources != null) {
- float strokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
- float dotStrokeSize = resources.getDimension(R.dimen.circletimer_dot_size);
- float markerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
- return calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize);
- } else {
- return 0f;
+ public static void setClockSecondsEnabled(TextClock digitalClock, AnalogClock analogClock) {
+ final boolean displaySeconds = DataModel.getDataModel().getDisplayClockSeconds();
+ final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle();
+ switch (clockStyle) {
+ case ANALOG:
+ setTimeFormat(digitalClock, false);
+ analogClock.enableSeconds(displaySeconds);
+ return;
+ case DIGITAL:
+ analogClock.enableSeconds(false);
+ setTimeFormat(digitalClock, displaySeconds);
+ return;
}
+
+ throw new IllegalStateException("unexpected clock style: " + clockStyle);
}
/**
- * For screensavers to set whether the digital or analog clock should be displayed.
+ * Set whether the digital or analog clock should be displayed in the application.
* Returns the view to be displayed.
*/
public static View setClockStyle(View digitalClock, View analogClock) {
@@ -267,7 +262,7 @@
* Update and return the PendingIntent corresponding to the given {@code intent}.
*
* @param context the Context in which the PendingIntent should start the service
- * @param intent an Intent describing the service to be started
+ * @param intent an Intent describing the service to be started
* @return a PendingIntent that will start a service
*/
public static PendingIntent pendingServiceIntent(Context context, Intent intent) {
@@ -278,7 +273,7 @@
* Update and return the PendingIntent corresponding to the given {@code intent}.
*
* @param context the Context in which the PendingIntent should start the activity
- * @param intent an Intent describing the activity to be started
+ * @param intent an Intent describing the activity to be started
* @return a PendingIntent that will start an activity
*/
public static PendingIntent pendingActivityIntent(Context context, Intent intent) {
@@ -302,7 +297,7 @@
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static String getNextAlarmLOrLater(Context context) {
final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- final AlarmManager.AlarmClockInfo info = am.getNextAlarmClock();
+ final AlarmClockInfo info = getNextAlarmClock(am);
if (info != null) {
final long triggerTime = info.getTriggerTime();
final Calendar alarmTime = Calendar.getInstance();
@@ -313,6 +308,16 @@
return null;
}
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static AlarmClockInfo getNextAlarmClock(AlarmManager am) {
+ return am.getNextAlarmClock();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public static void updateNextAlarm(AlarmManager am, AlarmClockInfo info, PendingIntent op) {
+ am.setAlarmClock(info, op);
+ }
+
public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) {
final Calendar nextAlarmTime = alarmInstance.getAlarmTime();
final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis();
@@ -337,13 +342,17 @@
nextAlarmView.setVisibility(View.VISIBLE);
nextAlarmIconView.setVisibility(View.VISIBLE);
nextAlarmIconView.setContentDescription(description);
- nextAlarmIconView.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
} else {
nextAlarmView.setVisibility(View.GONE);
nextAlarmIconView.setVisibility(View.GONE);
}
}
+ public static void setClockIconTypeface(View clock) {
+ final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon);
+ nextAlarmIconView.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
+ }
+
/**
* Clock views can call this to refresh their date.
**/
@@ -367,24 +376,27 @@
* Formats the time in the TextClock according to the Locale with a special
* formatting treatment for the am/pm label.
*
- * @param clock - TextClock to format
+ * @param clock TextClock to format
+ * @param includeSeconds whether or not to include seconds in the clock's time
*/
- public static void setTimeFormat(TextClock clock) {
+ public static void setTimeFormat(TextClock clock, boolean includeSeconds) {
if (clock != null) {
// Get the best format for 12 hours mode according to the locale
- clock.setFormat12Hour(get12ModeFormat(0.4f /* amPmRatio */));
+ clock.setFormat12Hour(get12ModeFormat(0.4f /* amPmRatio */, includeSeconds));
// Get the best format for 24 hours mode according to the locale
- clock.setFormat24Hour(get24ModeFormat());
+ clock.setFormat24Hour(get24ModeFormat(includeSeconds));
}
}
/**
- * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the
- * am/pm string to the time string
- * @return format string for 12 hours mode time
+ * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the
+ * am/pm string to the time string
+ * @param includeSeconds whether or not to include seconds in the time string
+ * @return format string for 12 hours mode time, not including seconds
*/
- public static CharSequence get12ModeFormat(float amPmRatio) {
- String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "hma");
+ public static CharSequence get12ModeFormat(float amPmRatio, boolean includeSeconds) {
+ String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
+ includeSeconds ? "hmsa" : "hma");
if (amPmRatio <= 0) {
pattern = pattern.replaceAll("a", "").trim();
}
@@ -408,8 +420,9 @@
return sp;
}
- public static CharSequence get24ModeFormat() {
- return DateFormat.getBestDateTimePattern(Locale.getDefault(), "Hm");
+ public static CharSequence get24ModeFormat(boolean includeSeconds) {
+ return DateFormat.getBestDateTimePattern(Locale.getDefault(),
+ includeSeconds ? "Hms" : "Hm");
}
/**
@@ -436,7 +449,7 @@
* e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for
* midnight on 1/2/2016 in the NY timezone since it changes days first.
*
- * @param time a point in time from which to compute midnight on the subsequent day
+ * @param time a point in time from which to compute midnight on the subsequent day
* @param zones a collection of time zones
* @return the nearest point in the future at which any of the time zones changes days
*/
@@ -463,86 +476,6 @@
return next == null ? null : next.getTime();
}
- /**
- * Convenience method for retrieving a themed color value.
- *
- * @param context the {@link Context} to resolve the theme attribute against
- * @param attr the attribute corresponding to the color to resolve
- * @param defValue the default color value to use if the attribute cannot be resolved
- * @return the color value of the resolve attribute
- */
- public static int obtainStyledColor(Context context, int attr, int defValue) {
- TEMP_ARRAY[0] = attr;
- final TypedArray a = context.obtainStyledAttributes(TEMP_ARRAY);
- try {
- return a.getColor(0, defValue);
- } finally {
- a.recycle();
- }
- }
-
- /**
- * @param firstDay is the result from getZeroIndexedFirstDayOfWeek
- * @return Single-char version of day name, e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
- */
- public static String getShortWeekday(int position, int firstDay) {
- generateShortAndLongWeekdaysIfNeeded();
- return sShortWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK];
- }
-
- /**
- * @param firstDay is the result from getZeroIndexedFirstDayOfWeek
- * @return Long-version of day name, e.g.: 'Sunday', 'Monday', 'Tuesday', etc
- */
- public static String getLongWeekday(int position, int firstDay) {
- generateShortAndLongWeekdaysIfNeeded();
- return sLongWeekdays[(position + firstDay) % DaysOfWeek.DAYS_IN_A_WEEK];
- }
-
- // Return the first day of the week value corresponding to Calendar.<WEEKDAY> value, which is
- // 1-indexed starting with Sunday.
- public static int getFirstDayOfWeek(Context context) {
- return Integer.parseInt(getDefaultSharedPreferences(context)
- .getString(SettingsActivity.KEY_WEEK_START, String.valueOf(DEFAULT_WEEK_START)));
- }
-
- // Return the first day of the week value corresponding to a week with Sunday at 0 index.
- public static int getZeroIndexedFirstDayOfWeek(Context context) {
- return getFirstDayOfWeek(context) - 1;
- }
-
- private static boolean localeHasChanged() {
- return sLocaleUsedForWeekdays != Locale.getDefault();
- }
-
- /**
- * Generate arrays of short and long weekdays, starting from Sunday
- */
- private static void generateShortAndLongWeekdaysIfNeeded() {
- if (sShortWeekdays != null && sLongWeekdays != null && !localeHasChanged()) {
- // nothing to do
- return;
- }
-
- final Locale locale = Locale.getDefault();
- final SimpleDateFormat shortFormat = new SimpleDateFormat(DATE_FORMAT_SHORT, locale);
- final SimpleDateFormat longFormat = new SimpleDateFormat(DATE_FORMAT_LONG, locale);
-
- sShortWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK];
- sLongWeekdays = new String[DaysOfWeek.DAYS_IN_A_WEEK];
-
- // Create a date (2014/07/20) that is a Sunday
- final long aSunday = new GregorianCalendar(2014, Calendar.JULY, 20).getTimeInMillis();
- for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; i++) {
- final long dayMillis = aSunday + i * DateUtils.DAY_IN_MILLIS;
- sShortWeekdays[i] = shortFormat.format(new Date(dayMillis));
- sLongWeekdays[i] = longFormat.format(new Date(dayMillis));
- }
-
- // Track the Locale used to generate these weekdays
- sLocaleUsedForWeekdays = Locale.getDefault();
- }
-
public static String getNumberFormattedQuantityString(Context context, int id, int quantity) {
final String localizedQuantity = NumberFormat.getInstance().format(quantity);
return context.getResources().getQuantityString(id, quantity, localizedQuantity);
@@ -587,28 +520,6 @@
}
/**
- * Returns the default {@link SharedPreferences} instance from the underlying storage context.
- */
- @TargetApi(Build.VERSION_CODES.N)
- public static SharedPreferences getDefaultSharedPreferences(Context context) {
- final Context storageContext;
- if (isNOrLater()) {
- // All N devices have split storage areas, but we may need to
- // migrate existing preferences into the new device encrypted
- // storage area, which is where our data lives from now on.
- storageContext = context.createDeviceProtectedStorageContext();
- if (!storageContext.moveSharedPreferencesFrom(context,
- PreferenceManager.getDefaultSharedPreferencesName(context))) {
- LogUtils.wtf("Failed to migrate shared preferences");
- }
- } else {
- storageContext = context;
- }
-
- return PreferenceManager.getDefaultSharedPreferences(storageContext);
- }
-
- /**
* @param context from which to query the current device configuration
* @return {@code true} if the device is currently in portrait or reverse portrait orientation
*/
@@ -625,10 +536,93 @@
}
public static long now() {
- return SystemClock.elapsedRealtime();
+ return DataModel.getDataModel().elapsedRealtime();
}
public static long wallClock() {
- return System.currentTimeMillis();
+ return DataModel.getDataModel().currentTimeMillis();
+ }
+
+ /**
+ * @param context to obtain strings.
+ * @param displayMinutes whether or not minutes should be included
+ * @param isAhead {@code true} if the time should be marked 'ahead', else 'behind'
+ * @param hoursDifferent the number of hours the time is ahead/behind
+ * @param minutesDifferent the number of minutes the time is ahead/behind
+ * @return String describing the hours/minutes ahead or behind
+ */
+ public static String createHoursDifferentString(Context context, boolean displayMinutes,
+ boolean isAhead, int hoursDifferent, int minutesDifferent) {
+ String timeString;
+ if (displayMinutes && hoursDifferent != 0) {
+ // Both minutes and hours
+ final String hoursShortQuantityString =
+ Utils.getNumberFormattedQuantityString(context,
+ R.plurals.hours_short, Math.abs(hoursDifferent));
+ final String minsShortQuantityString =
+ Utils.getNumberFormattedQuantityString(context,
+ R.plurals.minutes_short, Math.abs(minutesDifferent));
+ final @StringRes int stringType = isAhead
+ ? R.string.world_hours_minutes_ahead
+ : R.string.world_hours_minutes_behind;
+ timeString = context.getString(stringType, hoursShortQuantityString,
+ minsShortQuantityString);
+ } else {
+ // Minutes alone or hours alone
+ final String hoursQuantityString = Utils.getNumberFormattedQuantityString(
+ context, R.plurals.hours, Math.abs(hoursDifferent));
+ final String minutesQuantityString = Utils.getNumberFormattedQuantityString(
+ context, R.plurals.minutes, Math.abs(minutesDifferent));
+ final @StringRes int stringType = isAhead ? R.string.world_time_ahead
+ : R.string.world_time_behind;
+ timeString = context.getString(stringType, displayMinutes
+ ? minutesQuantityString : hoursQuantityString);
+ }
+ return timeString;
+ }
+
+ /**
+ * @param context The context from which to obtain strings
+ * @param hours Hours to display (if any)
+ * @param minutes Minutes to display (if any)
+ * @param seconds Seconds to display
+ * @return Provided time formatted as a String
+ */
+ static String getTimeString(Context context, int hours, int minutes, int seconds) {
+ if (hours != 0) {
+ return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds);
+ }
+ if (minutes != 0) {
+ return context.getString(R.string.minutes_seconds, minutes, seconds);
+ }
+ return context.getString(R.string.seconds, seconds);
+ }
+
+ public static final class ClickAccessibilityDelegate extends AccessibilityDelegateCompat {
+
+ /** The label for talkback to apply to the view */
+ private final String mLabel;
+
+ /** Whether or not to always make the view visible to talkback */
+ private final boolean mIsAlwaysAccessibilityVisible;
+
+ public ClickAccessibilityDelegate(String label) {
+ this(label, false);
+ }
+
+ public ClickAccessibilityDelegate(String label, boolean isAlwaysAccessibilityVisible) {
+ mLabel = label;
+ mIsAlwaysAccessibilityVisible = isAlwaysAccessibilityVisible;
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (mIsAlwaysAccessibilityVisible) {
+ info.setVisibleToUser(true);
+ }
+ info.addAction(new AccessibilityActionCompat(
+ AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel));
+ }
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/VerticalViewPager.java b/src/com/android/deskclock/VerticalViewPager.java
index c7cdfe5..027523f 100644
--- a/src/com/android/deskclock/VerticalViewPager.java
+++ b/src/com/android/deskclock/VerticalViewPager.java
@@ -17,54 +17,19 @@
package com.android.deskclock;
import android.content.Context;
-import android.support.v4.view.ViewConfigurationCompat;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewParent;
public class VerticalViewPager extends ViewPager {
- // TODO Remove the hack of using a parent view pager
- private ViewPager mParentViewPager;
- private float mLastMotionX;
- private float mLastMotionY;
- private float mTouchSlop;
- private boolean mVerticalDrag;
- private boolean mHorizontalDrag;
-
- // Vertical transit page transformer
- private final ViewPager.PageTransformer mPageTransformer = new ViewPager.PageTransformer() {
- @Override
- public void transformPage(View view, float position) {
- final int pageWidth = view.getWidth();
- final int pageHeight = view.getHeight();
- if (position < -1) {
- // This page is way off-screen to the left.
- view.setAlpha(0);
- } else if (position <= 1) {
- view.setAlpha(1);
- // Counteract the default slide transition
- view.setTranslationX(pageWidth * -position);
- // set Y position to swipe in from top
- float yPosition = position * pageHeight;
- view.setTranslationY(yPosition);
- } else {
- // This page is way off-screen to the right.
- view.setAlpha(0);
- }
- }
- };
public VerticalViewPager(Context context) {
- super(context, null);
+ this(context, null);
}
public VerticalViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
- final ViewConfiguration configuration = ViewConfiguration.get(context);
- mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
init();
}
@@ -86,80 +51,58 @@
private void init() {
// Make page transit vertical
- setPageTransformer(true, mPageTransformer);
+ setPageTransformer(true, new VerticalPageTransformer());
// Get rid of the overscroll drawing that happens on the left and right (the ripple)
setOverScrollMode(View.OVER_SCROLL_NEVER);
}
@Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final boolean toIntercept = super.onInterceptTouchEvent(flipXY(ev));
+ // Return MotionEvent to normal
+ flipXY(ev);
+ return toIntercept;
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent ev) {
- try {
- initializeParent();
- final float x = ev.getX();
- final float y = ev.getY();
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- mLastMotionX = x;
- mLastMotionY = y;
- if (!mParentViewPager.onTouchEvent(ev)) {
- return false;
- }
- return verticalDrag(ev);
- }
- case MotionEvent.ACTION_MOVE: {
- final float xDiff = Math.abs(x - mLastMotionX);
- final float yDiff = Math.abs(y - mLastMotionY);
- if (!mHorizontalDrag && !mVerticalDrag) {
- if (xDiff > mTouchSlop && xDiff > yDiff) { // Swiping left and right
- mHorizontalDrag = true;
- } else if (yDiff > mTouchSlop && yDiff > xDiff) { //Swiping up and down
- mVerticalDrag = true;
- }
- }
- if (mHorizontalDrag) {
- return mParentViewPager.onTouchEvent(ev);
- } else if (mVerticalDrag) {
- return verticalDrag(ev);
- }
- }
- case MotionEvent.ACTION_UP: {
- if (mHorizontalDrag) {
- mHorizontalDrag = false;
- return mParentViewPager.onTouchEvent(ev);
- }
- if (mVerticalDrag) {
- mVerticalDrag = false;
- return verticalDrag(ev);
- }
- }
- }
- // Set both flags to false in case user lifted finger in the parent view pager
- mHorizontalDrag = false;
- mVerticalDrag = false;
- } catch (Exception e) {
- // The mParentViewPager shouldn't be null, but just in case. If this happens,
- // app should not crash, instead just ignore the user swipe input
- // TODO: handle the exception gracefully
- }
- return false;
+ final boolean toHandle = super.onTouchEvent(flipXY(ev));
+ // Return MotionEvent to normal
+ flipXY(ev);
+ return toHandle;
}
- private void initializeParent() {
- if (mParentViewPager == null) {
- // This vertical view pager is nested in the frame layout inside the timer tab
- // (fragment), which is nested inside the horizontal view pager. Therefore,
- // it needs 3 layers to get all the way to the horizontal view pager.
- final ViewParent parent = getParent().getParent().getParent();
- if (parent instanceof ViewPager) {
- mParentViewPager = (ViewPager) parent;
- }
- }
+ private MotionEvent flipXY(MotionEvent ev) {
+ final float width = getWidth();
+ final float height = getHeight();
+
+ final float x = (ev.getY() / height) * width;
+ final float y = (ev.getX() / width) * height;
+
+ ev.setLocation(x, y);
+
+ return ev;
}
- private boolean verticalDrag(MotionEvent ev) {
- final float x = ev.getX();
- final float y = ev.getY();
- ev.setLocation(y, x);
- return super.onTouchEvent(ev);
+ private static final class VerticalPageTransformer implements ViewPager.PageTransformer {
+ @Override
+ public void transformPage(View view, float position) {
+ final int pageWidth = view.getWidth();
+ final int pageHeight = view.getHeight();
+ if (position < -1) {
+ // This page is way off-screen to the left.
+ view.setAlpha(0);
+ } else if (position <= 1) {
+ view.setAlpha(1);
+ // Counteract the default slide transition
+ view.setTranslationX(pageWidth * -position);
+ // set Y position to swipe in from top
+ float yPosition = position * pageHeight;
+ view.setTranslationY(yPosition);
+ } else {
+ // This page is way off-screen to the right.
+ view.setAlpha(0);
+ }
+ }
}
}
diff --git a/src/com/android/deskclock/Voice.java b/src/com/android/deskclock/Voice.java
deleted file mode 100644
index f33bbe7..0000000
--- a/src/com/android/deskclock/Voice.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.VoiceInteractor;
-import android.os.Build;
-
-/**
- * Notifies Voice Interactor about whether the action
- * was successful. Voice Interactor is called only if
- * the build version is post-Lollipop.
- */
-public final class Voice {
-
- private static Delegate sDelegate = new VoiceInteractorDelegate();
-
- private Voice() { }
-
- public static void setDelegate(Delegate delegate) {
- sDelegate = delegate;
- }
-
- public static Delegate getDelegate() {
- return sDelegate;
- }
-
- public static void notifySuccess(Activity activity, String message) {
- if (Utils.isMOrLater()) {
- sDelegate.notifySuccess(activity.getVoiceInteractor(), message);
- }
- }
-
- public static void notifyFailure(Activity activity, String message) {
- if (Utils.isMOrLater()) {
- sDelegate.notifyFailure(activity.getVoiceInteractor(), message);
- }
- }
-
- public interface Delegate {
- void notifySuccess(VoiceInteractor vi, String message);
-
- void notifyFailure(VoiceInteractor vi, String message);
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- private static class VoiceInteractorDelegate implements Delegate {
- @Override
- public void notifySuccess(VoiceInteractor vi, String message) {
- if (vi != null) {
- final VoiceInteractor.Prompt prompt = new VoiceInteractor.Prompt(message);
- vi.submitRequest(new VoiceInteractor.CompleteVoiceRequest(prompt, null));
- }
- }
-
- @Override
- public void notifyFailure(VoiceInteractor vi, String message) {
- if (vi != null) {
- final VoiceInteractor.Prompt prompt = new VoiceInteractor.Prompt(message);
- vi.submitRequest(new VoiceInteractor.AbortVoiceRequest(prompt, null));
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmActivity.java b/src/com/android/deskclock/alarms/AlarmActivity.java
index 3ee1a25..a4321e4 100644
--- a/src/com/android/deskclock/alarms/AlarmActivity.java
+++ b/src/com/android/deskclock/alarms/AlarmActivity.java
@@ -33,13 +33,13 @@
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
+import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.v4.graphics.ColorUtils;
import android.support.v4.view.animation.PathInterpolatorCompat;
-import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
@@ -51,20 +51,22 @@
import android.widget.TextView;
import com.android.deskclock.AnimatorUtils;
+import com.android.deskclock.BaseActivity;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
+import com.android.deskclock.ThemeUtils;
import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
import com.android.deskclock.events.Events;
import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.settings.SettingsActivity;
-import com.android.deskclock.uidata.UiDataModel;
import com.android.deskclock.widget.CircleView;
import java.util.List;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
-public class AlarmActivity extends AppCompatActivity
+public class AlarmActivity extends BaseActivity
implements View.OnClickListener, View.OnTouchListener {
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmActivity");
@@ -125,7 +127,7 @@
private AlarmInstance mAlarmInstance;
private boolean mAlarmHandled;
- private String mVolumeBehavior;
+ private AlarmVolumeButtonBehavior mVolumeBehavior;
private int mCurrentHourColor;
private boolean mReceiverRegistered;
/** Whether the AlarmService is currently bound */
@@ -154,6 +156,7 @@
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setVolumeControlStream(AudioManager.STREAM_ALARM);
final long instanceId = AlarmInstance.getId(getIntent().getData());
mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId);
if (mAlarmInstance == null) {
@@ -170,9 +173,7 @@
LOGGER.i("Displaying alarm for instance: %s", mAlarmInstance);
// Get the volume/camera button behavior setting
- mVolumeBehavior = Utils.getDefaultSharedPreferences(this)
- .getString(SettingsActivity.KEY_VOLUME_BUTTONS,
- SettingsActivity.DEFAULT_VOLUME_BEHAVIOR);
+ mVolumeBehavior = DataModel.getDataModel().getAlarmVolumeButtonBehavior();
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
@@ -187,7 +188,7 @@
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
// Honor rotation on tablets; fix the orientation on phones.
- if (!getResources().getBoolean(R.bool.config_rotateAlarmAlert)) {
+ if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
}
@@ -210,9 +211,9 @@
final CircleView pulseView = (CircleView) mContentView.findViewById(R.id.pulse);
titleView.setText(mAlarmInstance.getLabelOrDefault(this));
- Utils.setTimeFormat(digitalClock);
+ Utils.setTimeFormat(digitalClock, false);
- mCurrentHourColor = UiDataModel.getUiDataModel().getWindowBackgroundColor();
+ mCurrentHourColor = ThemeUtils.resolveColor(this, android.R.attr.windowBackground);
getWindow().setBackgroundDrawable(new ColorDrawable(mCurrentHourColor));
mAlarmButton.setOnTouchListener(this);
@@ -285,31 +286,31 @@
// Do this in dispatch to intercept a few of the system keys.
LOGGER.v("dispatchKeyEvent: %s", keyEvent);
- switch (keyEvent.getKeyCode()) {
+ final int keyCode = keyEvent.getKeyCode();
+ switch (keyCode) {
// Volume keys and camera keys dismiss the alarm.
- case KeyEvent.KEYCODE_POWER:
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_MUTE:
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_CAMERA:
case KeyEvent.KEYCODE_FOCUS:
- if (!mAlarmHandled && keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ if (!mAlarmHandled) {
switch (mVolumeBehavior) {
- case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE:
- snooze();
- break;
- case SettingsActivity.VOLUME_BEHAVIOR_DISMISS:
- dismiss();
- break;
- default:
- break;
+ case SNOOZE:
+ if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ snooze();
+ }
+ return true;
+ case DISMISS:
+ if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ dismiss();
+ }
+ return true;
}
}
- return true;
- default:
- return super.dispatchKeyEvent(keyEvent);
}
+ return super.dispatchKeyEvent(keyEvent);
}
@Override
@@ -485,17 +486,17 @@
mAlarmHandled = true;
LOGGER.v("Snoozed: %s", mAlarmInstance);
- final int accentColor = Utils.obtainStyledColor(this, R.attr.colorAccent, Color.RED);
+ final int colorAccent = ThemeUtils.resolveColor(this, R.attr.colorAccent);
setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
- final int snoozeMinutes = AlarmStateManager.getSnoozedMinutes(this);
+ final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength();
final String infoText = getResources().getQuantityString(
R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes);
final String accessibilityText = getResources().getQuantityString(
R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes);
getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText,
- accessibilityText, accentColor, accentColor).start();
+ accessibilityText, colorAccent, colorAccent).start();
AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */);
diff --git a/src/com/android/deskclock/alarms/AlarmKlaxon.java b/src/com/android/deskclock/alarms/AlarmKlaxon.java
index cf2bd88..a162423 100644
--- a/src/com/android/deskclock/alarms/AlarmKlaxon.java
+++ b/src/com/android/deskclock/alarms/AlarmKlaxon.java
@@ -25,14 +25,15 @@
import com.android.deskclock.AsyncRingtonePlayer;
import com.android.deskclock.LogUtils;
import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.settings.SettingsActivity;
/**
- * Manages playing ringtone and vibrating the device.
+ * Manages playing alarm ringtones and vibrating the device.
*/
-public final class AlarmKlaxon {
- private static final long[] sVibratePattern = {500, 500};
+final class AlarmKlaxon {
+
+ private static final long[] VIBRATE_PATTERN = {500, 500};
private static boolean sStarted = false;
private static AsyncRingtonePlayer sAsyncRingtonePlayer;
@@ -54,7 +55,8 @@
LogUtils.v("AlarmKlaxon.start()");
if (!AlarmInstance.NO_RINGTONE_URI.equals(instance.mRingtone)) {
- getAsyncRingtonePlayer(context).play(instance.mRingtone);
+ final long crescendoDuration = DataModel.getDataModel().getAlarmCrescendoDuration();
+ getAsyncRingtonePlayer(context).play(instance.mRingtone, crescendoDuration);
}
if (instance.mVibrate) {
@@ -62,7 +64,7 @@
if (Utils.isLOrLater()) {
vibrateLOrLater(vibrator);
} else {
- vibrator.vibrate(sVibratePattern, 0);
+ vibrator.vibrate(VIBRATE_PATTERN, 0);
}
}
@@ -71,7 +73,7 @@
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static void vibrateLOrLater(Vibrator vibrator) {
- vibrator.vibrate(sVibratePattern, 0, new AudioAttributes.Builder()
+ vibrator.vibrate(VIBRATE_PATTERN, 0, new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build());
@@ -83,8 +85,7 @@
private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
if (sAsyncRingtonePlayer == null) {
- sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext(),
- SettingsActivity.KEY_ALARM_CRESCENDO);
+ sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
}
return sAsyncRingtonePlayer;
diff --git a/src/com/android/deskclock/alarms/AlarmNotifications.java b/src/com/android/deskclock/alarms/AlarmNotifications.java
index a8dcd08..df711d2 100644
--- a/src/com/android/deskclock/alarms/AlarmNotifications.java
+++ b/src/com/android/deskclock/alarms/AlarmNotifications.java
@@ -43,8 +43,8 @@
import java.util.Locale;
import java.util.Objects;
-public final class AlarmNotifications {
- public static final String EXTRA_NOTIFICATION_ID = "extra_notification_id";
+final class AlarmNotifications {
+ static final String EXTRA_NOTIFICATION_ID = "extra_notification_id";
/**
* Formats times such that chronological order and lexicographical order agree.
@@ -76,11 +76,17 @@
*/
private static final int ALARM_GROUP_MISSED_NOTIFICATION_ID = Integer.MAX_VALUE - 5;
- public static synchronized void showLowPriorityNotification(Context context,
+ /**
+ * This value is coordinated with notification ids from
+ * {@link com.android.deskclock.data.NotificationModel}
+ */
+ private static final int ALARM_FIRING_NOTIFICATION_ID = Integer.MAX_VALUE - 7;
+
+ static synchronized void showLowPriorityNotification(Context context,
AlarmInstance instance) {
LogUtils.v("Displaying low priority notification for alarm instance: " + instance.mId);
- NotificationCompat.Builder notification = new NotificationCompat.Builder(context)
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setShowWhen(false)
.setContentTitle(context.getString(
R.string.alarm_alert_predismiss_title))
@@ -95,39 +101,41 @@
.setLocalOnly(true);
if (Utils.isNOrLater()) {
- notification.setGroup(UPCOMING_GROUP_KEY);
+ builder.setGroup(UPCOMING_GROUP_KEY);
}
// Setup up hide notification
Intent hideIntent = AlarmStateManager.createStateChangeIntent(context,
AlarmStateManager.ALARM_DELETE_TAG, instance,
AlarmInstance.HIDE_NOTIFICATION_STATE);
- notification.setDeleteIntent(PendingIntent.getService(context, instance.hashCode(),
+ final int id = instance.hashCode();
+ builder.setDeleteIntent(PendingIntent.getService(context, id,
hideIntent, PendingIntent.FLAG_UPDATE_CURRENT));
// Setup up dismiss action
Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.PREDISMISSED_STATE);
- notification.addAction(R.drawable.ic_alarm_off_24dp,
+ builder.addAction(R.drawable.ic_alarm_off_24dp,
context.getString(R.string.alarm_alert_dismiss_text),
- PendingIntent.getService(context, instance.hashCode(),
+ PendingIntent.getService(context, id,
dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
// Setup content action if instance is owned by alarm
Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
- notification.setContentIntent(PendingIntent.getActivity(context, instance.hashCode(),
+ builder.setContentIntent(PendingIntent.getActivity(context, id,
viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- nm.notify(instance.hashCode(), notification.build());
- updateUpcomingAlarmGroupNotification(context);
+ final Notification notification = builder.build();
+ nm.notify(id, notification);
+ updateUpcomingAlarmGroupNotification(context, -1, notification);
}
- public static synchronized void showHighPriorityNotification(Context context,
+ static synchronized void showHighPriorityNotification(Context context,
AlarmInstance instance) {
LogUtils.v("Displaying high priority notification for alarm instance: " + instance.mId);
- NotificationCompat.Builder notification = new NotificationCompat.Builder(context)
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setShowWhen(false)
.setContentTitle(context.getString(R.string.alarm_alert_predismiss_title))
.setContentText(AlarmUtils.getAlarmText(context, instance, true /* includeLabel */))
@@ -141,25 +149,27 @@
.setLocalOnly(true);
if (Utils.isNOrLater()) {
- notification.setGroup(UPCOMING_GROUP_KEY);
+ builder.setGroup(UPCOMING_GROUP_KEY);
}
// Setup up dismiss action
Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.PREDISMISSED_STATE);
- notification.addAction(R.drawable.ic_alarm_off_24dp,
+ final int id = instance.hashCode();
+ builder.addAction(R.drawable.ic_alarm_off_24dp,
context.getString(R.string.alarm_alert_dismiss_text),
- PendingIntent.getService(context, instance.hashCode(),
+ PendingIntent.getService(context, id,
dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
// Setup content action if instance is owned by alarm
Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
- notification.setContentIntent(PendingIntent.getActivity(context, instance.hashCode(),
+ builder.setContentIntent(PendingIntent.getActivity(context, id,
viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- nm.notify(instance.hashCode(), notification.build());
- updateUpcomingAlarmGroupNotification(context);
+ final Notification notification = builder.build();
+ nm.notify(id, notification);
+ updateUpcomingAlarmGroupNotification(context, -1, notification);
}
@TargetApi(Build.VERSION_CODES.N)
@@ -167,15 +177,31 @@
return (n.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY;
}
+ /**
+ * Method which returns the first active notification for a given group. If a notification was
+ * just posted, provide it to make sure it is included as a potential result. If a notification
+ * was just canceled, provide the id so that it is not included as a potential result. These
+ * extra parameters are needed due to a race condition which exists in
+ * {@link NotificationManager#getActiveNotifications()}.
+ *
+ * @param context Context from which to grab the NotificationManager
+ * @param group The group key to query for notifications
+ * @param canceledNotificationId The id of the just-canceled notification (-1 if none)
+ * @param postedNotification The notification that was just posted
+ * @return The first active notification for the group
+ */
@TargetApi(Build.VERSION_CODES.N)
- private static Notification getFirstActiveNotification(Context context, String group) {
+ private static Notification getFirstActiveNotification(Context context, String group,
+ int canceledNotificationId, Notification postedNotification) {
final NotificationManager nm =
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
final StatusBarNotification[] notifications = nm.getActiveNotifications();
- Notification firstActiveNotification = null;
+ Notification firstActiveNotification = postedNotification;
for (StatusBarNotification statusBarNotification : notifications) {
final Notification n = statusBarNotification.getNotification();
- if (!isGroupSummary(n) && group.equals(n.getGroup())) {
+ if (!isGroupSummary(n)
+ && group.equals(n.getGroup())
+ && statusBarNotification.getId() != canceledNotificationId) {
if (firstActiveNotification == null
|| n.getSortKey().compareTo(firstActiveNotification.getSortKey()) < 0) {
firstActiveNotification = n;
@@ -199,14 +225,16 @@
return null;
}
- private static void updateUpcomingAlarmGroupNotification(Context context) {
+ private static void updateUpcomingAlarmGroupNotification(Context context,
+ int canceledNotificationId, Notification postedNotification) {
if (!Utils.isNOrLater()) {
return;
}
final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- final Notification firstUpcoming = getFirstActiveNotification(context, UPCOMING_GROUP_KEY);
+ final Notification firstUpcoming = getFirstActiveNotification(context, UPCOMING_GROUP_KEY,
+ canceledNotificationId, postedNotification);
if (firstUpcoming == null) {
nm.cancel(ALARM_GROUP_NOTIFICATION_ID);
return;
@@ -231,14 +259,16 @@
}
}
- private static void updateMissedAlarmGroupNotification(Context context) {
+ private static void updateMissedAlarmGroupNotification(Context context,
+ int canceledNotificationId, Notification postedNotification) {
if (!Utils.isNOrLater()) {
return;
}
final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- final Notification firstMissed = getFirstActiveNotification(context, MISSED_GROUP_KEY);
+ final Notification firstMissed = getFirstActiveNotification(context, MISSED_GROUP_KEY,
+ canceledNotificationId, postedNotification);
if (firstMissed == null) {
nm.cancel(ALARM_GROUP_MISSED_NOTIFICATION_ID);
return;
@@ -263,11 +293,11 @@
}
}
- public static synchronized void showSnoozeNotification(Context context,
+ static synchronized void showSnoozeNotification(Context context,
AlarmInstance instance) {
LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId);
- NotificationCompat.Builder notification = new NotificationCompat.Builder(context)
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setShowWhen(false)
.setContentTitle(instance.getLabelOrDefault(context))
.setContentText(context.getString(R.string.alarm_alert_snooze_until,
@@ -282,34 +312,36 @@
.setLocalOnly(true);
if (Utils.isNOrLater()) {
- notification.setGroup(UPCOMING_GROUP_KEY);
+ builder.setGroup(UPCOMING_GROUP_KEY);
}
// Setup up dismiss action
Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
- notification.addAction(R.drawable.ic_alarm_off_24dp,
+ final int id = instance.hashCode();
+ builder.addAction(R.drawable.ic_alarm_off_24dp,
context.getString(R.string.alarm_alert_dismiss_text),
- PendingIntent.getService(context, instance.hashCode(),
+ PendingIntent.getService(context, id,
dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
// Setup content action if instance is owned by alarm
Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
- notification.setContentIntent(PendingIntent.getActivity(context, instance.hashCode(),
+ builder.setContentIntent(PendingIntent.getActivity(context, id,
viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- nm.notify(instance.hashCode(), notification.build());
- updateUpcomingAlarmGroupNotification(context);
+ final Notification notification = builder.build();
+ nm.notify(id, notification);
+ updateUpcomingAlarmGroupNotification(context, -1, notification);
}
- public static synchronized void showMissedNotification(Context context,
+ static synchronized void showMissedNotification(Context context,
AlarmInstance instance) {
LogUtils.v("Displaying missed notification for alarm instance: " + instance.mId);
String label = instance.mLabel;
String alarmTime = AlarmUtils.getFormattedTime(context, instance.getAlarmTime());
- NotificationCompat.Builder notification = new NotificationCompat.Builder(context)
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
.setShowWhen(false)
.setContentTitle(context.getString(R.string.alarm_missed_title))
.setContentText(instance.mLabel.isEmpty() ? alarmTime :
@@ -323,31 +355,32 @@
.setLocalOnly(true);
if (Utils.isNOrLater()) {
- notification.setGroup(MISSED_GROUP_KEY);
+ builder.setGroup(MISSED_GROUP_KEY);
}
- final int hashCode = instance.hashCode();
+ final int id = instance.hashCode();
// Setup dismiss intent
Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
- notification.setDeleteIntent(PendingIntent.getService(context, hashCode,
+ builder.setDeleteIntent(PendingIntent.getService(context, id,
dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
// Setup content intent
Intent showAndDismiss = AlarmInstance.createIntent(context, AlarmStateManager.class,
instance.mId);
- showAndDismiss.putExtra(EXTRA_NOTIFICATION_ID, hashCode);
+ showAndDismiss.putExtra(EXTRA_NOTIFICATION_ID, id);
showAndDismiss.setAction(AlarmStateManager.SHOW_AND_DISMISS_ALARM_ACTION);
- notification.setContentIntent(PendingIntent.getBroadcast(context, hashCode,
+ builder.setContentIntent(PendingIntent.getBroadcast(context, id,
showAndDismiss, PendingIntent.FLAG_UPDATE_CURRENT));
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- nm.notify(hashCode, notification.build());
- updateMissedAlarmGroupNotification(context);
+ final Notification notification = builder.build();
+ nm.notify(id, notification);
+ updateMissedAlarmGroupNotification(context, -1, notification);
}
- public static synchronized void showAlarmNotification(Service service, AlarmInstance instance) {
+ static synchronized void showAlarmNotification(Service service, AlarmInstance instance) {
LogUtils.v("Displaying alarm notification for alarm instance: " + instance.mId);
Resources resources = service.getResources();
@@ -368,9 +401,8 @@
Intent snoozeIntent = AlarmStateManager.createStateChangeIntent(service,
AlarmStateManager.ALARM_SNOOZE_TAG, instance, AlarmInstance.SNOOZE_STATE);
snoozeIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true);
- PendingIntent snoozePendingIntent = PendingIntent.getService(service, instance.hashCode(),
- snoozeIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent snoozePendingIntent = PendingIntent.getService(service,
+ ALARM_FIRING_NOTIFICATION_ID, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
notification.addAction(R.drawable.ic_snooze_24dp,
resources.getString(R.string.alarm_alert_snooze_text), snoozePendingIntent);
@@ -379,7 +411,7 @@
AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
dismissIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true);
PendingIntent dismissPendingIntent = PendingIntent.getService(service,
- instance.hashCode(), dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ ALARM_FIRING_NOTIFICATION_ID, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT);
notification.addAction(R.drawable.ic_alarm_off_24dp,
resources.getString(R.string.alarm_alert_dismiss_text),
dismissPendingIntent);
@@ -388,7 +420,7 @@
Intent contentIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
instance.mId);
notification.setContentIntent(PendingIntent.getActivity(service,
- instance.hashCode(), contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
+ ALARM_FIRING_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
// Setup fullscreen intent
Intent fullScreenIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
@@ -398,25 +430,27 @@
fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_NO_USER_ACTION);
notification.setFullScreenIntent(PendingIntent.getActivity(service,
- instance.hashCode(), fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT), true);
+ ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT),
+ true);
notification.setPriority(NotificationCompat.PRIORITY_MAX);
clearNotification(service, instance);
- service.startForeground(instance.hashCode(), notification.build());
+ service.startForeground(ALARM_FIRING_NOTIFICATION_ID, notification.build());
}
- public static synchronized void clearNotification(Context context, AlarmInstance instance) {
+ static synchronized void clearNotification(Context context, AlarmInstance instance) {
LogUtils.v("Clearing notifications for alarm instance: " + instance.mId);
NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- nm.cancel(instance.hashCode());
- updateUpcomingAlarmGroupNotification(context);
- updateMissedAlarmGroupNotification(context);
+ final int id = instance.hashCode();
+ nm.cancel(id);
+ updateUpcomingAlarmGroupNotification(context, id, null);
+ updateMissedAlarmGroupNotification(context, id, null);
}
/**
* Updates the notification for an existing alarm. Use if the label has changed.
*/
- public static void updateNotification(Context context, AlarmInstance instance) {
+ static void updateNotification(Context context, AlarmInstance instance) {
switch (instance.mAlarmState) {
case AlarmInstance.LOW_NOTIFICATION_STATE:
showLowPriorityNotification(context, instance);
@@ -435,7 +469,7 @@
}
}
- public static Intent createViewAlarmIntent(Context context, AlarmInstance instance) {
+ static Intent createViewAlarmIntent(Context context, AlarmInstance instance) {
final long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
return Alarm.createIntent(context, DeskClock.class, alarmId)
.putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
diff --git a/src/com/android/deskclock/alarms/AlarmService.java b/src/com/android/deskclock/alarms/AlarmService.java
index cd1bec4..b9a97db 100644
--- a/src/com/android/deskclock/alarms/AlarmService.java
+++ b/src/com/android/deskclock/alarms/AlarmService.java
@@ -62,12 +62,15 @@
/** Private action used to stop an alarm with this service. */
public static final String STOP_ALARM_ACTION = "STOP_ALARM";
- /** Binder given to AlarmActivity */
+ /** Binder given to AlarmActivity. */
private final IBinder mBinder = new Binder();
/** Whether the service is currently bound to AlarmActivity */
private boolean mIsBound = false;
+ /** Listener for changes in phone state. */
+ private final PhoneStateChangeListener mPhoneStateListener = new PhoneStateChangeListener();
+
/** Whether the receiver is currently registered */
private boolean mIsRegistered = false;
@@ -99,23 +102,8 @@
}
private TelephonyManager mTelephonyManager;
- private int mInitialCallState;
private AlarmInstance mCurrentAlarm = null;
- private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
- @Override
- public void onCallStateChanged(int state, String ignored) {
- // The user might already be in a call when the alarm fires. When
- // we register onCallStateChanged, we get the initial in-call state
- // which kills the alarm. Check against the initial call state so
- // we don't kill the alarm during a call.
- if (state != TelephonyManager.CALL_STATE_IDLE && state != mInitialCallState) {
- startService(AlarmStateManager.createStateChangeIntent(AlarmService.this,
- "AlarmService", mCurrentAlarm, AlarmInstance.MISSED_STATE));
- }
- }
- };
-
private void startAlarm(AlarmInstance instance) {
LogUtils.v("AlarmService.start with instance: " + instance.mId);
if (mCurrentAlarm != null) {
@@ -127,8 +115,7 @@
mCurrentAlarm = instance;
AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
- mInitialCallState = mTelephonyManager.getCallState();
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+ mTelephonyManager.listen(mPhoneStateListener.init(), PhoneStateListener.LISTEN_CALL_STATE);
AlarmKlaxon.start(this, mCurrentAlarm);
sendBroadcast(new Intent(ALARM_ALERT_ACTION));
}
@@ -146,15 +133,7 @@
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
sendBroadcast(new Intent(ALARM_DONE_ACTION));
- // Since we use the same id for all notifications, the system has no way to distinguish the
- // firing notification we were bound to from other subsequent notifications posted for the
- // same AlarmInstance (e.g. after snoozing). We workaround the issue by forcing removal of
- // the notification and re-posting it.
stopForeground(true /* removeNotification */);
- mCurrentAlarm = AlarmInstance.getInstance(getContentResolver(), instanceId);
- if (mCurrentAlarm != null) {
- AlarmNotifications.updateNotification(this, mCurrentAlarm);
- }
mCurrentAlarm = null;
AlarmAlertWakeLock.releaseCpuLock();
@@ -207,6 +186,9 @@
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.v("AlarmService.onStartCommand() with %s", intent);
+ if (intent == null) {
+ return Service.START_NOT_STICKY;
+ }
final long instanceId = AlarmInstance.getId(intent.getData());
switch (intent.getAction()) {
@@ -260,4 +242,26 @@
mIsRegistered = false;
}
}
+
+ private final class PhoneStateChangeListener extends PhoneStateListener {
+
+ private int mPhoneCallState;
+
+ PhoneStateChangeListener init() {
+ mPhoneCallState = -1;
+ return this;
+ }
+
+ @Override
+ public void onCallStateChanged(int state, String ignored) {
+ if (mPhoneCallState == -1) {
+ mPhoneCallState = state;
+ }
+
+ if (state != TelephonyManager.CALL_STATE_IDLE && state != mPhoneCallState) {
+ startService(AlarmStateManager.createStateChangeIntent(AlarmService.this,
+ "AlarmService", mCurrentAlarm, AlarmInstance.MISSED_STATE));
+ }
+ }
+ }
}
diff --git a/src/com/android/deskclock/alarms/AlarmStateManager.java b/src/com/android/deskclock/alarms/AlarmStateManager.java
index 357dd0b..f0a8db5 100644
--- a/src/com/android/deskclock/alarms/AlarmStateManager.java
+++ b/src/com/android/deskclock/alarms/AlarmStateManager.java
@@ -17,12 +17,12 @@
import android.annotation.TargetApi;
import android.app.AlarmManager;
+import android.app.AlarmManager.AlarmClockInfo;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
@@ -40,16 +40,17 @@
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.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.settings.SettingsActivity;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import static android.content.Context.ALARM_SERVICE;
import static android.provider.Settings.System.NEXT_ALARM_FORMATTED;
/**
@@ -103,9 +104,6 @@
* parent to see if it should disable or schedule a new alarm instance.
*/
public final class AlarmStateManager extends BroadcastReceiver {
- // These defaults must match the values in res/xml/settings.xml
- private static final String DEFAULT_SNOOZE_MINUTES = "10";
-
// Intent action to trigger an instance state change.
public static final String CHANGE_STATE_ACTION = "change_state";
@@ -146,8 +144,9 @@
new AlarmManagerStateChangeScheduler();
private static Calendar getCurrentTime() {
- return sCurrentTimeFactory == null ?
- Calendar.getInstance() : sCurrentTimeFactory.getCurrentTime();
+ return sCurrentTimeFactory == null
+ ? DataModel.getDataModel().getCalendar()
+ : sCurrentTimeFactory.getCurrentTime();
}
static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) {
@@ -161,17 +160,6 @@
sStateChangeScheduler = stateChangeScheduler;
}
- public static int getGlobalIntentId(Context context) {
- SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
- return prefs.getInt(ALARM_GLOBAL_ID_EXTRA, -1);
- }
-
- public static void updateGlobalIntentId(Context context) {
- SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
- int globalId = prefs.getInt(ALARM_GLOBAL_ID_EXTRA, -1) + 1;
- prefs.edit().putInt(ALARM_GLOBAL_ID_EXTRA, globalId).commit();
- }
-
/**
* Update the next alarm stored in framework. This value is also displayed in digital widgets
* and the clock tab in this app.
@@ -241,14 +229,14 @@
// alarm that is going to fire next. The operation is constructed such that it is ignored
// by AlarmStateManager.
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(
- Context.ALARM_SERVICE);
+ final AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
- int flags = nextAlarm == null ? PendingIntent.FLAG_NO_CREATE : 0;
- PendingIntent operation = PendingIntent.getBroadcast(context, 0 /* requestCode */,
+ final int flags = nextAlarm == null ? PendingIntent.FLAG_NO_CREATE : 0;
+ final PendingIntent operation = PendingIntent.getBroadcast(context, 0 /* requestCode */,
AlarmStateManager.createIndicatorIntent(context), flags);
if (nextAlarm != null) {
+ LogUtils.i("Setting upcoming AlarmClockInfo for alarm: " + nextAlarm.mId);
long alarmTime = nextAlarm.getAlarmTime().getTimeInMillis();
// Create an intent that can be used to show or edit details of the next alarm.
@@ -256,10 +244,10 @@
AlarmNotifications.createViewAlarmIntent(context, nextAlarm),
PendingIntent.FLAG_UPDATE_CURRENT);
- AlarmManager.AlarmClockInfo info =
- new AlarmManager.AlarmClockInfo(alarmTime, viewIntent);
- alarmManager.setAlarmClock(info, operation);
+ final AlarmClockInfo info = new AlarmClockInfo(alarmTime, viewIntent);
+ Utils.updateNextAlarm(alarmManager, info, operation);
} else if (operation != null) {
+ LogUtils.i("Canceling upcoming AlarmClockInfo");
alarmManager.cancel(operation);
}
}
@@ -325,7 +313,7 @@
Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId);
intent.setAction(CHANGE_STATE_ACTION);
intent.addCategory(tag);
- intent.putExtra(ALARM_GLOBAL_ID_EXTRA, getGlobalIntentId(context));
+ intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.getDataModel().getGlobalIntentId());
if (state != null) {
intent.putExtra(ALARM_STATE_EXTRA, state.intValue());
}
@@ -469,7 +457,7 @@
Events.sendAlarmEvent(R.string.action_fire, 0);
- Calendar timeout = instance.getTimeout(context);
+ Calendar timeout = instance.getTimeout();
if (timeout != null) {
scheduleInstanceStateChange(context, timeout, instance, AlarmInstance.MISSED_STATE);
}
@@ -492,9 +480,7 @@
AlarmService.stopAlarm(context, instance);
// Calculate the new snooze alarm time
- String snoozeMinutesStr = Utils.getDefaultSharedPreferences(context)
- .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE_MINUTES);
- final int snoozeMinutes = Integer.parseInt(snoozeMinutesStr);
+ final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength();
Calendar newAlarmTime = Calendar.getInstance();
newAlarmTime.add(Calendar.MINUTE, snoozeMinutes);
@@ -529,12 +515,6 @@
updateNextAlarm(context);
}
- public static int getSnoozedMinutes(Context context) {
- final String snoozeMinutesStr = Utils.getDefaultSharedPreferences(context)
- .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE_MINUTES);
- return Integer.parseInt(snoozeMinutesStr);
- }
-
/**
* This will set the alarm instance to the MISSED_STATE and update
* the application notifications and schedule any state changes that need
@@ -672,11 +652,12 @@
*/
public static void registerInstance(Context context, AlarmInstance instance,
boolean updateNextAlarm) {
+ LogUtils.i("Registering instance: " + instance.mId);
final ContentResolver cr = context.getContentResolver();
final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
final Calendar currentTime = getCurrentTime();
final Calendar alarmTime = instance.getAlarmTime();
- final Calendar timeoutTime = instance.getTimeout(context);
+ final Calendar timeoutTime = instance.getTimeout();
final Calendar lowNotificationTime = instance.getLowNotificationTime();
final Calendar highNotificationTime = instance.getHighNotificationTime();
final Calendar missedTTL = instance.getMissedTimeToLive();
@@ -772,6 +753,7 @@
* @param alarmId to find instances to delete.
*/
public static void deleteAllInstances(Context context, long alarmId) {
+ LogUtils.i("Deleting all instances of alarm: " + alarmId);
ContentResolver cr = context.getContentResolver();
List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId);
for (AlarmInstance instance : instances) {
@@ -786,6 +768,7 @@
* is modified superficially (label, vibrate, or ringtone change).
*/
public static void deleteNonSnoozeInstances(Context context, long alarmId) {
+ LogUtils.i("Deleting all non-snooze instances of alarm: " + alarmId);
ContentResolver cr = context.getContentResolver();
List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId);
for (AlarmInstance instance : instances) {
@@ -804,6 +787,7 @@
* @param context application context
*/
public static void fixAlarmInstances(Context context) {
+ LogUtils.i("Fixing alarm instances");
// Register all instances after major time changes or when phone restarts
final ContentResolver contentResolver = context.getContentResolver();
final Calendar currentTime = getCurrentTime();
@@ -925,7 +909,7 @@
return;
}
- int globalId = getGlobalIntentId(context);
+ int globalId = DataModel.getDataModel().getGlobalIntentId();
int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1);
int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1);
if (intentId != globalId) {
@@ -1025,7 +1009,7 @@
PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ final AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
if (Utils.isMOrLater()) {
// Ensure the alarm fires even if the device is dozing.
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
@@ -1044,7 +1028,7 @@
PendingIntent.FLAG_NO_CREATE);
if (pendingIntent != null) {
- AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
am.cancel(pendingIntent);
pendingIntent.cancel();
}
diff --git a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
index f799c0d..6c94649 100644
--- a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
+++ b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
@@ -17,22 +17,22 @@
package com.android.deskclock.alarms;
import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
import android.content.Context;
import android.content.Intent;
-import android.media.RingtoneManager;
import android.os.Bundle;
import android.os.Vibrator;
-import android.text.format.DateFormat;
+import com.android.deskclock.AlarmClockFragment;
import com.android.deskclock.LabelDialogFragment;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
-import com.android.deskclock.RingtonePickerDialogFragment;
-import com.android.deskclock.alarms.utils.DayOrderUtils;
+import com.android.deskclock.alarms.dataadapter.AlarmItemHolder;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Weekdays;
+import com.android.deskclock.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
+import com.android.deskclock.ringtone.RingtonePickerActivity;
import java.util.Calendar;
@@ -44,19 +44,19 @@
private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmTimeClickHandler");
private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap";
- private static final String RINGTONE_PICKER_FRAG_TAG = "ringtone_picker_dialog";
private final Fragment mFragment;
+ private final Context mContext;
private final AlarmUpdateHandler mAlarmUpdateHandler;
private final ScrollHandler mScrollHandler;
private Alarm mSelectedAlarm;
private Bundle mPreviousDaysOfWeekMap;
- private int[] mDayOrder;
public AlarmTimeClickHandler(Fragment fragment, Bundle savedState,
AlarmUpdateHandler alarmUpdateHandler, ScrollHandler smoothScrollController) {
mFragment = fragment;
+ mContext = mFragment.getActivity().getApplicationContext();
mAlarmUpdateHandler = alarmUpdateHandler;
mScrollHandler = smoothScrollController;
if (savedState != null) {
@@ -65,11 +65,6 @@
if (mPreviousDaysOfWeekMap == null) {
mPreviousDaysOfWeekMap = new Bundle();
}
- mDayOrder = DayOrderUtils.getDayOrder(fragment.getActivity());
- }
-
- public Alarm getSelectedAlarm() {
- return mSelectedAlarm;
}
public void setSelectedAlarm(Alarm selectedAlarm) {
@@ -83,6 +78,8 @@
public void setAlarmEnabled(Alarm alarm, boolean newState) {
if (newState != alarm.enabled) {
alarm.enabled = newState;
+ Events.sendAlarmEvent(newState ? R.string.action_enable : R.string.action_disable,
+ R.string.label_deskclock);
mAlarmUpdateHandler.asyncUpdateAlarm(alarm, alarm.enabled, false);
LOGGER.d("Updating alarm enabled state to " + newState);
}
@@ -91,13 +88,13 @@
public void setAlarmVibrationEnabled(Alarm alarm, boolean newState) {
if (newState != alarm.vibrate) {
alarm.vibrate = newState;
+ Events.sendAlarmEvent(R.string.action_toggle_vibrate, R.string.label_deskclock);
mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true);
LOGGER.d("Updating vibrate state to " + newState);
if (newState) {
// Buzz the vibrator to preview the alarm firing behavior.
- final Context context = mFragment.getActivity();
- final Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ final Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
if (v.hasVibrator()) {
v.vibrate(300);
}
@@ -114,92 +111,81 @@
// or
// Set all days if no previous.
final int bitSet = mPreviousDaysOfWeekMap.getInt(alarmId);
- alarm.daysOfWeek.setBitSet(bitSet);
+ alarm.daysOfWeek = Weekdays.fromBits(bitSet);
if (!alarm.daysOfWeek.isRepeating()) {
- alarm.daysOfWeek.setDaysOfWeek(true, mDayOrder);
+ alarm.daysOfWeek = Weekdays.ALL;
}
} else {
// Remember the set days in case the user wants it back.
- final int bitSet = alarm.daysOfWeek.getBitSet();
+ final int bitSet = alarm.daysOfWeek.getBits();
mPreviousDaysOfWeekMap.putInt(alarmId, bitSet);
// Remove all repeat days
- alarm.daysOfWeek.clearAllDays();
+ alarm.daysOfWeek = Weekdays.NONE;
}
// if the change altered the next scheduled alarm time, tell the user
final Calendar newNextAlarmTime = alarm.getNextAlarmTime(now);
final boolean popupToast = !oldNextAlarmTime.equals(newNextAlarmTime);
+ Events.sendAlarmEvent(R.string.action_toggle_repeat_days, R.string.label_deskclock);
mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, false);
}
public void setDayOfWeekEnabled(Alarm alarm, boolean checked, int index) {
final Calendar now = Calendar.getInstance();
final Calendar oldNextAlarmTime = alarm.getNextAlarmTime(now);
- alarm.daysOfWeek.setDaysOfWeek(checked, mDayOrder[index]);
+
+ final int weekday = DataModel.getDataModel().getWeekdayOrder().getCalendarDays().get(index);
+ alarm.daysOfWeek = alarm.daysOfWeek.setBit(weekday, checked);
+
// if the change altered the next scheduled alarm time, tell the user
final Calendar newNextAlarmTime = alarm.getNextAlarmTime(now);
final boolean popupToast = !oldNextAlarmTime.equals(newNextAlarmTime);
mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, false);
}
- public void onDeleteClicked(Alarm alarm) {
+ public void onDeleteClicked(AlarmItemHolder itemHolder) {
+ if (mFragment instanceof AlarmClockFragment) {
+ ((AlarmClockFragment) mFragment).removeItem(itemHolder);
+ }
+ final Alarm alarm = itemHolder.item;
+ Events.sendAlarmEvent(R.string.action_delete, R.string.label_deskclock);
mAlarmUpdateHandler.asyncDeleteAlarm(alarm);
LOGGER.d("Deleting alarm.");
}
public void onClockClicked(Alarm alarm) {
mSelectedAlarm = alarm;
- TimePickerCompat.showTimeEditDialog(mFragment, alarm,
- DateFormat.is24HourFormat(mFragment.getActivity()));
+ Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock);
+ TimePickerDialogFragment.show(mFragment, alarm.hour, alarm.minutes);
}
public void dismissAlarmInstance(AlarmInstance alarmInstance) {
- final Context context = mFragment.getActivity().getApplicationContext();
final Intent dismissIntent = AlarmStateManager.createStateChangeIntent(
- context, AlarmStateManager.ALARM_DISMISS_TAG, alarmInstance,
+ mContext, AlarmStateManager.ALARM_DISMISS_TAG, alarmInstance,
AlarmInstance.PREDISMISSED_STATE);
- context.startService(dismissIntent);
+ mContext.startService(dismissIntent);
mAlarmUpdateHandler.showPredismissToast(alarmInstance);
}
- public void onRingtoneClicked(Alarm alarm) {
+ public void onRingtoneClicked(Context context, Alarm alarm) {
mSelectedAlarm = alarm;
- final FragmentManager fragmentManager = mFragment.getChildFragmentManager();
- fragmentManager.executePendingTransactions();
- final FragmentTransaction ft = fragmentManager.beginTransaction();
- final Fragment prev = fragmentManager.findFragmentByTag(RINGTONE_PICKER_FRAG_TAG);
- if (prev != null) {
- ft.remove(prev);
- }
- ft.addToBackStack(null);
+ Events.sendAlarmEvent(R.string.action_set_ringtone, R.string.label_deskclock);
- new RingtonePickerDialogFragment.Builder()
- .setTitle(R.string.alert)
- .setDefaultRingtoneTitle(R.string.default_alarm_ringtone_title)
- .setDefaultRingtoneUri(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
- .setExistingRingtoneUri(alarm.alert)
- .show(ft, RINGTONE_PICKER_FRAG_TAG);
+ final Intent intent =
+ RingtonePickerActivity.createAlarmRingtonePickerIntent(context, alarm);
+ context.startActivity(intent);
}
public void onEditLabelClicked(Alarm alarm) {
- final FragmentManager fragmentManager = mFragment.getChildFragmentManager();
- fragmentManager.executePendingTransactions();
- final FragmentTransaction ft = fragmentManager.beginTransaction();
- final Fragment prev = fragmentManager.findFragmentByTag("label_dialog");
- if (prev != null) {
- ft.remove(prev);
- }
- ft.addToBackStack(null);
-
- // Create and show the dialog.
- final LabelDialogFragment newFragment =
+ Events.sendAlarmEvent(R.string.action_set_label, R.string.label_deskclock);
+ final LabelDialogFragment fragment =
LabelDialogFragment.newInstance(alarm, alarm.label, mFragment.getTag());
- newFragment.show(ft, "label_dialog");
+ LabelDialogFragment.show(mFragment.getFragmentManager(), fragment);
}
- public void processTimeSet(int hourOfDay, int minute) {
+ public void onTimeSet(int hourOfDay, int minute) {
if (mSelectedAlarm == null) {
// If mSelectedAlarm is null then we're creating a new alarm.
final Alarm a = new Alarm();
@@ -216,4 +202,4 @@
mSelectedAlarm = null;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmUpdateHandler.java b/src/com/android/deskclock/alarms/AlarmUpdateHandler.java
index 12c7ed2..2a25350 100644
--- a/src/com/android/deskclock/alarms/AlarmUpdateHandler.java
+++ b/src/com/android/deskclock/alarms/AlarmUpdateHandler.java
@@ -105,7 +105,6 @@
new AsyncTask<Void, Void, AlarmInstance>() {
@Override
protected AlarmInstance doInBackground(Void... parameters) {
- Events.sendAlarmEvent(R.string.action_update, R.string.label_deskclock);
ContentResolver cr = mAppContext.getContentResolver();
// Update alarm
@@ -163,7 +162,6 @@
// Nothing to do here, just return.
return false;
}
- Events.sendAlarmEvent(R.string.action_delete, R.string.label_deskclock);
AlarmStateManager.deleteAllInstances(mAppContext, alarm.id);
return Alarm.deleteAlarm(mAppContext.getContentResolver(), alarm.id);
}
diff --git a/src/com/android/deskclock/alarms/TimePickerCompat.java b/src/com/android/deskclock/alarms/TimePickerCompat.java
deleted file mode 100644
index 59da1e5..0000000
--- a/src/com/android/deskclock/alarms/TimePickerCompat.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * 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.alarms;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.os.Bundle;
-import android.text.format.DateFormat;
-import android.widget.TimePicker;
-
-import com.android.datetimepicker.time.RadialPickerLayout;
-import com.android.datetimepicker.time.TimePickerDialog;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.provider.Alarm;
-
-import java.util.Calendar;
-
-/**
- * Displays and handles callback for time picker UI.
- */
-public final class TimePickerCompat {
-
- /**
- * Callback when a valid time is selected from UI.
- */
- public interface OnTimeSetListener {
- void processTimeSet(int hourOfDay, int minute);
- }
-
- // Tag for timer picker fragment in FragmentManager.
- private static final String FRAG_TAG_TIME_PICKER = "time_dialog";
-
- // Do not instantiate.
- private TimePickerCompat() {}
-
- /**
- * Pre-L implementation of timer picker UI.
- */
- public static class TimerPickerPreL extends TimePickerDialog {
-
- public static TimerPickerPreL newInstance(final Fragment targetFragment, final int hour,
- final int minutes, final boolean use24hourFormat) {
- final TimerPickerPreL dialog = new TimerPickerPreL();
- dialog.initialize(new OnTimeSetListener() {
- @Override
- public void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute) {
- ((TimePickerCompat.OnTimeSetListener) targetFragment)
- .processTimeSet(hourOfDay, minute);
- }
- }, hour, minutes, use24hourFormat);
- dialog.setTargetFragment(targetFragment, 0);
- dialog.setThemeDark(true);
- return dialog;
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- if (getTargetFragment() instanceof TimePickerCompat.OnTimeSetListener) {
- setOnTimeSetListener(new OnTimeSetListener() {
- @Override
- public void onTimeSet(RadialPickerLayout view, int hourOfDay, int minute) {
- ((TimePickerCompat.OnTimeSetListener) getTargetFragment())
- .processTimeSet(hourOfDay, minute);
- }
- });
- }
- }
- }
-
- /**
- * Post-L implmenetation of timer picker UI.
- */
- public static class TimePickerPostL extends DialogFragment {
-
- private Alarm mAlarm;
- private android.app.TimePickerDialog.OnTimeSetListener mListener;
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final int hour, minute;
- if (mAlarm == null) {
- final Calendar c = Calendar.getInstance();
- hour = c.get(Calendar.HOUR_OF_DAY);
- minute = c.get(Calendar.MINUTE);
- } else {
- hour = mAlarm.hour;
- minute = mAlarm.minutes;
- }
-
- return new android.app.TimePickerDialog(getActivity(), R.style.TimePickerTheme,
- mListener, hour, minute, DateFormat.is24HourFormat(getActivity()));
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- if (getTargetFragment() instanceof OnTimeSetListener) {
- setOnTimeSetListener((OnTimeSetListener) getTargetFragment());
- }
- }
-
- public void setOnTimeSetListener(final OnTimeSetListener listener) {
- mListener = new android.app.TimePickerDialog.OnTimeSetListener() {
- @Override
- public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
- listener.processTimeSet(hourOfDay, minute);
- }
- };
- }
-
- public void setAlarm(Alarm alarm) {
- mAlarm = alarm;
- }
- }
-
- /**
- * Show the time picker dialog for post-L devices.
- * This is called from AlarmClockFragment to set alarm.
- *
- * @param targetFragment The calling fragment (which is also a {@link OnTimeSetListener}),
- * we use it as the target fragment of the time picker fragment, so later
- * the latter can retrieve it and set it as its onTimeSetListener when
- * the fragment is recreated.
- * @param alarm The clicked alarm, it can be null if user was clicking the fab.
- * @param use24hourFormat Whether or not the time picker should use 24-hour format if supported.
- */
- public static void showTimeEditDialog(Fragment targetFragment, Alarm alarm,
- boolean use24hourFormat) {
- final FragmentManager manager = targetFragment.getFragmentManager();
- // Make sure the dialog isn't already added.
- removeTimeEditDialog(manager);
-
- if (Utils.isLOrLater()) {
- final TimePickerPostL picker = new TimePickerPostL();
- picker.setTargetFragment(targetFragment, 0);
- picker.setOnTimeSetListener((OnTimeSetListener) targetFragment);
- picker.setAlarm(alarm);
- picker.show(manager, FRAG_TAG_TIME_PICKER);
- } else {
- final int hour, minutes;
- if (alarm == null) {
- hour = 0;
- minutes = 0;
- } else {
- hour = alarm.hour;
- minutes = alarm.minutes;
- }
- TimerPickerPreL picker = TimerPickerPreL.newInstance(targetFragment, hour,
- minutes, use24hourFormat);
- if (!picker.isAdded()) {
- picker.show(manager, FRAG_TAG_TIME_PICKER);
- }
- }
- }
-
- public static void removeTimeEditDialog(FragmentManager manager) {
- if (manager == null) {
- return;
- }
- final Fragment prev = manager.findFragmentByTag(FRAG_TAG_TIME_PICKER);
- if (prev != null) {
- manager.beginTransaction().remove(prev).commit();
- }
- }
-
-}
diff --git a/src/com/android/deskclock/alarms/TimePickerDialogFragment.java b/src/com/android/deskclock/alarms/TimePickerDialogFragment.java
new file mode 100644
index 0000000..5301529
--- /dev/null
+++ b/src/com/android/deskclock/alarms/TimePickerDialogFragment.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016 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.alarms;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.TimePickerDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.text.format.DateFormat;
+import android.widget.TimePicker;
+
+import com.android.deskclock.Utils;
+
+import java.util.Calendar;
+
+/**
+ * DialogFragment used to show TimePicker.
+ */
+public class TimePickerDialogFragment extends DialogFragment {
+
+ /**
+ * Tag for timer picker fragment in FragmentManager.
+ */
+ private static final String TAG = "TimePickerDialogFragment";
+
+ private static final String ARG_HOUR = TAG + "_hour";
+ private static final String ARG_MINUTE = TAG + "_minute";
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final OnTimeSetListener listener = ((OnTimeSetListener) getParentFragment());
+
+ final Calendar now = Calendar.getInstance();
+ final Bundle args = getArguments() == null ? Bundle.EMPTY : getArguments();
+ final int hour = args.getInt(ARG_HOUR, now.get(Calendar.HOUR_OF_DAY));
+ final int minute = args.getInt(ARG_MINUTE, now.get(Calendar.MINUTE));
+
+ if (Utils.isLOrLater()) {
+ final Context context = getActivity();
+ return new TimePickerDialog(context, new TimePickerDialog.OnTimeSetListener() {
+ @Override
+ public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
+ listener.onTimeSet(TimePickerDialogFragment.this, hourOfDay, minute);
+ }
+ }, hour, minute, DateFormat.is24HourFormat(context));
+ } else {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ final Context context = builder.getContext();
+
+ final TimePicker timePicker = new TimePicker(context);
+ timePicker.setCurrentHour(hour);
+ timePicker.setCurrentMinute(minute);
+ timePicker.setIs24HourView(DateFormat.is24HourFormat(context));
+
+ return builder.setView(timePicker)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ listener.onTimeSet(TimePickerDialogFragment.this,
+ timePicker.getCurrentHour(), timePicker.getCurrentMinute());
+ }
+ }).setNegativeButton(android.R.string.cancel, null /* listener */)
+ .create();
+ }
+ }
+
+ public static void show(Fragment fragment) {
+ show(fragment, -1 /* hour */, -1 /* minute */);
+ }
+
+ public static void show(Fragment parentFragment, int hourOfDay, int minute) {
+ if (!(parentFragment instanceof OnTimeSetListener)) {
+ throw new IllegalArgumentException("Fragment must implement OnTimeSetListener");
+ }
+
+ final FragmentManager manager = parentFragment.getChildFragmentManager();
+ if (manager == null || manager.isDestroyed()) {
+ return;
+ }
+
+ // Make sure the dialog isn't already added.
+ removeTimeEditDialog(manager);
+
+ final TimePickerDialogFragment fragment = new TimePickerDialogFragment();
+
+ final Bundle args = new Bundle();
+ if (hourOfDay >= 0 && hourOfDay < 24) {
+ args.putInt(ARG_HOUR, hourOfDay);
+ }
+ if (minute >= 0 && minute < 60) {
+ args.putInt(ARG_MINUTE, minute);
+ }
+
+ fragment.setArguments(args);
+ fragment.show(manager, TAG);
+ }
+
+ public static void removeTimeEditDialog(FragmentManager manager) {
+ if (manager != null) {
+ final Fragment prev = manager.findFragmentByTag(TAG);
+ if (prev != null) {
+ manager.beginTransaction().remove(prev).commit();
+ }
+ }
+ }
+
+ /**
+ * The callback interface used to indicate the user is done filling in the time (e.g. they
+ * clicked on the 'OK' button).
+ */
+ public interface OnTimeSetListener {
+ /**
+ * Called when the user is done setting a new time and the dialog has closed.
+ *
+ * @param fragment the fragment associated with this listener
+ * @param hourOfDay the hour that was set
+ * @param minute the minute that was set
+ */
+ void onTimeSet(TimePickerDialogFragment fragment, int hourOfDay, int minute);
+ }
+}
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java
index d34faba..d9c4986 100644
--- a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java
+++ b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java
@@ -19,10 +19,12 @@
import android.content.Context;
import android.view.View;
import android.widget.CompoundButton;
+import android.widget.ImageView;
import android.widget.TextView;
import com.android.deskclock.AlarmUtils;
import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.ItemAnimator;
import com.android.deskclock.R;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
@@ -31,15 +33,25 @@
/**
* Abstract ViewHolder for alarm time items.
*/
-public abstract class AlarmItemViewHolder extends ItemAdapter.ItemViewHolder<AlarmItemHolder> {
+public abstract class AlarmItemViewHolder extends ItemAdapter.ItemViewHolder<AlarmItemHolder>
+ implements ItemAnimator.OnAnimateChangeListener {
private static final float CLOCK_ENABLED_ALPHA = 1f;
private static final float CLOCK_DISABLED_ALPHA = 0.69f;
+ public static final float ANIM_STANDARD_DELAY_MULTIPLIER = 1f / 6f;
+ public static final float ANIM_LONG_DURATION_MULTIPLIER = 2f / 3f;
+ public static final float ANIM_SHORT_DURATION_MULTIPLIER = 1f / 4f;
+ public static final float ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER =
+ 1f - ANIM_LONG_DURATION_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER;
+ public static final float ANIM_LONG_DELAY_INCREMENT_MULTIPLIER =
+ 1f - ANIM_STANDARD_DELAY_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER;
+
+ public static final String ANIMATE_REPEAT_DAYS = "ANIMATE_REPEAT_DAYS";
+
public final TextTime clock;
public final CompoundButton onOff;
- public final View arrow;
- public final View preemptiveDismissContainer;
+ public final ImageView arrow;
public final TextView preemptiveDismissButton;
public AlarmItemViewHolder(View itemView) {
@@ -47,8 +59,7 @@
clock = (TextTime) itemView.findViewById(R.id.digital_clock);
onOff = (CompoundButton) itemView.findViewById(R.id.onoff);
- arrow = itemView.findViewById(R.id.arrow);
- preemptiveDismissContainer = itemView.findViewById(R.id.preemptive_dismiss_container);
+ arrow = (ImageView) itemView.findViewById(R.id.arrow);
preemptiveDismissButton =
(TextView) itemView.findViewById(R.id.preemptive_dismiss_button);
preemptiveDismissButton.setOnClickListener(new View.OnClickListener() {
@@ -93,7 +104,7 @@
AlarmInstance alarmInstance) {
final boolean canBind = alarm.canPreemptivelyDismiss() && alarmInstance != null;
if (canBind) {
- preemptiveDismissContainer.setVisibility(View.VISIBLE);
+ preemptiveDismissButton.setVisibility(View.VISIBLE);
final String dismissText = alarm.instanceState == AlarmInstance.SNOOZE_STATE
? context.getString(R.string.alarm_alert_snooze_until,
AlarmUtils.getAlarmText(context, alarmInstance, false))
@@ -101,7 +112,7 @@
preemptiveDismissButton.setText(dismissText);
preemptiveDismissButton.setClickable(true);
} else {
- preemptiveDismissContainer.setVisibility(View.GONE);
+ preemptiveDismissButton.setVisibility(View.GONE);
preemptiveDismissButton.setClickable(false);
}
return canBind;
diff --git a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java
index 6b7265e..dfbfc43 100644
--- a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java
+++ b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java
@@ -16,20 +16,29 @@
package com.android.deskclock.alarms.dataadapter;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
import android.content.Context;
-import android.text.TextUtils;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import com.android.deskclock.AnimatorUtils;
import com.android.deskclock.ItemAdapter;
import com.android.deskclock.R;
-import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Weekdays;
+import com.android.deskclock.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
import java.util.Calendar;
+import java.util.List;
/**
* A ViewHolder containing views for an alarm item in collapsed stated.
@@ -38,13 +47,14 @@
public static final int VIEW_TYPE = R.layout.alarm_time_collapsed;
- public final TextView alarmLabel;
+ private final TextView alarmLabel;
public final TextView daysOfWeek;
- public final TextView upcomingInstanceLabel;
- public final View hairLine;
+ private final TextView upcomingInstanceLabel;
+ private final View hairLine;
- public CollapsedAlarmViewHolder(View itemView) {
+ private CollapsedAlarmViewHolder(View itemView) {
super(itemView);
+
alarmLabel = (TextView) itemView.findViewById(R.id.label);
daysOfWeek = (TextView) itemView.findViewById(R.id.days_of_week);
upcomingInstanceLabel = (TextView) itemView.findViewById(R.id.upcoming_instance_label);
@@ -54,18 +64,21 @@
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
+ Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock);
getItemHolder().expand();
}
});
alarmLabel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
+ Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock);
getItemHolder().expand();
}
});
arrow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
+ Events.sendAlarmEvent(R.string.action_expand, R.string.label_deskclock);
getItemHolder().expand();
}
});
@@ -74,9 +87,12 @@
@Override
public void onClick(View v) {
getItemHolder().getAlarmTimeClickHandler().onClockClicked(getItemHolder().item);
+ Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock);
getItemHolder().expand();
}
});
+
+ itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
@Override
@@ -88,9 +104,7 @@
bindRepeatText(context, alarm);
bindReadOnlyLabel(context, alarm);
bindUpcomingInstance(context, alarm);
- boolean boundPreemptiveDismiss =
- bindPreemptiveDismissButton(context, alarm, alarmInstance);
- hairLine.setVisibility(boundPreemptiveDismiss ? View.GONE : View.VISIBLE);
+ bindPreemptiveDismissButton(context, alarm, alarmInstance);
}
private void bindReadOnlyLabel(Context context, Alarm alarm) {
@@ -105,12 +119,14 @@
}
private void bindRepeatText(Context context, Alarm alarm) {
- final String daysOfWeekText =
- alarm.daysOfWeek.toString(context, Utils.getFirstDayOfWeek(context));
- if (!TextUtils.isEmpty(daysOfWeekText)) {
+ if (alarm.daysOfWeek.isRepeating()) {
+ final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
+ final String daysOfWeekText = alarm.daysOfWeek.toString(context, weekdayOrder);
daysOfWeek.setText(daysOfWeekText);
- daysOfWeek.setContentDescription(alarm.daysOfWeek.toAccessibilityString(
- context, Utils.getFirstDayOfWeek(context)));
+
+ final String string = alarm.daysOfWeek.toAccessibilityString(context, weekdayOrder);
+ daysOfWeek.setContentDescription(string);
+
daysOfWeek.setVisibility(View.VISIBLE);
} else {
daysOfWeek.setVisibility(View.GONE);
@@ -129,8 +145,119 @@
}
}
- public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
+ @Override
+ public Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
+ int fromBottom, long duration) {
+ /* There are no possible partial animations for collapsed view holders. */
+ return null;
+ }
+ @Override
+ public Animator onAnimateChange(final ViewHolder oldHolder, ViewHolder newHolder,
+ long duration) {
+ if (!(oldHolder instanceof AlarmItemViewHolder)
+ || !(newHolder instanceof AlarmItemViewHolder)) {
+ return null;
+ }
+
+ final boolean isCollapsing = this == newHolder;
+ setChangingViewsAlpha(isCollapsing ? 0f : 1f);
+
+ final Animator changeAnimatorSet = isCollapsing
+ ? createCollapsingAnimator((AlarmItemViewHolder) oldHolder, duration)
+ : createExpandingAnimator((AlarmItemViewHolder) newHolder, duration);
+ changeAnimatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ clock.setVisibility(View.VISIBLE);
+ onOff.setVisibility(View.VISIBLE);
+ arrow.setVisibility(View.VISIBLE);
+ arrow.setTranslationY(0f);
+ setChangingViewsAlpha(1f);
+ arrow.jumpDrawablesToCurrentState();
+ }
+ });
+ return changeAnimatorSet;
+ }
+
+ private Animator createExpandingAnimator(AlarmItemViewHolder newHolder, long duration) {
+ clock.setVisibility(View.INVISIBLE);
+ onOff.setVisibility(View.INVISIBLE);
+ arrow.setVisibility(View.INVISIBLE);
+
+ final AnimatorSet alphaAnimatorSet = new AnimatorSet();
+ alphaAnimatorSet.playTogether(
+ ObjectAnimator.ofFloat(alarmLabel, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(daysOfWeek, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(upcomingInstanceLabel, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f));
+ alphaAnimatorSet.setDuration((long) (duration * ANIM_SHORT_DURATION_MULTIPLIER));
+
+ final View oldView = itemView;
+ final View newView = newHolder.itemView;
+ final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView)
+ .setDuration(duration);
+ boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(alphaAnimatorSet, boundsAnimator);
+ return animatorSet;
+ }
+
+ private Animator createCollapsingAnimator(AlarmItemViewHolder oldHolder, long duration) {
+ final AnimatorSet alphaAnimatorSet = new AnimatorSet();
+ alphaAnimatorSet.playTogether(
+ ObjectAnimator.ofFloat(alarmLabel, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(daysOfWeek, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(upcomingInstanceLabel, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f));
+ final long standardDelay = (long) (duration * ANIM_STANDARD_DELAY_MULTIPLIER);
+ alphaAnimatorSet.setDuration(standardDelay);
+ alphaAnimatorSet.setStartDelay(duration - standardDelay);
+
+ final View oldView = oldHolder.itemView;
+ final View newView = itemView;
+ final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView)
+ .setDuration(duration);
+ boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ final View oldArrow = oldHolder.arrow;
+ final Rect oldArrowRect = new Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight());
+ final Rect newArrowRect = new Rect(0, 0, arrow.getWidth(), arrow.getHeight());
+ ((ViewGroup) newView).offsetDescendantRectToMyCoords(arrow, newArrowRect);
+ ((ViewGroup) oldView).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect);
+ final float arrowTranslationY = oldArrowRect.bottom - newArrowRect.bottom;
+ arrow.setTranslationY(arrowTranslationY);
+ arrow.setVisibility(View.VISIBLE);
+ clock.setVisibility(View.VISIBLE);
+ onOff.setVisibility(View.VISIBLE);
+
+ final Animator arrowAnimation = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
+ .setDuration(duration);
+ arrowAnimation.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(alphaAnimatorSet, boundsAnimator, arrowAnimation);
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ AnimatorUtils.startDrawableAnimation(arrow);
+ }
+ });
+ return animatorSet;
+ }
+
+ private void setChangingViewsAlpha(float alpha) {
+ alarmLabel.setAlpha(alpha);
+ daysOfWeek.setAlpha(alpha);
+ upcomingInstanceLabel.setAlpha(alpha);
+ hairLine.setAlpha(alpha);
+ preemptiveDismissButton.setAlpha(alpha);
+ }
+
+ public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
private final LayoutInflater mLayoutInflater;
public Factory(LayoutInflater layoutInflater) {
diff --git a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java
index 623ba50..21647c2 100644
--- a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java
+++ b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java
@@ -16,103 +16,113 @@
package com.android.deskclock.alarms.dataadapter;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
import android.graphics.Color;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Vibrator;
import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
-import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
+import com.android.deskclock.AnimatorUtils;
import com.android.deskclock.ItemAdapter;
import com.android.deskclock.R;
+import com.android.deskclock.ThemeUtils;
import com.android.deskclock.Utils;
import com.android.deskclock.alarms.AlarmTimeClickHandler;
-import com.android.deskclock.alarms.utils.DayOrderUtils;
import com.android.deskclock.data.DataModel;
+import com.android.deskclock.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.provider.DaysOfWeek;
import com.android.deskclock.uidata.UiDataModel;
-import java.util.HashSet;
+import java.util.List;
import static android.content.Context.VIBRATOR_SERVICE;
+import static android.view.View.TRANSLATION_Y;
/**
- * A ViewHolder containing views for an alarm item in expanded stated.
+ * A ViewHolder containing views for an alarm item in expanded state.
*/
public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
-
public static final int VIEW_TYPE = R.layout.alarm_time_expanded;
public final CheckBox repeat;
- public final TextView editLabel;
+ private final TextView editLabel;
public final LinearLayout repeatDays;
- public final CompoundButton[] dayButtons = new CompoundButton[7];
+ private final CompoundButton[] dayButtons = new CompoundButton[7];
public final CheckBox vibrate;
public final TextView ringtone;
- public final ImageButton delete;
+ public final TextView delete;
+ private final View hairLine;
private final boolean mHasVibrator;
- private final int[] mDayOrder;
- public ExpandedAlarmViewHolder(View itemView, boolean hasVibrator) {
+ private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator) {
super(itemView);
- final Context context = itemView.getContext();
mHasVibrator = hasVibrator;
- mDayOrder = DayOrderUtils.getDayOrder(context);
- final Resources.Theme theme = context.getTheme();
- int[] attrs = new int[] { android.R.attr.selectableItemBackground };
- final TypedArray typedArray = theme.obtainStyledAttributes(attrs);
- final LayerDrawable background = new LayerDrawable(new Drawable[] {
- ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded),
- typedArray.getDrawable(0) });
- itemView.setBackground(background);
- typedArray.recycle();
-
- final int firstDay = Utils.getZeroIndexedFirstDayOfWeek(context);
-
- delete = (ImageButton) itemView.findViewById(R.id.delete);
-
+ delete = (TextView) itemView.findViewById(R.id.delete);
repeat = (CheckBox) itemView.findViewById(R.id.repeat_onoff);
vibrate = (CheckBox) itemView.findViewById(R.id.vibrate_onoff);
ringtone = (TextView) itemView.findViewById(R.id.choose_ringtone);
editLabel = (TextView) itemView.findViewById(R.id.edit_label);
repeatDays = (LinearLayout) itemView.findViewById(R.id.repeat_days);
+ hairLine = itemView.findViewById(R.id.hairline);
+
+ final Context context = itemView.getContext();
+ itemView.setBackground(new LayerDrawable(new Drawable[] {
+ ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded),
+ ThemeUtils.resolveDrawable(context, R.attr.selectableItemBackground)
+ }));
// Build button for each day.
- LayoutInflater mInflater = LayoutInflater.from(context);
- for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; i++) {
- final CompoundButton dayButton = (CompoundButton) mInflater.inflate(
- R.layout.day_button, repeatDays, false /* attachToRoot */);
- dayButton.setText(Utils.getShortWeekday(i, firstDay));
- dayButton.setContentDescription(Utils.getLongWeekday(i, firstDay));
- repeatDays.addView(dayButton);
+ final LayoutInflater inflater = LayoutInflater.from(context);
+ final List<Integer> weekdays = DataModel.getDataModel().getWeekdayOrder().getCalendarDays();
+ for (int i = 0; i < 7; i++) {
+ final View dayButtonFrame = inflater.inflate(R.layout.day_button, repeatDays,
+ false /* attachToRoot */);
+ final CompoundButton dayButton =
+ (CompoundButton) dayButtonFrame.findViewById(R.id.day_button_box);
+ final int weekday = weekdays.get(i);
+ dayButton.setText(UiDataModel.getUiDataModel().getShortWeekday(weekday));
+ dayButton.setContentDescription(UiDataModel.getUiDataModel().getLongWeekday(weekday));
+ repeatDays.addView(dayButtonFrame);
dayButtons[i] = dayButton;
}
+ // Cannot set in xml since we need compat functionality for API < 21
+ final Drawable labelIcon = Utils.getVectorDrawable(context, R.drawable.ic_label);
+ editLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(labelIcon, null, null, null);
+ final Drawable deleteIcon = Utils.getVectorDrawable(context, R.drawable.ic_delete_small);
+ delete.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null);
+
// Collapse handler
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
+ Events.sendAlarmEvent(R.string.action_collapse_implied, R.string.label_deskclock);
getItemHolder().collapse();
}
});
arrow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
+ Events.sendAlarmEvent(R.string.action_collapse, R.string.label_deskclock);
getItemHolder().collapse();
}
});
@@ -142,14 +152,14 @@
ringtone.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- getAlarmTimeClickHandler().onRingtoneClicked(getItemHolder().item);
+ getAlarmTimeClickHandler().onRingtoneClicked(context, getItemHolder().item);
}
});
// Delete alarm handler
delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
- getAlarmTimeClickHandler().onDeleteClicked(getItemHolder().item);
+ getAlarmTimeClickHandler().onDeleteClicked(getItemHolder());
v.announceForAccessibility(context.getString(R.string.alarm_deleted));
}
});
@@ -159,10 +169,11 @@
public void onClick(View view) {
final boolean checked = ((CheckBox) view).isChecked();
getAlarmTimeClickHandler().setAlarmRepeatEnabled(getItemHolder().item, checked);
+ getItemHolder().notifyItemChanged(ANIMATE_REPEAT_DAYS);
}
});
// Day buttons handler
- for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; i++) {
+ for (int i = 0; i < dayButtons.length; i++) {
final int buttonIndex = i;
dayButtons[i].setOnClickListener(new View.OnClickListener() {
@Override
@@ -173,41 +184,45 @@
}
});
}
+
+ itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
@Override
protected void onBindItemView(final AlarmItemHolder itemHolder) {
super.onBindItemView(itemHolder);
+
final Alarm alarm = itemHolder.item;
final AlarmInstance alarmInstance = itemHolder.getAlarmInstance();
final Context context = itemView.getContext();
- bindEditLabel(alarm);
- bindDaysOfWeekButtons(alarm);
+ bindEditLabel(context, alarm);
+ bindDaysOfWeekButtons(alarm, context);
bindVibrator(alarm);
bindRingtone(context, alarm);
bindPreemptiveDismissButton(context, alarm, alarmInstance);
}
private void bindRingtone(Context context, Alarm alarm) {
- final String title = DataModel.getDataModel().getAlarmRingtoneTitle(alarm.alert);
+ final String title = DataModel.getDataModel().getRingtoneTitle(alarm.alert);
ringtone.setText(title);
final String description = context.getString(R.string.ringtone_description);
ringtone.setContentDescription(description + " " + title);
final boolean silent = Utils.RINGTONE_SILENT.equals(alarm.alert);
- final int startResId = silent ? R.drawable.ic_ringtone_silent : R.drawable.ic_ringtone;
- final Drawable startDrawable = Utils.getVectorDrawable(context, startResId);
- ringtone.setCompoundDrawablesWithIntrinsicBounds(startDrawable, null, null, null);
+ final Drawable icon = Utils.getVectorDrawable(context,
+ silent ? R.drawable.ic_ringtone_silent : R.drawable.ic_ringtone);
+ ringtone.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
}
- private void bindDaysOfWeekButtons(Alarm alarm) {
- HashSet<Integer> setDays = alarm.daysOfWeek.getSetDays();
- for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; i++) {
+ private void bindDaysOfWeekButtons(Alarm alarm, Context context) {
+ final List<Integer> weekdays = DataModel.getDataModel().getWeekdayOrder().getCalendarDays();
+ for (int i = 0; i < weekdays.size(); i++) {
final CompoundButton dayButton = dayButtons[i];
- if (setDays.contains(mDayOrder[i])) {
+ if (alarm.daysOfWeek.isBitOn(weekdays.get(i))) {
dayButton.setChecked(true);
- dayButton.setTextColor(UiDataModel.getUiDataModel().getWindowBackgroundColor());
+ dayButton.setTextColor(ThemeUtils.resolveColor(context,
+ android.R.attr.windowBackground));
} else {
dayButton.setChecked(false);
dayButton.setTextColor(Color.WHITE);
@@ -222,12 +237,11 @@
}
}
- private void bindEditLabel(Alarm alarm) {
- if (alarm.label != null && alarm.label.length() > 0) {
- editLabel.setText(alarm.label);
- } else {
- editLabel.setText(R.string.label);
- }
+ private void bindEditLabel(Context context, Alarm alarm) {
+ editLabel.setText(alarm.label);
+ editLabel.setContentDescription(alarm.label != null && alarm.label.length() > 0
+ ? context.getString(R.string.label_description) + " " + alarm.label
+ : context.getString(R.string.no_label_specified));
}
private void bindVibrator(Alarm alarm) {
@@ -243,20 +257,275 @@
return getItemHolder().getAlarmTimeClickHandler();
}
+ @Override
+ public Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
+ int fromBottom, long duration) {
+ if (payloads == null || payloads.isEmpty() || !payloads.contains(ANIMATE_REPEAT_DAYS)) {
+ return null;
+ }
+
+ final boolean isExpansion = repeatDays.getVisibility() == View.VISIBLE;
+ final int height = repeatDays.getHeight();
+ setTranslationY(isExpansion ? -height : 0f, isExpansion ? -height : height);
+ repeatDays.setVisibility(View.VISIBLE);
+ repeatDays.setAlpha(isExpansion ? 0f : 1f);
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(AnimatorUtils.getBoundsAnimator(itemView,
+ fromLeft, fromTop, fromRight, fromBottom,
+ itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()),
+ ObjectAnimator.ofFloat(repeatDays, View.ALPHA, isExpansion ? 1f : 0f),
+ ObjectAnimator.ofFloat(repeatDays, TRANSLATION_Y, isExpansion ? 0f : -height),
+ ObjectAnimator.ofFloat(ringtone, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(vibrate, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(editLabel, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(preemptiveDismissButton, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(hairLine, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(delete, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(arrow, TRANSLATION_Y, 0f));
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ setTranslationY(0f, 0f);
+ repeatDays.setAlpha(1f);
+ repeatDays.setVisibility(isExpansion ? View.VISIBLE : View.GONE);
+ itemView.requestLayout();
+ }
+ });
+ animatorSet.setDuration(duration);
+ animatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ return animatorSet;
+ }
+
+ private void setTranslationY(float repeatDaysTranslationY, float translationY) {
+ repeatDays.setTranslationY(repeatDaysTranslationY);
+ ringtone.setTranslationY(translationY);
+ vibrate.setTranslationY(translationY);
+ editLabel.setTranslationY(translationY);
+ preemptiveDismissButton.setTranslationY(translationY);
+ hairLine.setTranslationY(translationY);
+ delete.setTranslationY(translationY);
+ arrow.setTranslationY(translationY);
+ }
+
+ @Override
+ public Animator onAnimateChange(final ViewHolder oldHolder, ViewHolder newHolder,
+ long duration) {
+ if (!(oldHolder instanceof AlarmItemViewHolder)
+ || !(newHolder instanceof AlarmItemViewHolder)) {
+ return null;
+ }
+
+ final boolean isExpanding = this == newHolder;
+ AnimatorUtils.setBackgroundAlpha(itemView, isExpanding ? 0 : 255);
+ setChangingViewsAlpha(isExpanding ? 0f : 1f);
+
+ final Animator changeAnimatorSet = isExpanding
+ ? createExpandingAnimator((AlarmItemViewHolder) oldHolder, duration)
+ : createCollapsingAnimator((AlarmItemViewHolder) newHolder, duration);
+ changeAnimatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ AnimatorUtils.setBackgroundAlpha(itemView, 255);
+ clock.setVisibility(View.VISIBLE);
+ onOff.setVisibility(View.VISIBLE);
+ arrow.setVisibility(View.VISIBLE);
+ arrow.setTranslationY(0f);
+ setChangingViewsAlpha(1f);
+ arrow.jumpDrawablesToCurrentState();
+ }
+ });
+ return changeAnimatorSet;
+ }
+
+ private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long duration) {
+ arrow.setVisibility(View.INVISIBLE);
+ clock.setVisibility(View.INVISIBLE);
+ onOff.setVisibility(View.INVISIBLE);
+
+ final boolean daysVisible = repeatDays.getVisibility() == View.VISIBLE;
+ final int numberOfItems = countNumberOfItems();
+
+ final View oldView = itemView;
+ final View newView = newHolder.itemView;
+
+ final Animator backgroundAnimator = ObjectAnimator.ofPropertyValuesHolder(oldView,
+ PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 255, 0));
+ backgroundAnimator.setDuration(duration);
+
+ final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView);
+ boundsAnimator.setDuration(duration);
+ boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ final long shortDuration = (long) (duration * ANIM_SHORT_DURATION_MULTIPLIER);
+ final Animator repeatAnimation = ObjectAnimator.ofFloat(repeat, View.ALPHA, 0f)
+ .setDuration(shortDuration);
+ final Animator editLabelAnimation = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 0f)
+ .setDuration(shortDuration);
+ final Animator repeatDaysAnimation = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 0f)
+ .setDuration(shortDuration);
+ final Animator vibrateAnimation = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 0f)
+ .setDuration(shortDuration);
+ final Animator ringtoneAnimation = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 0f)
+ .setDuration(shortDuration);
+ final Animator dismissAnimation = ObjectAnimator.ofFloat(preemptiveDismissButton,
+ View.ALPHA, 0f).setDuration(shortDuration);
+ final Animator deleteAnimation = ObjectAnimator.ofFloat(delete, View.ALPHA, 0f)
+ .setDuration(shortDuration);
+ final Animator hairLineAnimation = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f)
+ .setDuration(shortDuration);
+
+ // Set the staggered delays; use the first portion (duration * (1 - 1/4 - 1/6)) of the time,
+ // so that the final animation, with a duration of 1/4 the total duration, finishes exactly
+ // before the collapsed holder begins expanding.
+ long startDelay = 0L;
+ final long delayIncrement = (long) (duration * ANIM_LONG_DELAY_INCREMENT_MULTIPLIER)
+ / (numberOfItems - 1);
+ deleteAnimation.setStartDelay(startDelay);
+ if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
+ startDelay += delayIncrement;
+ dismissAnimation.setStartDelay(startDelay);
+ }
+ hairLineAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ editLabelAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ vibrateAnimation.setStartDelay(startDelay);
+ ringtoneAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ if (daysVisible) {
+ repeatDaysAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ }
+ repeatAnimation.setStartDelay(startDelay);
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(backgroundAnimator, boundsAnimator, repeatAnimation,
+ repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
+ deleteAnimation, hairLineAnimation, dismissAnimation);
+ return animatorSet;
+ }
+
+ private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long duration) {
+ final View oldView = oldHolder.itemView;
+ final View newView = itemView;
+ final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView);
+ boundsAnimator.setDuration(duration);
+ boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ final Animator backgroundAnimator = ObjectAnimator.ofPropertyValuesHolder(newView,
+ PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255));
+ backgroundAnimator.setDuration(duration);
+
+ final View oldArrow = oldHolder.arrow;
+ final Rect oldArrowRect = new Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight());
+ final Rect newArrowRect = new Rect(0, 0, arrow.getWidth(), arrow.getHeight());
+ ((ViewGroup) newView).offsetDescendantRectToMyCoords(arrow, newArrowRect);
+ ((ViewGroup) oldView).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect);
+ final float arrowTranslationY = oldArrowRect.bottom - newArrowRect.bottom;
+
+ arrow.setTranslationY(arrowTranslationY);
+ arrow.setVisibility(View.VISIBLE);
+ clock.setVisibility(View.VISIBLE);
+ onOff.setVisibility(View.VISIBLE);
+
+ final long longDuration = (long) (duration * ANIM_LONG_DURATION_MULTIPLIER);
+ final Animator repeatAnimation = ObjectAnimator.ofFloat(repeat, View.ALPHA, 1f)
+ .setDuration(longDuration);
+ final Animator repeatDaysAnimation = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 1f)
+ .setDuration(longDuration);
+ final Animator ringtoneAnimation = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 1f)
+ .setDuration(longDuration);
+ final Animator dismissAnimation = ObjectAnimator.ofFloat(preemptiveDismissButton,
+ View.ALPHA, 1f).setDuration(longDuration);
+ final Animator vibrateAnimation = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 1f)
+ .setDuration(longDuration);
+ final Animator editLabelAnimation = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 1f)
+ .setDuration(longDuration);
+ final Animator hairLineAnimation = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f)
+ .setDuration(longDuration);
+ final Animator deleteAnimation = ObjectAnimator.ofFloat(delete, View.ALPHA, 1f)
+ .setDuration(longDuration);
+ final Animator arrowAnimation = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
+ .setDuration(duration);
+ arrowAnimation.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ // Set the stagger delays; delay the first by the amount of time it takes for the collapse
+ // to complete, then stagger the expansion with the remaining time.
+ long startDelay = (long) (duration * ANIM_STANDARD_DELAY_MULTIPLIER);
+ final int numberOfItems = countNumberOfItems();
+ final long delayIncrement = (long) (duration * ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER)
+ / (numberOfItems - 1);
+ repeatAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ final boolean daysVisible = repeatDays.getVisibility() == View.VISIBLE;
+ if (daysVisible) {
+ repeatDaysAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ }
+ ringtoneAnimation.setStartDelay(startDelay);
+ vibrateAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ editLabelAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ hairLineAnimation.setStartDelay(startDelay);
+ if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
+ dismissAnimation.setStartDelay(startDelay);
+ startDelay += delayIncrement;
+ }
+ deleteAnimation.setStartDelay(startDelay);
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(backgroundAnimator, repeatAnimation, boundsAnimator,
+ repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
+ deleteAnimation, hairLineAnimation, dismissAnimation, arrowAnimation);
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ AnimatorUtils.startDrawableAnimation(arrow);
+ }
+ });
+ return animatorSet;
+ }
+
+ private int countNumberOfItems() {
+ // Always between 4 and 6 items.
+ int numberOfItems = 4;
+ if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
+ numberOfItems++;
+ }
+ if (repeatDays.getVisibility() == View.VISIBLE) {
+ numberOfItems++;
+ }
+ return numberOfItems;
+ }
+
+ private void setChangingViewsAlpha(float alpha) {
+ repeat.setAlpha(alpha);
+ editLabel.setAlpha(alpha);
+ repeatDays.setAlpha(alpha);
+ vibrate.setAlpha(alpha);
+ ringtone.setAlpha(alpha);
+ hairLine.setAlpha(alpha);
+ delete.setAlpha(alpha);
+ preemptiveDismissButton.setAlpha(alpha);
+ }
+
public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
- private final LayoutInflater mLayoutInflator;
+ private final LayoutInflater mLayoutInflater;
private final boolean mHasVibrator;
- public Factory(Context context, LayoutInflater layoutInflater) {
- mLayoutInflator = layoutInflater;
+ public Factory(Context context) {
+ mLayoutInflater = LayoutInflater.from(context);
mHasVibrator = ((Vibrator) context.getSystemService(VIBRATOR_SERVICE)).hasVibrator();
}
@Override
public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
- return new ExpandedAlarmViewHolder(mLayoutInflator.inflate(
- viewType, parent, false /* attachToRoot */), mHasVibrator);
+ final View itemView = mLayoutInflater.inflate(viewType, parent, false);
+ return new ExpandedAlarmViewHolder(itemView, mHasVibrator);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/deskclock/alarms/utils/DayOrderUtils.java b/src/com/android/deskclock/alarms/utils/DayOrderUtils.java
deleted file mode 100644
index 6832ad0..0000000
--- a/src/com/android/deskclock/alarms/utils/DayOrderUtils.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.alarms.utils;
-
-import android.content.Context;
-
-import com.android.deskclock.Utils;
-import com.android.deskclock.provider.DaysOfWeek;
-
-import java.util.Calendar;
-
-/**
- * Contains information about the order that days of the week are shown in the UI.
- */
-public final class DayOrderUtils {
-
- // A reference used to create mDayOrder
- private static final int[] DAY_ORDER = {
- Calendar.SUNDAY,
- Calendar.MONDAY,
- Calendar.TUESDAY,
- Calendar.WEDNESDAY,
- Calendar.THURSDAY,
- Calendar.FRIDAY,
- Calendar.SATURDAY,
- };
-
- public static int[] getDayOrder(Context context) {
- // Value from preferences corresponds to Calendar.<WEEKDAY> value
- // -1 in order to correspond to DAY_ORDER indexing
- final int startDay = Utils.getZeroIndexedFirstDayOfWeek(context);
- final int[] dayOrder = new int[DaysOfWeek.DAYS_IN_A_WEEK];
-
- for (int i = 0; i < DaysOfWeek.DAYS_IN_A_WEEK; ++i) {
- dayOrder[i] = DAY_ORDER[(startDay + i) % DaysOfWeek.DAYS_IN_A_WEEK];
- }
- return dayOrder;
- }
-
-}
diff --git a/src/com/android/deskclock/controller/Controller.java b/src/com/android/deskclock/controller/Controller.java
index ad15db6..26ada45 100644
--- a/src/com/android/deskclock/controller/Controller.java
+++ b/src/com/android/deskclock/controller/Controller.java
@@ -16,16 +16,32 @@
package com.android.deskclock.controller;
+import android.app.Activity;
import android.content.Context;
+import android.support.annotation.StringRes;
import com.android.deskclock.Utils;
+import com.android.deskclock.events.EventTracker;
+import static com.android.deskclock.Utils.enforceMainLooper;
+
+/**
+ * Interactions with Android framework components responsible for part of the user experience are
+ * handled via this singleton.
+ */
public final class Controller {
private static final Controller sController = new Controller();
private Context mContext;
+ /** The controller that dispatches app events to event trackers. */
+ private EventController mEventController;
+
+ /** The controller that interacts with voice interaction sessions on M+. */
+ private VoiceController mVoiceController;
+
+ /** The controller that creates and updates launcher shortcuts on N MR1+ */
private ShortcutController mShortcutController;
private Controller() {}
@@ -35,19 +51,68 @@
}
public void setContext(Context context) {
- if (mContext != null) {
- throw new IllegalStateException("context has already been set");
- }
- mContext = context;
- if (Utils.isNMR1OrLater()) {
- mShortcutController = new ShortcutController(mContext);
+ if (mContext != context) {
+ mContext = context.getApplicationContext();
+ mEventController = new EventController();
+ mVoiceController = new VoiceController();
+ if (Utils.isNMR1OrLater()) {
+ mShortcutController = new ShortcutController(mContext);
+ }
}
}
+ //
+ // Event Tracking
+ //
+
+ /**
+ * @param eventTracker to be registered for tracking application events
+ */
+ public void addEventTracker(EventTracker eventTracker) {
+ enforceMainLooper();
+ mEventController.addEventTracker(eventTracker);
+ }
+
+ /**
+ * @param eventTracker to be unregistered from tracking application events
+ */
+ public void removeEventTracker(EventTracker eventTracker) {
+ enforceMainLooper();
+ mEventController.removeEventTracker(eventTracker);
+ }
+
+ /**
+ * Tracks an event. Events have a category, action and label. This method can be used to track
+ * events such as button presses or other user interactions with your application.
+ *
+ * @param category resource id of event category
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ public void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
+ mEventController.sendEvent(category, action, label);
+ }
+
+ //
+ // Voice Interaction
+ //
+
+ public void notifyVoiceSuccess(Activity activity, String message) {
+ mVoiceController.notifyVoiceSuccess(activity, message);
+ }
+
+ public void notifyVoiceFailure(Activity activity, String message) {
+ mVoiceController.notifyVoiceFailure(activity, message);
+ }
+
+ //
+ // Shortcuts
+ //
+
public void updateShortcuts() {
- Utils.enforceMainLooper();
+ enforceMainLooper();
if (mShortcutController != null) {
mShortcutController.updateShortcuts();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/EventController.java b/src/com/android/deskclock/controller/EventController.java
new file mode 100644
index 0000000..c746ba7
--- /dev/null
+++ b/src/com/android/deskclock/controller/EventController.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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.controller;
+
+import android.support.annotation.StringRes;
+
+import com.android.deskclock.events.EventTracker;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+class EventController {
+
+ private final Collection<EventTracker> mEventTrackers = new ArrayList<>();
+
+ void addEventTracker(EventTracker eventTracker) {
+ mEventTrackers.add(eventTracker);
+ }
+
+ void removeEventTracker(EventTracker eventTracker) {
+ mEventTrackers.remove(eventTracker);
+ }
+
+ void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
+ for (EventTracker eventTracker : mEventTrackers) {
+ eventTracker.sendEvent(category, action, label);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/ShortcutController.java b/src/com/android/deskclock/controller/ShortcutController.java
index 87ca2d8..063989e 100644
--- a/src/com/android/deskclock/controller/ShortcutController.java
+++ b/src/com/android/deskclock/controller/ShortcutController.java
@@ -59,7 +59,7 @@
mComponentName = new ComponentName(mContext, DeskClock.class);
mShortcutManager = mContext.getSystemService(ShortcutManager.class);
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- Events.addEventTracker(new ShortcutEventTracker(mContext));
+ Controller.getController().addEventTracker(new ShortcutEventTracker(mContext));
DataModel.getDataModel().addStopwatchListener(new StopwatchWatcher());
}
diff --git a/src/com/android/deskclock/controller/VoiceController.java b/src/com/android/deskclock/controller/VoiceController.java
new file mode 100644
index 0000000..1eae560
--- /dev/null
+++ b/src/com/android/deskclock/controller/VoiceController.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 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.controller;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.VoiceInteractor;
+import android.app.VoiceInteractor.AbortVoiceRequest;
+import android.app.VoiceInteractor.CompleteVoiceRequest;
+import android.app.VoiceInteractor.Prompt;
+import android.os.Build;
+
+import com.android.deskclock.Utils;
+
+@TargetApi(Build.VERSION_CODES.M)
+class VoiceController {
+ /**
+ * If the {@code activity} is currently hosting a voice interaction session, indicate the voice
+ * command was processed successfully.
+ *
+ * @param activity an Activity that may be hosting a voice interaction session
+ * @param message to be spoken to the user to indicate success
+ */
+ void notifyVoiceSuccess(Activity activity, String message) {
+ if (!Utils.isMOrLater()) {
+ return;
+ }
+
+ final VoiceInteractor voiceInteractor = activity.getVoiceInteractor();
+ if (voiceInteractor != null) {
+ final Prompt prompt = new Prompt(message);
+ voiceInteractor.submitRequest(new CompleteVoiceRequest(prompt, null));
+ }
+ }
+
+ /**
+ * If the {@code activity} is currently hosting a voice interaction session, indicate the voice
+ * command failed and must be aborted.
+ *
+ * @param activity an Activity that may be hosting a voice interaction session
+ * @param message to be spoken to the user to indicate failure
+ */
+ void notifyVoiceFailure(Activity activity, String message) {
+ if (!Utils.isMOrLater()) {
+ return;
+ }
+
+ final VoiceInteractor voiceInteractor = activity.getVoiceInteractor();
+ if (voiceInteractor != null) {
+ final Prompt prompt = new Prompt(message);
+ voiceInteractor.submitRequest(new AbortVoiceRequest(prompt, null));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/AlarmModel.java b/src/com/android/deskclock/data/AlarmModel.java
index 0ae5a0b..da50fe5 100644
--- a/src/com/android/deskclock/data/AlarmModel.java
+++ b/src/com/android/deskclock/data/AlarmModel.java
@@ -19,41 +19,29 @@
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Handler;
import android.provider.Settings;
-import android.util.ArrayMap;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
import com.android.deskclock.provider.Alarm;
-import java.util.Map;
-
/**
* All alarm data will eventually be accessed via this model.
*/
final class AlarmModel {
- private final Context mContext;
-
/** The model from which settings are fetched. */
private final SettingsModel mSettingsModel;
/** The uri of the default ringtone to play for new alarms; mirrors last selection. */
private Uri mDefaultAlarmRingtoneUri;
- /** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive. */
- private final Map<Uri, String> mRingtoneTitles = new ArrayMap<>(8);
-
AlarmModel(Context context, SettingsModel settingsModel) {
- mContext = context;
mSettingsModel = settingsModel;
// Clear caches affected by system settings when system settings change.
- final ContentResolver cr = mContext.getContentResolver();
+ final ContentResolver cr = context.getContentResolver();
final ContentObserver observer = new SystemAlarmAlertChangeObserver();
cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer);
}
@@ -74,28 +62,20 @@
}
}
- String getAlarmRingtoneTitle(Uri uri) {
- // Special case: no ringtone has a title of "Silent".
- if (Alarm.NO_RINGTONE_URI.equals(uri)) {
- return mContext.getString(R.string.silent_ringtone_title);
- }
+ long getAlarmCrescendoDuration() {
+ return mSettingsModel.getAlarmCrescendoDuration();
+ }
- // Check the cache.
- String title = mRingtoneTitles.get(uri);
+ AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
+ return mSettingsModel.getAlarmVolumeButtonBehavior();
+ }
- if (title == null) {
- // This is slow because a media player is created during Ringtone object creation.
- final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri);
- if (ringtone == null) {
- LogUtils.e("No ringtone for uri: %s", uri);
- return mContext.getString(R.string.unknown_ringtone_title);
- }
+ int getAlarmTimeout() {
+ return mSettingsModel.getAlarmTimeout();
+ }
- // Cache the title for later use.
- title = ringtone.getTitle(mContext);
- mRingtoneTitles.put(uri, title);
- }
- return title;
+ int getSnoozeLength() {
+ return mSettingsModel.getSnoozeLength();
}
/**
@@ -111,13 +91,7 @@
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
-
- LogUtils.i("Detected change to system default alarm ringtone; clearing caches");
-
mDefaultAlarmRingtoneUri = null;
-
- // Titles such as "Default ringtone (Oxygen)" are wrong after default ringtone changes.
- mRingtoneTitles.clear();
}
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityDAO.java b/src/com/android/deskclock/data/CityDAO.java
index 1399248..5b5789c 100644
--- a/src/com/android/deskclock/data/CityDAO.java
+++ b/src/com/android/deskclock/data/CityDAO.java
@@ -25,7 +25,6 @@
import android.util.ArrayMap;
import com.android.deskclock.R;
-import com.android.deskclock.Utils;
import java.util.ArrayList;
import java.util.Collection;
@@ -58,8 +57,7 @@
* @param cityMap maps city ids to city instances
* @return the list of city ids selected for display by the user
*/
- static List<City> getSelectedCities(Context context, Map<String, City> cityMap) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static List<City> getSelectedCities(SharedPreferences prefs, Map<String, City> cityMap) {
final int size = prefs.getInt(NUMBER_OF_CITIES, 0);
final List<City> selectedCities = new ArrayList<>(size);
@@ -77,8 +75,7 @@
/**
* @param cities the collection of cities selected for display by the user
*/
- static void setSelectedCities(Context context, Collection<City> cities) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void setSelectedCities(SharedPreferences prefs, Collection<City> cities) {
final SharedPreferences.Editor editor = prefs.edit();
editor.putInt(NUMBER_OF_CITIES, cities.size());
@@ -125,17 +122,11 @@
throw new IllegalStateException(message);
}
- // Attempt to resolve the time zone id to a valid time zone.
- final String tzId = cityParts[1];
- final TimeZone tz = TimeZone.getTimeZone(tzId);
- // If the time zone lookup fails, GMT is returned. No cities actually map to GMT.
- if ("GMT".equals(tz.getID())) {
- final String message = String.format(
- "Unable to locate timezone with id %s", tzId);
- throw new IllegalStateException(message);
+ final City city = createCity(id, cityParts[0], cityParts[1]);
+ // Skip cities whose timezone cannot be resolved.
+ if (city != null) {
+ cities.put(id, city);
}
-
- cities.put(id, createCity(id, cityParts[0], tz));
}
} finally {
cityStrings.recycle();
@@ -149,10 +140,16 @@
* @param formattedName "[index string]=[name]" or "[index string]=[name]:[phonetic name]",
* If [index string] is empty, use the first character of name as index,
* If phonetic name is empty, use the name itself as phonetic name.
- * @param tz the timezone in which the city is located
+ * @param tzId the string id of the timezone a given city is located in
*/
@VisibleForTesting
- static City createCity(String id, String formattedName, TimeZone tz) {
+ static City createCity(String id, String formattedName, String tzId) {
+ final TimeZone tz = TimeZone.getTimeZone(tzId);
+ // If the time zone lookup fails, GMT is returned. No cities actually map to GMT.
+ if ("GMT".equals(tz.getID())) {
+ return null;
+ }
+
final String[] parts = formattedName.split("[=:]");
final String name = parts[1];
// Extract index string from input, use the first character of city name as the index string
diff --git a/src/com/android/deskclock/data/CityListener.java b/src/com/android/deskclock/data/CityListener.java
index 37f1b52..91f66b3 100644
--- a/src/com/android/deskclock/data/CityListener.java
+++ b/src/com/android/deskclock/data/CityListener.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2016 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 java.util.List;
@@ -7,4 +23,4 @@
*/
public interface CityListener {
void citiesChanged(List<City> oldCities, List<City> newCities);
-}
+}
\ 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 fafd48b..9f45875 100644
--- a/src/com/android/deskclock/data/CityModel.java
+++ b/src/com/android/deskclock/data/CityModel.java
@@ -44,6 +44,8 @@
private final Context mContext;
+ private final SharedPreferences mPrefs;
+
/** The model from which settings are fetched. */
private final SettingsModel mSettingsModel;
@@ -76,8 +78,9 @@
/** A city instance representing the home timezone of the user. */
private City mHomeCity;
- CityModel(Context context, SettingsModel settingsModel) {
+ CityModel(Context context, SharedPreferences prefs, SettingsModel settingsModel) {
mContext = context;
+ mPrefs = prefs;
mSettingsModel = settingsModel;
// Clear caches affected by locale when locale changes.
@@ -85,7 +88,6 @@
mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
// Clear caches affected by preferences when preferences change.
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(mContext);
prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
}
@@ -119,22 +121,6 @@
}
/**
- * @param cityName the case-insensitive city name to search for
- * @return the city with the given {@code cityName}; {@code null} if no such city exists
- */
- City getCity(String cityName) {
- cityName = cityName.toUpperCase();
-
- for (City city : getAllCities()) {
- if (cityName.equals(city.getNameUpperCase())) {
- return city;
- }
- }
-
- return null;
- }
-
- /**
* @return a city representing the user's home timezone
*/
City getHomeCity() {
@@ -177,7 +163,7 @@
*/
List<City> getSelectedCities() {
if (mSelectedCities == null) {
- final List<City> selectedCities = CityDAO.getSelectedCities(mContext, getCityMap());
+ final List<City> selectedCities = CityDAO.getSelectedCities(mPrefs, getCityMap());
Collections.sort(selectedCities, new City.UtcOffsetComparator());
mSelectedCities = Collections.unmodifiableList(selectedCities);
}
@@ -190,7 +176,7 @@
*/
void setSelectedCities(Collection<City> cities) {
final List<City> oldCities = getAllCities();
- CityDAO.setSelectedCities(mContext, cities);
+ CityDAO.setSelectedCities(mPrefs, cities);
// Clear caches affected by this update.
mAllCities = null;
diff --git a/src/com/android/deskclock/data/CustomRingtone.java b/src/com/android/deskclock/data/CustomRingtone.java
new file mode 100644
index 0000000..bd847c6
--- /dev/null
+++ b/src/com/android/deskclock/data/CustomRingtone.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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.net.Uri;
+import android.support.annotation.NonNull;
+
+/**
+ * A read-only domain object representing a custom ringtone chosen from the file system.
+ */
+public final class CustomRingtone implements Comparable<CustomRingtone> {
+
+ /** The unique identifier of the custom ringtone. */
+ private final long mId;
+
+ /** The uri that allows playback of the ringtone. */
+ private final Uri mUri;
+
+ /** The title describing the file at the given uri; typically the file name. */
+ private final String mTitle;
+
+ /** {@code true} iff the application has permission to read the content of {@code mUri uri}. */
+ private final boolean mHasPermissions;
+
+ CustomRingtone(long id, Uri uri, String title, boolean hasPermissions) {
+ mId = id;
+ mUri = uri;
+ mTitle = title;
+ mHasPermissions = hasPermissions;
+ }
+
+ public long getId() { return mId; }
+ public Uri getUri() { return mUri; }
+ public String getTitle() { return mTitle; }
+ public boolean hasPermissions() { return mHasPermissions; }
+
+ CustomRingtone setHasPermissions(boolean hasPermissions) {
+ if (mHasPermissions == hasPermissions) {
+ return this;
+ }
+
+ return new CustomRingtone(mId, mUri, mTitle, hasPermissions);
+ }
+
+ @Override
+ public int compareTo(@NonNull CustomRingtone other) {
+ return String.CASE_INSENSITIVE_ORDER.compare(getTitle(), other.getTitle());
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CustomRingtoneDAO.java b/src/com/android/deskclock/data/CustomRingtoneDAO.java
new file mode 100644
index 0000000..827acb5
--- /dev/null
+++ b/src/com/android/deskclock/data/CustomRingtoneDAO.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 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.SharedPreferences;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This class encapsulates the transfer of data between {@link CustomRingtone} domain objects and
+ * their permanent storage in {@link SharedPreferences}.
+ */
+final class CustomRingtoneDAO {
+
+ /** Key to a preference that stores the set of all custom ringtone ids. */
+ private static final String RINGTONE_IDS = "ringtone_ids";
+
+ /** Key to a preference that stores the next unused ringtone id. */
+ private static final String NEXT_RINGTONE_ID = "next_ringtone_id";
+
+ /** Prefix for a key to a preference that stores the URI associated with the ringtone id. */
+ private static final String RINGTONE_URI = "ringtone_uri_";
+
+ /** Prefix for a key to a preference that stores the title associated with the ringtone id. */
+ private static final String RINGTONE_TITLE = "ringtone_title_";
+
+ private CustomRingtoneDAO() {}
+
+ /**
+ * @param uri points to an audio file located on the file system
+ * @param title the title of the audio content at the given {@code uri}
+ * @return the newly added custom ringtone
+ */
+ static CustomRingtone addCustomRingtone(SharedPreferences prefs, Uri uri, String title) {
+ final long id = prefs.getLong(NEXT_RINGTONE_ID, 0);
+ final Set<String> ids = getRingtoneIds(prefs);
+ ids.add(String.valueOf(id));
+
+ prefs.edit()
+ .putString(RINGTONE_URI + id, uri.toString())
+ .putString(RINGTONE_TITLE + id, title)
+ .putLong(NEXT_RINGTONE_ID, id + 1)
+ .putStringSet(RINGTONE_IDS, ids)
+ .apply();
+
+ return new CustomRingtone(id, uri, title, true);
+ }
+
+ /**
+ * @param id identifies the ringtone to be removed
+ */
+ static void removeCustomRingtone(SharedPreferences prefs, long id) {
+ final Set<String> ids = getRingtoneIds(prefs);
+ ids.remove(String.valueOf(id));
+
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(RINGTONE_URI + id);
+ editor.remove(RINGTONE_TITLE + id);
+ if (ids.isEmpty()) {
+ editor.remove(RINGTONE_IDS);
+ editor.remove(NEXT_RINGTONE_ID);
+ } else {
+ editor.putStringSet(RINGTONE_IDS, ids);
+ }
+ editor.apply();
+ }
+
+ /**
+ * @return a list of all known custom ringtones
+ */
+ static List<CustomRingtone> getCustomRingtones(SharedPreferences prefs) {
+ final Set<String> ids = prefs.getStringSet(RINGTONE_IDS, Collections.<String>emptySet());
+ final List<CustomRingtone> ringtones = new ArrayList<>(ids.size());
+
+ for (String id : ids) {
+ final long idLong = Long.parseLong(id);
+ final Uri uri = Uri.parse(prefs.getString(RINGTONE_URI + id, null));
+ final String title = prefs.getString(RINGTONE_TITLE + id, null);
+ ringtones.add(new CustomRingtone(idLong, uri, title, true));
+ }
+
+ return ringtones;
+ }
+
+ private static Set<String> getRingtoneIds(SharedPreferences prefs) {
+ return new HashSet<>(prefs.getStringSet(RINGTONE_IDS, Collections.<String>emptySet()));
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/DataModel.java b/src/com/android/deskclock/data/DataModel.java
index 7def051..2b16ae8 100644
--- a/src/com/android/deskclock/data/DataModel.java
+++ b/src/com/android/deskclock/data/DataModel.java
@@ -18,18 +18,33 @@
import android.app.Service;
import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.media.AudioManager;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.StringRes;
+import android.view.View;
+import com.android.deskclock.Predicate;
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
import com.android.deskclock.timer.TimerService;
+import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
+import static android.content.Context.AUDIO_SERVICE;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.media.AudioManager.FLAG_SHOW_UI;
+import static android.media.AudioManager.STREAM_ALARM;
+import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
+import static android.provider.Settings.ACTION_SOUND_SETTINGS;
import static com.android.deskclock.Utils.enforceMainLooper;
+import static com.android.deskclock.Utils.enforceNotMainLooper;
/**
* All application-wide data is accessible through this singleton.
@@ -42,6 +57,103 @@
/** Indicates the preferred sort order of cities. */
public enum CitySort {NAME, UTC_OFFSET}
+ /** Indicates the preferred behavior of hardware volume buttons when firing alarms. */
+ public enum AlarmVolumeButtonBehavior {NOTHING, SNOOZE, DISMISS}
+
+ /** Indicates the reason alarms may not fire or may fire silently. */
+ public enum SilentSetting {
+ @SuppressWarnings("unchecked")
+ DO_NOT_DISTURB(R.string.alarms_blocked_by_dnd, 0, Predicate.FALSE, null),
+ @SuppressWarnings("unchecked")
+ MUTED_VOLUME(R.string.alarm_volume_muted,
+ R.string.unmute_alarm_volume,
+ Predicate.TRUE,
+ new UnmuteAlarmVolumeListener()),
+ SILENT_RINGTONE(R.string.silent_default_alarm_ringtone,
+ R.string.change_setting_action,
+ new ChangeSoundActionPredicate(),
+ new ChangeSoundSettingsListener()),
+ @SuppressWarnings("unchecked")
+ BLOCKED_NOTIFICATIONS(R.string.app_notifications_blocked,
+ R.string.change_setting_action,
+ Predicate.TRUE,
+ new ChangeAppNotificationSettingsListener());
+
+ private final @StringRes int mLabelResId;
+ private final @StringRes int mActionResId;
+ private final Predicate<Context> mActionEnabled;
+ private final View.OnClickListener mActionListener;
+
+ SilentSetting(int labelResId, int actionResId, Predicate<Context> actionEnabled,
+ View.OnClickListener actionListener) {
+ mLabelResId = labelResId;
+ mActionResId = actionResId;
+ mActionEnabled = actionEnabled;
+ mActionListener = actionListener;
+ }
+
+ public @StringRes int getLabelResId() { return mLabelResId; }
+ public @StringRes int getActionResId() { return mActionResId; }
+ public View.OnClickListener getActionListener() { return mActionListener; }
+ public boolean isActionEnabled(Context context) {
+ return mLabelResId != 0 && mActionEnabled.apply(context);
+ }
+
+ private static class UnmuteAlarmVolumeListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ // Set the alarm volume to 11/16th of max and show the slider UI.
+ // 11/16th of max is the initial volume of the alarm stream on a fresh install.
+ final Context context = v.getContext();
+ final AudioManager am = (AudioManager) context.getSystemService(AUDIO_SERVICE);
+ final int index = Math.round(am.getStreamMaxVolume(STREAM_ALARM) * 11f / 16f);
+ am.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI);
+ }
+ }
+
+ private static class ChangeSoundSettingsListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ final Context context = v.getContext();
+ context.startActivity(new Intent(ACTION_SOUND_SETTINGS)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK));
+ }
+ }
+
+ private static class ChangeSoundActionPredicate implements Predicate<Context> {
+ @Override
+ public boolean apply(Context context) {
+ final Intent intent = new Intent(ACTION_SOUND_SETTINGS);
+ return intent.resolveActivity(context.getPackageManager()) != null;
+ }
+ }
+
+ private static class ChangeAppNotificationSettingsListener implements View.OnClickListener {
+ @Override
+ public void onClick(View v) {
+ final Context context = v.getContext();
+ if (Utils.isLOrLater()) {
+ try {
+ // Attempt to open the notification settings for this app.
+ context.startActivity(
+ new Intent("android.settings.APP_NOTIFICATION_SETTINGS")
+ .putExtra("app_package", context.getPackageName())
+ .putExtra("app_uid", context.getApplicationInfo().uid)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK));
+ return;
+ } catch (Exception ignored) {
+ // best attempt only; recovery code below
+ }
+ }
+
+ // Fall back to opening the app settings page.
+ context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", context.getPackageName(), null))
+ .addFlags(FLAG_ACTIVITY_NEW_TASK));
+ }
+ }
+ }
+
public static final String ACTION_WORLD_CITIES_CHANGED =
"com.android.deskclock.WORLD_CITIES_CHANGED";
@@ -67,12 +179,21 @@
/** The model from which widget data are fetched. */
private WidgetModel mWidgetModel;
+ /** The model from which data about settings that silence alarms are fetched. */
+ private SilentSettingsModel mSilentSettingsModel;
+
/** The model from which stopwatch data are fetched. */
private StopwatchModel mStopwatchModel;
/** The model from which notification data are fetched. */
private NotificationModel mNotificationModel;
+ /** The model from which time data are fetched. */
+ private TimeModel mTimeModel;
+
+ /** The model from which ringtone data are fetched. */
+ private RingtoneModel mRingtoneModel;
+
public static DataModel getDataModel() {
return sDataModel;
}
@@ -80,21 +201,24 @@
private DataModel() {}
/**
- * The context may be set precisely once during the application life.
+ * Initializes the data model with the context and shared preferences to be used.
*/
- public void setContext(Context context) {
- if (mContext != null) {
- throw new IllegalStateException("context has already been set");
- }
- mContext = context.getApplicationContext();
+ public void init(Context context, SharedPreferences prefs) {
+ if (mContext != context) {
+ mContext = context.getApplicationContext();
- mSettingsModel = new SettingsModel(mContext);
- mNotificationModel = new NotificationModel();
- mCityModel = new CityModel(mContext, mSettingsModel);
- mWidgetModel = new WidgetModel(mContext);
- mAlarmModel = new AlarmModel(mContext, mSettingsModel);
- mStopwatchModel = new StopwatchModel(mContext, mNotificationModel);
- mTimerModel = new TimerModel(mContext, mSettingsModel, mNotificationModel);
+ mTimeModel = new TimeModel(mContext);
+ mWidgetModel = new WidgetModel(prefs);
+ mNotificationModel = new NotificationModel();
+ mRingtoneModel = new RingtoneModel(mContext, prefs);
+ mSettingsModel = new SettingsModel(mContext, prefs, mTimeModel);
+ mCityModel = new CityModel(mContext, prefs, mSettingsModel);
+ mAlarmModel = new AlarmModel(mContext, mSettingsModel);
+ mSilentSettingsModel = new SilentSettingsModel(mContext, mNotificationModel);
+ mStopwatchModel = new StopwatchModel(mContext, prefs, mNotificationModel);
+ mTimerModel = new TimerModel(mContext, prefs, mSettingsModel, mRingtoneModel,
+ mNotificationModel);
+ }
}
/**
@@ -173,6 +297,7 @@
mTimerModel.updateNotification();
mTimerModel.updateMissedNotification();
mStopwatchModel.updateNotification();
+ mSilentSettingsModel.updateSilentState();
}
}
@@ -208,15 +333,6 @@
}
/**
- * @param cityName the case-insensitive city name to search for
- * @return the city with the given {@code cityName}; {@code null} if no such city exists
- */
- public City getCity(String cityName) {
- enforceMainLooper();
- return mCityModel.getCity(cityName);
- }
-
- /**
* @return a city representing the user's home timezone
*/
public City getHomeCity() {
@@ -455,7 +571,6 @@
mTimerModel.resetMissedTimers(eventLabelId);
}
-
/**
* @param timer the timer to which a minute should be added to the remaining time
*/
@@ -474,7 +589,7 @@
}
/**
- * @param timer the timer whose {@code length} to change
+ * @param timer the timer whose {@code length} to change
* @param length the new length of the timer in milliseconds
*/
public void setTimerLength(Timer timer, long length) {
@@ -483,7 +598,7 @@
}
/**
- * @param timer the timer whose {@code remainingTime} to change
+ * @param timer the timer whose {@code remainingTime} to change
* @param remainingTime the new remaining time of the timer in milliseconds
*/
public void setRemainingTime(Timer timer, long remainingTime) {
@@ -545,6 +660,15 @@
}
/**
+ * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+ * {@code 0} implies no crescendo should be applied
+ */
+ public long getTimerCrescendoDuration() {
+ enforceMainLooper();
+ return mTimerModel.getTimerCrescendoDuration();
+ }
+
+ /**
* @return whether vibrate is enabled for all timers.
*/
public boolean getTimerVibrate() {
@@ -581,12 +705,34 @@
}
/**
- * @param uri the uri of a ringtone
- * @return the title of the ringtone with the {@code uri}; {@code null} if it cannot be fetched
+ * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
+ * {@code 0} implies no crescendo should be applied
*/
- public String getAlarmRingtoneTitle(Uri uri) {
+ public long getAlarmCrescendoDuration() {
enforceMainLooper();
- return mAlarmModel.getAlarmRingtoneTitle(uri);
+ return mAlarmModel.getAlarmCrescendoDuration();
+ }
+
+ /**
+ * @return the behavior to execute when volume buttons are pressed while firing an alarm
+ */
+ public AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
+ enforceMainLooper();
+ return mAlarmModel.getAlarmVolumeButtonBehavior();
+ }
+
+ /**
+ * @return the number of minutes an alarm may ring before it has timed out and becomes missed
+ */
+ public int getAlarmTimeout() {
+ return mAlarmModel.getAlarmTimeout();
+ }
+
+ /**
+ * @return the number of minutes an alarm will remain snoozed before it rings again
+ */
+ public int getSnoozeLength() {
+ return mAlarmModel.getSnoozeLength();
}
//
@@ -683,6 +829,96 @@
}
//
+ // Time
+ // (Time settings/values are accessible from any Thread so no Thread-enforcement exists.)
+ //
+
+ /**
+ * @return the current time in milliseconds
+ */
+ public long currentTimeMillis() {
+ return mTimeModel.currentTimeMillis();
+ }
+
+ /**
+ * @return milliseconds since boot, including time spent in sleep
+ */
+ public long elapsedRealtime() {
+ return mTimeModel.elapsedRealtime();
+ }
+
+ /**
+ * @return {@code true} if 24 hour time format is selected; {@code false} otherwise
+ */
+ public boolean is24HourFormat() {
+ return mTimeModel.is24HourFormat();
+ }
+
+ /**
+ * @return a new calendar object initialized to the {@link #currentTimeMillis()}
+ */
+ public Calendar getCalendar() {
+ return mTimeModel.getCalendar();
+ }
+
+ //
+ // Ringtones
+ //
+
+ /**
+ * Ringtone titles are cached because loading them is expensive. This method
+ * <strong>must</strong> be called on a background thread and is responsible for priming the
+ * cache of ringtone titles to avoid later fetching titles on the main thread.
+ */
+ public void loadRingtoneTitles() {
+ enforceNotMainLooper();
+ mRingtoneModel.loadRingtoneTitles();
+ }
+
+ /**
+ * Recheck the permission to read each custom ringtone.
+ */
+ public void loadRingtonePermissions() {
+ enforceNotMainLooper();
+ mRingtoneModel.loadRingtonePermissions();
+ }
+
+ /**
+ * @param uri the uri of a ringtone
+ * @return the title of the ringtone with the {@code uri}; {@code null} if it cannot be fetched
+ */
+ public String getRingtoneTitle(Uri uri) {
+ enforceMainLooper();
+ return mRingtoneModel.getRingtoneTitle(uri);
+ }
+
+ /**
+ * @param uri the uri of an audio file to use as a ringtone
+ * @param title the title of the audio content at the given {@code uri}
+ * @return the ringtone instance created for the audio file
+ */
+ public CustomRingtone addCustomRingtone(Uri uri, String title) {
+ enforceMainLooper();
+ return mRingtoneModel.addCustomRingtone(uri, title);
+ }
+
+ /**
+ * @param uri identifies the ringtone to remove
+ */
+ public void removeCustomRingtone(Uri uri) {
+ enforceMainLooper();
+ mRingtoneModel.removeCustomRingtone(uri);
+ }
+
+ /**
+ * @return all available custom ringtones
+ */
+ public List<CustomRingtone> getCustomRingtones() {
+ enforceMainLooper();
+ return mRingtoneModel.getCustomRingtones();
+ }
+
+ //
// Widgets
//
@@ -701,6 +937,37 @@
//
/**
+ * @param silentSettingsListener to be notified when alarm-silencing settings change
+ */
+ public void addSilentSettingsListener(OnSilentSettingsListener silentSettingsListener) {
+ enforceMainLooper();
+ mSilentSettingsModel.addSilentSettingsListener(silentSettingsListener);
+ }
+
+ /**
+ * @param silentSettingsListener to no longer be notified when alarm-silencing settings change
+ */
+ public void removeSilentSettingsListener(OnSilentSettingsListener silentSettingsListener) {
+ enforceMainLooper();
+ mSilentSettingsModel.removeSilentSettingsListener(silentSettingsListener);
+ }
+
+ /**
+ * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ public int getGlobalIntentId() {
+ return mSettingsModel.getGlobalIntentId();
+ }
+
+ /**
+ * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ public void updateGlobalIntentId() {
+ enforceMainLooper();
+ mSettingsModel.updateGlobalIntentId();
+ }
+
+ /**
* @return the style of clock to display in the clock application
*/
public ClockStyle getClockStyle() {
@@ -709,6 +976,22 @@
}
/**
+ * @return the style of clock to display in the clock application
+ */
+ public boolean getDisplayClockSeconds() {
+ enforceMainLooper();
+ return mSettingsModel.getDisplayClockSeconds();
+ }
+
+ /**
+ * @param displaySeconds whether or not to display seconds for main clock
+ */
+ public void setDisplayClockSeconds(boolean displaySeconds) {
+ enforceMainLooper();
+ mSettingsModel.setDisplayClockSeconds(displaySeconds);
+ }
+
+ /**
* @return the style of clock to display in the clock screensaver
*/
public ClockStyle getScreensaverClockStyle() {
@@ -734,6 +1017,37 @@
}
/**
+ * @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
+ * {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
+ */
+ public Weekdays.Order getWeekdayOrder() {
+ enforceMainLooper();
+ return mSettingsModel.getWeekdayOrder();
+ }
+
+ /**
+ * @return {@code true} if the restore process (of backup and restore) has completed
+ */
+ public boolean isRestoreBackupFinished() {
+ return mSettingsModel.isRestoreBackupFinished();
+ }
+
+ /**
+ * @param finished {@code true} means the restore process (of backup and restore) has completed
+ */
+ public void setRestoreBackupFinished(boolean finished) {
+ mSettingsModel.setRestoreBackupFinished(finished);
+ }
+
+ /**
+ * @return a description of the time zones available for selection
+ */
+ public TimeZones getTimeZones() {
+ enforceMainLooper();
+ return mSettingsModel.getTimeZones();
+ }
+
+ /**
* Used to execute a delegate runnable and track its completion.
*/
private static class ExecutedRunnable implements Runnable {
diff --git a/src/com/android/deskclock/data/NotificationModel.java b/src/com/android/deskclock/data/NotificationModel.java
index f722fb9..b0b00f3 100644
--- a/src/com/android/deskclock/data/NotificationModel.java
+++ b/src/com/android/deskclock/data/NotificationModel.java
@@ -43,6 +43,7 @@
// Used elsewhere:
// Integer.MAX_VALUE - 4
// Integer.MAX_VALUE - 5
+ // Integer.MAX_VALUE - 7
//
/**
diff --git a/src/com/android/deskclock/uidata/OnAppColorChangeListener.java b/src/com/android/deskclock/data/OnSilentSettingsListener.java
similarity index 65%
rename from src/com/android/deskclock/uidata/OnAppColorChangeListener.java
rename to src/com/android/deskclock/data/OnSilentSettingsListener.java
index ef26d68..783b6e0 100644
--- a/src/com/android/deskclock/uidata/OnAppColorChangeListener.java
+++ b/src/com/android/deskclock/data/OnSilentSettingsListener.java
@@ -14,18 +14,12 @@
* limitations under the License.
*/
-package com.android.deskclock.uidata;
-
-import android.support.annotation.ColorInt;
+package com.android.deskclock.data;
/**
- * The interface through which interested parties are notified of changes to the app window.
+ * The interface through which interested parties are notified of changes to device settings that
+ * silence firing alarms.
*/
-public interface OnAppColorChangeListener {
-
- /**
- * @param oldColor the prior color of the app window
- * @param newColor the new color of the app window
- */
- void onAppColorChange(@ColorInt int oldColor, @ColorInt int newColor);
+public interface OnSilentSettingsListener {
+ void onSilentSettingsChange(DataModel.SilentSetting before, DataModel.SilentSetting after);
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/RingtoneModel.java b/src/com/android/deskclock/data/RingtoneModel.java
new file mode 100644
index 0000000..90b7f91
--- /dev/null
+++ b/src/com/android/deskclock/data/RingtoneModel.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2016 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.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.UriPermission;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.deskclock.LogUtils;
+import com.android.deskclock.R;
+import com.android.deskclock.provider.Alarm;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+
+import static android.media.AudioManager.STREAM_ALARM;
+import static android.media.RingtoneManager.TITLE_COLUMN_INDEX;
+
+/**
+ * All ringtone data is accessed via this model.
+ */
+final class RingtoneModel {
+
+ private final Context mContext;
+
+ private final SharedPreferences mPrefs;
+
+ /** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive. */
+ private final Map<Uri, String> mRingtoneTitles = new ArrayMap<>(16);
+
+ /** Clears data structures containing data that is locale-sensitive. */
+ @SuppressWarnings("FieldCanBeLocal")
+ private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
+
+ /** A mutable copy of the custom ringtones. */
+ private List<CustomRingtone> mCustomRingtones;
+
+ RingtoneModel(Context context, SharedPreferences prefs) {
+ mContext = context;
+ mPrefs = prefs;
+
+ // Clear caches affected by system settings when system settings change.
+ final ContentResolver cr = mContext.getContentResolver();
+ final ContentObserver observer = new SystemAlarmAlertChangeObserver();
+ cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer);
+
+ // Clear caches affected by locale when locale changes.
+ final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+ mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
+ }
+
+ CustomRingtone addCustomRingtone(Uri uri, String title) {
+ // If the uri is already present in an existing ringtone, do nothing.
+ final CustomRingtone existing = getCustomRingtone(uri);
+ if (existing != null) {
+ return existing;
+ }
+
+ final CustomRingtone ringtone = CustomRingtoneDAO.addCustomRingtone(mPrefs, uri, title);
+ getMutableCustomRingtones().add(ringtone);
+ Collections.sort(getMutableCustomRingtones());
+ return ringtone;
+ }
+
+ void removeCustomRingtone(Uri uri) {
+ final List<CustomRingtone> ringtones = getMutableCustomRingtones();
+ for (CustomRingtone ringtone : ringtones) {
+ if (ringtone.getUri().equals(uri)) {
+ CustomRingtoneDAO.removeCustomRingtone(mPrefs, ringtone.getId());
+ ringtones.remove(ringtone);
+ break;
+ }
+ }
+ }
+
+ private CustomRingtone getCustomRingtone(Uri uri) {
+ for (CustomRingtone ringtone : getMutableCustomRingtones()) {
+ if (ringtone.getUri().equals(uri)) {
+ return ringtone;
+ }
+ }
+
+ return null;
+ }
+
+ List<CustomRingtone> getCustomRingtones() {
+ return Collections.unmodifiableList(getMutableCustomRingtones());
+ }
+
+ @SuppressLint("NewApi")
+ void loadRingtonePermissions() {
+ final List<CustomRingtone> ringtones = getMutableCustomRingtones();
+ if (ringtones.isEmpty()) {
+ return;
+ }
+
+ final List<UriPermission> uriPermissions =
+ mContext.getContentResolver().getPersistedUriPermissions();
+ final Set<Uri> permissions = new ArraySet<>(uriPermissions.size());
+ for (UriPermission uriPermission : uriPermissions) {
+ permissions.add(uriPermission.getUri());
+ }
+
+ for (ListIterator<CustomRingtone> i = ringtones.listIterator(); i.hasNext();) {
+ final CustomRingtone ringtone = i.next();
+ i.set(ringtone.setHasPermissions(permissions.contains(ringtone.getUri())));
+ }
+ }
+
+ void loadRingtoneTitles() {
+ // Early return if the cache is already primed.
+ if (!mRingtoneTitles.isEmpty()) {
+ return;
+ }
+
+ final RingtoneManager ringtoneManager = new RingtoneManager(mContext);
+ ringtoneManager.setType(STREAM_ALARM);
+
+ // Cache a title for each system ringtone.
+ try (Cursor cursor = ringtoneManager.getCursor()) {
+ for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+ final String ringtoneTitle = cursor.getString(TITLE_COLUMN_INDEX);
+ final Uri ringtoneUri = ringtoneManager.getRingtoneUri(cursor.getPosition());
+ mRingtoneTitles.put(ringtoneUri, ringtoneTitle);
+ }
+ } catch (Throwable ignored) {
+ // best attempt only
+ LogUtils.e("Error loading ringtone title cache", ignored);
+ }
+ }
+
+ String getRingtoneTitle(Uri uri) {
+ // Special case: no ringtone has a title of "Silent".
+ if (Alarm.NO_RINGTONE_URI.equals(uri)) {
+ return mContext.getString(R.string.silent_ringtone_title);
+ }
+
+ // If the ringtone is custom, it has its own title.
+ final CustomRingtone customRingtone = getCustomRingtone(uri);
+ if (customRingtone != null) {
+ return customRingtone.getTitle();
+ }
+
+ // Check the cache.
+ String title = mRingtoneTitles.get(uri);
+
+ if (title == null) {
+ // This is slow because a media player is created during Ringtone object creation.
+ final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri);
+ if (ringtone == null) {
+ LogUtils.e("No ringtone for uri: %s", uri);
+ return mContext.getString(R.string.unknown_ringtone_title);
+ }
+
+ // Cache the title for later use.
+ title = ringtone.getTitle(mContext);
+ mRingtoneTitles.put(uri, title);
+ }
+ return title;
+ }
+
+ private List<CustomRingtone> getMutableCustomRingtones() {
+ if (mCustomRingtones == null) {
+ mCustomRingtones = CustomRingtoneDAO.getCustomRingtones(mPrefs);
+ Collections.sort(mCustomRingtones);
+ }
+
+ return mCustomRingtones;
+ }
+
+ /**
+ * This receiver is notified when system settings change. Cached information built on
+ * those system settings must be cleared.
+ */
+ private final class SystemAlarmAlertChangeObserver extends ContentObserver {
+
+ private SystemAlarmAlertChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+
+ // Titles such as "Default ringtone (Oxygen)" are wrong after default ringtone changes.
+ mRingtoneTitles.clear();
+ }
+ }
+
+ /**
+ * Cached information that is locale-sensitive must be cleared in response to locale changes.
+ */
+ private final class LocaleChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Titles such as "Default ringtone (Oxygen)" are wrong after locale changes.
+ mRingtoneTitles.clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SettingsDAO.java b/src/com/android/deskclock/data/SettingsDAO.java
index b609293..7e9408f 100644
--- a/src/com/android/deskclock/data/SettingsDAO.java
+++ b/src/com/android/deskclock/data/SettingsDAO.java
@@ -18,18 +18,36 @@
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.net.Uri;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.text.format.DateUtils;
import com.android.deskclock.R;
-import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
import com.android.deskclock.data.DataModel.CitySort;
import com.android.deskclock.data.DataModel.ClockStyle;
import com.android.deskclock.settings.ScreensaverSettingsActivity;
import com.android.deskclock.settings.SettingsActivity;
+import java.util.Arrays;
+import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.DISMISS;
+import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.NOTHING;
+import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.SNOOZE;
+import static com.android.deskclock.data.Weekdays.Order.MON_TO_SUN;
+import static com.android.deskclock.data.Weekdays.Order.SAT_TO_FRI;
+import static com.android.deskclock.data.Weekdays.Order.SUN_TO_SAT;
+import static java.util.Calendar.MONDAY;
+import static java.util.Calendar.SATURDAY;
+import static java.util.Calendar.SUNDAY;
+
/**
* This class encapsulates the storage of application preferences in {@link SharedPreferences}.
*/
@@ -41,14 +59,34 @@
/** Key to a preference that stores the default ringtone for new alarms. */
private static final String KEY_DEFAULT_ALARM_RINGTONE_URI = "default_alarm_ringtone_uri";
+ /** Key to a preference that stores the global broadcast id. */
+ private static final String KEY_ALARM_GLOBAL_ID = "intent.extra.alarm.global.id";
+
+ /** Key to a preference that indicates whether restore (of backup and restore) has completed. */
+ private static final String KEY_RESTORE_BACKUP_FINISHED = "restore_finished";
+
private SettingsDAO() {}
/**
+ * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ static int getGlobalIntentId(SharedPreferences prefs) {
+ return prefs.getInt(KEY_ALARM_GLOBAL_ID, -1);
+ }
+
+ /**
+ * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ static void updateGlobalIntentId(SharedPreferences prefs) {
+ final int globalId = prefs.getInt(KEY_ALARM_GLOBAL_ID, -1) + 1;
+ prefs.edit().putInt(KEY_ALARM_GLOBAL_ID, globalId).apply();
+ }
+
+ /**
* @return an enumerated value indicating the order in which cities are ordered
*/
- static CitySort getCitySort(Context context) {
+ static CitySort getCitySort(SharedPreferences prefs) {
final int defaultSortOrdinal = CitySort.NAME.ordinal();
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
final int citySortOrdinal = prefs.getInt(KEY_SORT_PREFERENCE, defaultSortOrdinal);
return CitySort.values()[citySortOrdinal];
}
@@ -56,10 +94,9 @@
/**
* Adjust the sort order of cities.
*/
- static void toggleCitySort(Context context) {
- final CitySort oldSort = getCitySort(context);
+ static void toggleCitySort(SharedPreferences prefs) {
+ final CitySort oldSort = getCitySort(prefs);
final CitySort newSort = oldSort == CitySort.NAME ? CitySort.UTC_OFFSET : CitySort.NAME;
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
prefs.edit().putInt(KEY_SORT_PREFERENCE, newSort.ordinal()).apply();
}
@@ -67,53 +104,77 @@
* @return {@code true} if a clock for the user's home timezone should be automatically
* displayed when it doesn't match the current timezone
*/
- static boolean getAutoShowHomeClock(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
- return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, false);
+ static boolean getAutoShowHomeClock(SharedPreferences prefs) {
+ return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, true);
}
/**
* @return the user's home timezone
*/
- static TimeZone getHomeTimeZone(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
- final String defaultTimeZoneId = TimeZone.getDefault().getID();
- final String timeZoneId = prefs.getString(SettingsActivity.KEY_HOME_TZ, defaultTimeZoneId);
- return TimeZone.getTimeZone(timeZoneId);
- }
+ static TimeZone getHomeTimeZone(Context context, SharedPreferences prefs, TimeZone defaultTZ) {
+ String timeZoneId = prefs.getString(SettingsActivity.KEY_HOME_TZ, null);
- /**
- * Sets the user's home timezone to the current system timezone if no home timezone is yet set.
- *
- * @param homeTimeZone the timezone to set as the user's home timezone if necessary
- */
- static void setDefaultHomeTimeZone(Context context, TimeZone homeTimeZone) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
- final String homeTimeZoneId = prefs.getString(SettingsActivity.KEY_HOME_TZ, null);
- if (homeTimeZoneId == null) {
- prefs.edit().putString(SettingsActivity.KEY_HOME_TZ, homeTimeZone.getID()).apply();
+ // If the recorded home timezone is legal, use it.
+ final TimeZones timeZones = getTimeZones(context, System.currentTimeMillis());
+ if (timeZones.contains(timeZoneId)) {
+ return TimeZone.getTimeZone(timeZoneId);
}
+
+ // No legal home timezone has yet been recorded, attempt to record the default.
+ timeZoneId = defaultTZ.getID();
+ if (timeZones.contains(timeZoneId)) {
+ prefs.edit().putString(SettingsActivity.KEY_HOME_TZ, timeZoneId).apply();
+ }
+
+ // The timezone returned here may be valid or invalid. When it matches TimeZone.getDefault()
+ // the Home city will not show, regardless of its validity.
+ return defaultTZ;
}
/**
* @return a value indicating whether analog or digital clocks are displayed in the app
*/
- static ClockStyle getClockStyle(Context context) {
- return getClockStyle(context, SettingsActivity.KEY_CLOCK_STYLE);
+ static ClockStyle getClockStyle(Context context, SharedPreferences prefs) {
+ return getClockStyle(context, prefs, SettingsActivity.KEY_CLOCK_STYLE);
+ }
+
+ /**
+ * @return a value indicating whether analog or digital clocks are displayed in the app
+ */
+ static boolean getDisplayClockSeconds(SharedPreferences prefs) {
+ return prefs.getBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, false);
+ }
+
+ /**
+ * @param displaySeconds whether or not to display seconds on main clock
+ */
+ static void setDisplayClockSeconds(SharedPreferences prefs, boolean displaySeconds) {
+ prefs.edit().putBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, displaySeconds).apply();
+ }
+
+ /**
+ * Sets the user's display seconds preference based on the currently selected clock if one has
+ * not yet been manually chosen.
+ */
+ static void setDefaultDisplayClockSeconds(Context context, SharedPreferences prefs) {
+ if (!prefs.contains(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS)) {
+ // If on analog clock style on upgrade, default to true. Otherwise, default to false.
+ final boolean isAnalog = getClockStyle(context, prefs) == ClockStyle.ANALOG;
+ setDisplayClockSeconds(prefs, isAnalog);
+ }
}
/**
* @return a value indicating whether analog or digital clocks are displayed on the screensaver
*/
- static ClockStyle getScreensaverClockStyle(Context context) {
- return getClockStyle(context, ScreensaverSettingsActivity.KEY_CLOCK_STYLE);
+ static ClockStyle getScreensaverClockStyle(Context context, SharedPreferences prefs) {
+ return getClockStyle(context, prefs, ScreensaverSettingsActivity.KEY_CLOCK_STYLE);
}
/**
* @return {@code true} if the screen saver should be dimmed for lower contrast at night
*/
- static boolean getScreensaverNightModeOn(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static boolean getScreensaverNightModeOn(SharedPreferences prefs) {
return prefs.getBoolean(ScreensaverSettingsActivity.KEY_NIGHT_MODE, false);
}
@@ -121,8 +182,7 @@
* @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
* has yet been made
*/
- static Uri getTimerRingtoneUri(Context context, Uri defaultUri) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static Uri getTimerRingtoneUri(SharedPreferences prefs, Uri defaultUri) {
final String uriString = prefs.getString(SettingsActivity.KEY_TIMER_RINGTONE, null);
return uriString == null ? defaultUri : Uri.parse(uriString);
}
@@ -130,24 +190,21 @@
/**
* @return whether timer vibration is enabled. false by default.
*/
- static boolean getTimerVibrate(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static boolean getTimerVibrate(SharedPreferences prefs) {
return prefs.getBoolean(SettingsActivity.KEY_TIMER_VIBRATE, false);
}
/**
* @param enabled whether vibration will be turned on for all timers.
*/
- static void setTimerVibrate(Context context, boolean enabled) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void setTimerVibrate(SharedPreferences prefs, boolean enabled) {
prefs.edit().putBoolean(SettingsActivity.KEY_TIMER_VIBRATE, enabled).apply();
}
/**
* @param uri the uri of the ringtone to play for all timers
*/
- static void setTimerRingtoneUri(Context context, Uri uri) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void setTimerRingtoneUri(SharedPreferences prefs, Uri uri) {
prefs.edit().putString(SettingsActivity.KEY_TIMER_RINGTONE, uri.toString()).apply();
}
@@ -155,25 +212,176 @@
* @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
* has yet been made
*/
- static Uri getDefaultAlarmRingtoneUri(Context context, Uri defaultUri) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static Uri getDefaultAlarmRingtoneUri(SharedPreferences prefs) {
final String uriString = prefs.getString(KEY_DEFAULT_ALARM_RINGTONE_URI, null);
- return uriString == null ? defaultUri : Uri.parse(uriString);
+ return uriString == null ? Settings.System.DEFAULT_ALARM_ALERT_URI : Uri.parse(uriString);
}
+
/**
* @param uri identifies the default ringtone to play for new alarms
*/
- static void setDefaultAlarmRingtoneUri(Context context, Uri uri) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void setDefaultAlarmRingtoneUri(SharedPreferences prefs, Uri uri) {
prefs.edit().putString(KEY_DEFAULT_ALARM_RINGTONE_URI, uri.toString()).apply();
}
- private static ClockStyle getClockStyle(Context context, String prefKey) {
+ /**
+ * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
+ * {@code 0} implies no crescendo should be applied
+ */
+ static long getAlarmCrescendoDuration(SharedPreferences prefs) {
+ final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_ALARM_CRESCENDO, "0");
+ return Integer.parseInt(crescendoSeconds) * DateUtils.SECOND_IN_MILLIS;
+ }
+
+ /**
+ * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+ * {@code 0} implies no crescendo should be applied
+ */
+ static long getTimerCrescendoDuration(SharedPreferences prefs) {
+ final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_TIMER_CRESCENDO, "0");
+ return Integer.parseInt(crescendoSeconds) * DateUtils.SECOND_IN_MILLIS;
+ }
+
+ /**
+ * @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
+ * {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
+ */
+ static Weekdays.Order getWeekdayOrder(SharedPreferences prefs) {
+ final String defaultValue = String.valueOf(Calendar.getInstance().getFirstDayOfWeek());
+ final String value = prefs.getString(SettingsActivity.KEY_WEEK_START, defaultValue);
+ final int firstCalendarDay = Integer.parseInt(value);
+ switch (firstCalendarDay) {
+ case SATURDAY: return SAT_TO_FRI;
+ case SUNDAY: return SUN_TO_SAT;
+ case MONDAY: return MON_TO_SUN;
+ default:
+ throw new IllegalArgumentException("Unknown weekday: " + firstCalendarDay);
+ }
+ }
+
+ /**
+ * @return {@code true} if the restore process (of backup and restore) has completed
+ */
+ static boolean isRestoreBackupFinished(SharedPreferences prefs) {
+ return prefs.getBoolean(KEY_RESTORE_BACKUP_FINISHED, false);
+ }
+
+ /**
+ * @param finished {@code true} means the restore process (of backup and restore) has completed
+ */
+ static void setRestoreBackupFinished(SharedPreferences prefs, boolean finished) {
+ if (finished) {
+ prefs.edit().putBoolean(KEY_RESTORE_BACKUP_FINISHED, true).apply();
+ } else {
+ prefs.edit().remove(KEY_RESTORE_BACKUP_FINISHED).apply();
+ }
+ }
+
+ /**
+ * @return the behavior to execute when volume buttons are pressed while firing an alarm
+ */
+ static AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior(SharedPreferences prefs) {
+ final String defaultValue = SettingsActivity.DEFAULT_VOLUME_BEHAVIOR;
+ final String value = prefs.getString(SettingsActivity.KEY_VOLUME_BUTTONS, defaultValue);
+ switch (value) {
+ case SettingsActivity.DEFAULT_VOLUME_BEHAVIOR: return NOTHING;
+ case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE: return SNOOZE;
+ case SettingsActivity.VOLUME_BEHAVIOR_DISMISS: return DISMISS;
+ default:
+ throw new IllegalArgumentException("Unknown volume button behavior: " + value);
+ }
+ }
+
+ /**
+ * @return the number of minutes an alarm may ring before it has timed out and becomes missed
+ */
+ static int getAlarmTimeout(SharedPreferences prefs) {
+ // Default value must match the one in res/xml/settings.xml
+ final String string = prefs.getString(SettingsActivity.KEY_AUTO_SILENCE, "10");
+ return Integer.parseInt(string);
+ }
+
+ /**
+ * @return the number of minutes an alarm will remain snoozed before it rings again
+ */
+ static int getSnoozeLength(SharedPreferences prefs) {
+ // Default value must match the one in res/xml/settings.xml
+ final String string = prefs.getString(SettingsActivity.KEY_ALARM_SNOOZE, "10");
+ return Integer.parseInt(string);
+ }
+
+ /**
+ * @param currentTime timezone offsets created relative to this time
+ * @return a description of the time zones available for selection
+ */
+ static TimeZones getTimeZones(Context context, long currentTime) {
+ final Locale locale = Locale.getDefault();
+ final Resources resources = context.getResources();
+ final String[] timeZoneIds = resources.getStringArray(R.array.timezone_values);
+ final String[] timeZoneNames = resources.getStringArray(R.array.timezone_labels);
+
+ // Verify the data is consistent.
+ if (timeZoneIds.length != timeZoneNames.length) {
+ final String message = String.format(Locale.US,
+ "id count (%d) does not match name count (%d) for locale %s",
+ timeZoneIds.length, timeZoneNames.length, locale);
+ throw new IllegalStateException(message);
+ }
+
+ // Create TimeZoneDescriptors for each TimeZone so they can be sorted.
+ final TimeZoneDescriptor[] descriptors = new TimeZoneDescriptor[timeZoneIds.length];
+ for (int i = 0; i < timeZoneIds.length; i++) {
+ final String id = timeZoneIds[i];
+ final String name = timeZoneNames[i].replaceAll("\"", "");
+ descriptors[i] = new TimeZoneDescriptor(locale, id, name, currentTime);
+ }
+ Arrays.sort(descriptors);
+
+ // Transfer the TimeZoneDescriptors into parallel arrays for easy consumption by the caller.
+ final CharSequence[] tzIds = new CharSequence[descriptors.length];
+ final CharSequence[] tzNames = new CharSequence[descriptors.length];
+ for (int i = 0; i < descriptors.length; i++) {
+ final TimeZoneDescriptor descriptor = descriptors[i];
+ tzIds[i] = descriptor.mTimeZoneId;
+ tzNames[i] = descriptor.mTimeZoneName;
+ }
+
+ return new TimeZones(tzIds, tzNames);
+ }
+
+ private static ClockStyle getClockStyle(Context context, SharedPreferences prefs, String key) {
final String defaultStyle = context.getString(R.string.default_clock_style);
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
- final String clockStyle = prefs.getString(prefKey, defaultStyle);
+ final String clockStyle = prefs.getString(key, defaultStyle);
// Use hardcoded locale to perform toUpperCase, because in some languages toUpperCase adds
// accent to character, which breaks the enum conversion.
return ClockStyle.valueOf(clockStyle.toUpperCase(Locale.US));
}
+
+ /**
+ * These descriptors have a natural order from furthest ahead of GMT to furthest behind GMT.
+ */
+ private static class TimeZoneDescriptor implements Comparable<TimeZoneDescriptor> {
+
+ private final int mOffset;
+ private final String mTimeZoneId;
+ private final String mTimeZoneName;
+
+ private TimeZoneDescriptor(Locale locale, String id, String name, long currentTime) {
+ mTimeZoneId = id;
+
+ final TimeZone tz = TimeZone.getTimeZone(id);
+ mOffset = tz.getOffset(currentTime);
+
+ final char sign = mOffset < 0 ? '-' : '+';
+ final int absoluteGMTOffset = Math.abs(mOffset);
+ final long hour = absoluteGMTOffset / HOUR_IN_MILLIS;
+ final long minute = (absoluteGMTOffset / MINUTE_IN_MILLIS) % 60;
+ mTimeZoneName = String.format(locale, "(GMT%s%d:%02d) %s", sign, hour, minute, name);
+ }
+
+ @Override
+ public int compareTo(@NonNull TimeZoneDescriptor other) {
+ return mOffset - other.mOffset;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SettingsModel.java b/src/com/android/deskclock/data/SettingsModel.java
index caeef93..103c210 100644
--- a/src/com/android/deskclock/data/SettingsModel.java
+++ b/src/com/android/deskclock/data/SettingsModel.java
@@ -17,11 +17,12 @@
package com.android.deskclock.data;
import android.content.Context;
+import android.content.SharedPreferences;
import android.net.Uri;
-import android.provider.Settings;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
import com.android.deskclock.data.DataModel.CitySort;
import com.android.deskclock.data.DataModel.ClockStyle;
@@ -34,50 +35,74 @@
private final Context mContext;
+ private final SharedPreferences mPrefs;
+
+ /** The model from which time data are fetched. */
+ private final TimeModel mTimeModel;
+
/** The uri of the default ringtone to use for timers until the user explicitly chooses one. */
private Uri mDefaultTimerRingtoneUri;
- SettingsModel(Context context) {
+ SettingsModel(Context context, SharedPreferences prefs, TimeModel timeModel) {
mContext = context;
+ mPrefs = prefs;
+ mTimeModel = timeModel;
- // Set the user's default home timezone if one has not yet been chosen.
- SettingsDAO.setDefaultHomeTimeZone(mContext, TimeZone.getDefault());
+ // Set the user's default display seconds preference if one has not yet been chosen.
+ SettingsDAO.setDefaultDisplayClockSeconds(mContext, prefs);
+ }
+
+ int getGlobalIntentId() {
+ return SettingsDAO.getGlobalIntentId(mPrefs);
+ }
+
+ void updateGlobalIntentId() {
+ SettingsDAO.updateGlobalIntentId(mPrefs);
}
CitySort getCitySort() {
- return SettingsDAO.getCitySort(mContext);
+ return SettingsDAO.getCitySort(mPrefs);
}
void toggleCitySort() {
- SettingsDAO.toggleCitySort(mContext);
+ SettingsDAO.toggleCitySort(mPrefs);
}
TimeZone getHomeTimeZone() {
- return SettingsDAO.getHomeTimeZone(mContext);
+ return SettingsDAO.getHomeTimeZone(mContext, mPrefs, TimeZone.getDefault());
}
ClockStyle getClockStyle() {
- return SettingsDAO.getClockStyle(mContext);
+ return SettingsDAO.getClockStyle(mContext, mPrefs);
+ }
+
+ boolean getDisplayClockSeconds() {
+ return SettingsDAO.getDisplayClockSeconds(mPrefs);
+ }
+
+ void setDisplayClockSeconds(boolean shouldDisplaySeconds) {
+ SettingsDAO.setDisplayClockSeconds(mPrefs, shouldDisplaySeconds);
}
ClockStyle getScreensaverClockStyle() {
- return SettingsDAO.getScreensaverClockStyle(mContext);
+ return SettingsDAO.getScreensaverClockStyle(mContext, mPrefs);
}
boolean getScreensaverNightModeOn() {
- return SettingsDAO.getScreensaverNightModeOn(mContext);
+ return SettingsDAO.getScreensaverNightModeOn(mPrefs);
}
boolean getShowHomeClock() {
- if (!SettingsDAO.getAutoShowHomeClock(mContext)) {
+ if (!SettingsDAO.getAutoShowHomeClock(mPrefs)) {
return false;
}
// Show the home clock if the current time and home time differ.
// (By using UTC offset for this comparison the various DST rules are considered)
- final TimeZone homeTimeZone = SettingsDAO.getHomeTimeZone(mContext);
+ final TimeZone defaultTZ = TimeZone.getDefault();
+ final TimeZone homeTimeZone = SettingsDAO.getHomeTimeZone(mContext, mPrefs, defaultTZ);
final long now = System.currentTimeMillis();
- return homeTimeZone.getOffset(now) != TimeZone.getDefault().getOffset(now);
+ return homeTimeZone.getOffset(now) != defaultTZ.getOffset(now);
}
Uri getDefaultTimerRingtoneUri() {
@@ -89,27 +114,62 @@
}
void setTimerRingtoneUri(Uri uri) {
- SettingsDAO.setTimerRingtoneUri(mContext, uri);
+ SettingsDAO.setTimerRingtoneUri(mPrefs, uri);
}
Uri getTimerRingtoneUri() {
- return SettingsDAO.getTimerRingtoneUri(mContext, getDefaultTimerRingtoneUri());
+ return SettingsDAO.getTimerRingtoneUri(mPrefs, getDefaultTimerRingtoneUri());
+ }
+
+ AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
+ return SettingsDAO.getAlarmVolumeButtonBehavior(mPrefs);
+ }
+
+ int getAlarmTimeout() {
+ return SettingsDAO.getAlarmTimeout(mPrefs);
+ }
+
+ int getSnoozeLength() {
+ return SettingsDAO.getSnoozeLength(mPrefs);
}
Uri getDefaultAlarmRingtoneUri() {
- return SettingsDAO.getDefaultAlarmRingtoneUri(mContext,
- Settings.System.DEFAULT_ALARM_ALERT_URI);
+ return SettingsDAO.getDefaultAlarmRingtoneUri(mPrefs);
}
void setDefaultAlarmRingtoneUri(Uri uri) {
- SettingsDAO.setDefaultAlarmRingtoneUri(mContext, uri);
+ SettingsDAO.setDefaultAlarmRingtoneUri(mPrefs, uri);
+ }
+
+ long getAlarmCrescendoDuration() {
+ return SettingsDAO.getAlarmCrescendoDuration(mPrefs);
+ }
+
+ long getTimerCrescendoDuration() {
+ return SettingsDAO.getTimerCrescendoDuration(mPrefs);
+ }
+
+ Weekdays.Order getWeekdayOrder() {
+ return SettingsDAO.getWeekdayOrder(mPrefs);
+ }
+
+ boolean isRestoreBackupFinished() {
+ return SettingsDAO.isRestoreBackupFinished(mPrefs);
+ }
+
+ void setRestoreBackupFinished(boolean finished) {
+ SettingsDAO.setRestoreBackupFinished(mPrefs, finished);
}
boolean getTimerVibrate() {
- return SettingsDAO.getTimerVibrate(mContext);
+ return SettingsDAO.getTimerVibrate(mPrefs);
}
void setTimerVibrate(boolean enabled) {
- SettingsDAO.setTimerVibrate(mContext, enabled);
+ SettingsDAO.setTimerVibrate(mPrefs, enabled);
+ }
+
+ TimeZones getTimeZones() {
+ return SettingsDAO.getTimeZones(mContext, mTimeModel.currentTimeMillis());
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SilentSettingsModel.java b/src/com/android/deskclock/data/SilentSettingsModel.java
new file mode 100644
index 0000000..4e41dfe
--- /dev/null
+++ b/src/com/android/deskclock/data/SilentSettingsModel.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2016 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.annotation.TargetApi;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Handler;
+import android.support.v4.app.NotificationManagerCompat;
+
+import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel.SilentSetting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
+import static android.content.Context.AUDIO_SERVICE;
+import static android.content.Context.NOTIFICATION_SERVICE;
+import static android.media.AudioManager.STREAM_ALARM;
+import static android.media.RingtoneManager.TYPE_ALARM;
+import static android.provider.Settings.System.CONTENT_URI;
+import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
+
+/**
+ * This model fetches and stores reasons that alarms may be suppressed or silenced by system
+ * settings on the device. This information is displayed passively to notify the user of this
+ * condition and set their expectations for future firing alarms.
+ */
+final class SilentSettingsModel {
+
+ /** The Uri to the settings entry that stores alarm stream volume. */
+ private static final Uri VOLUME_URI = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker");
+
+ private final Context mContext;
+
+ /** Used to query the alarm volume and display the system control to change the alarm volume. */
+ private final AudioManager mAudioManager;
+
+ /** Used to query the do-not-disturb setting value, also called "interruption filter". */
+ private final NotificationManager mNotificationManager;
+
+ /** Used to determine if the application is in the foreground. */
+ private final NotificationModel mNotificationModel;
+
+ /** List of listeners to invoke upon silence state change. */
+ private final List<OnSilentSettingsListener> mListeners = new ArrayList<>(1);
+
+ /**
+ * The last setting known to be blocking alarms; {@code null} indicates no settings are
+ * blocking the app or the app is not in the foreground.
+ */
+ private SilentSetting mSilentSetting;
+
+ /** The background task that checks the device system settings that influence alarm firing. */
+ private CheckSilenceSettingsTask mCheckSilenceSettingsTask;
+
+ SilentSettingsModel(Context context, NotificationModel notificationModel) {
+ mContext = context;
+ mNotificationModel = notificationModel;
+
+ mAudioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
+ mNotificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
+
+ // Watch for changes to the settings that may silence alarms.
+ final ContentResolver cr = context.getContentResolver();
+ final ContentObserver contentChangeWatcher = new ContentChangeWatcher();
+ cr.registerContentObserver(VOLUME_URI, false, contentChangeWatcher);
+ cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, contentChangeWatcher);
+ if (Utils.isMOrLater()) {
+ final IntentFilter filter = new IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED);
+ context.registerReceiver(new DoNotDisturbChangeReceiver(), filter);
+ }
+ }
+
+ void addSilentSettingsListener(OnSilentSettingsListener listener) {
+ mListeners.add(listener);
+ }
+
+ void removeSilentSettingsListener(OnSilentSettingsListener listener) {
+ mListeners.remove(listener);
+ }
+
+ /**
+ * If the app is in the foreground, start a task to determine if any device setting will block
+ * alarms from firing. If the app is in the background, clear any results from the last time
+ * those settings were inspected.
+ */
+ void updateSilentState() {
+ // Cancel any task in flight, the result is no longer relevant.
+ if (mCheckSilenceSettingsTask != null) {
+ mCheckSilenceSettingsTask.cancel(true);
+ mCheckSilenceSettingsTask = null;
+ }
+
+ if (mNotificationModel.isApplicationInForeground()) {
+ mCheckSilenceSettingsTask = new CheckSilenceSettingsTask();
+ mCheckSilenceSettingsTask.execute();
+ } else {
+ setSilentState(null);
+ }
+ }
+
+ /**
+ * @param silentSetting the latest notion of which setting is suppressing alarms; {@code null}
+ * if no settings are suppressing alarms
+ */
+ private void setSilentState(SilentSetting silentSetting) {
+ if (mSilentSetting != silentSetting) {
+ final SilentSetting oldReason = mSilentSetting;
+ mSilentSetting = silentSetting;
+
+ for (OnSilentSettingsListener listener : mListeners) {
+ listener.onSilentSettingsChange(oldReason, silentSetting);
+ }
+ }
+ }
+
+ /**
+ * This task inspects a variety of system settings that can prevent alarms from firing or the
+ * associated ringtone from playing. If any of them would prevent an alarm from firing or
+ * making noise, a description of the setting is reported to this model on the main thread.
+ */
+ private final class CheckSilenceSettingsTask extends AsyncTask<Void, Void, SilentSetting> {
+ @Override
+ protected SilentSetting doInBackground(Void... parameters) {
+ if (!isCancelled() && isDoNotDisturbBlockingAlarms()) {
+ return SilentSetting.DO_NOT_DISTURB;
+ } else if (!isCancelled() && isAlarmStreamMuted()) {
+ return SilentSetting.MUTED_VOLUME;
+ } else if (!isCancelled() && isSystemAlarmRingtoneSilent()) {
+ return SilentSetting.SILENT_RINGTONE;
+ } else if (!isCancelled() && isAppNotificationBlocked()) {
+ return SilentSetting.BLOCKED_NOTIFICATIONS;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ if (mCheckSilenceSettingsTask == this) {
+ mCheckSilenceSettingsTask = null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(SilentSetting silentSetting) {
+ if (mCheckSilenceSettingsTask == this) {
+ mCheckSilenceSettingsTask = null;
+ setSilentState(silentSetting);
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ private boolean isDoNotDisturbBlockingAlarms() {
+ if (!Utils.isMOrLater()) {
+ return false;
+ }
+
+ try {
+ final int interruptionFilter = mNotificationManager.getCurrentInterruptionFilter();
+ return interruptionFilter == INTERRUPTION_FILTER_NONE;
+ } catch (Exception e) {
+ // Since this is purely informational, avoid crashing the app.
+ return false;
+ }
+ }
+
+ private boolean isAlarmStreamMuted() {
+ try {
+ return mAudioManager.getStreamVolume(STREAM_ALARM) <= 0;
+ } catch (Exception e) {
+ // Since this is purely informational, avoid crashing the app.
+ return false;
+ }
+ }
+
+ private boolean isSystemAlarmRingtoneSilent() {
+ try {
+ return RingtoneManager.getActualDefaultRingtoneUri(mContext, TYPE_ALARM) == null;
+ } catch (Exception e) {
+ // Since this is purely informational, avoid crashing the app.
+ return false;
+ }
+ }
+
+ private boolean isAppNotificationBlocked() {
+ try {
+ return !NotificationManagerCompat.from(mContext).areNotificationsEnabled();
+ } catch (Exception e) {
+ // Since this is purely informational, avoid crashing the app.
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Observe changes to specific URI for settings that can silence firing alarms.
+ */
+ private final class ContentChangeWatcher extends ContentObserver {
+ private ContentChangeWatcher() {
+ super(new Handler());
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ updateSilentState();
+ }
+ }
+
+ /**
+ * Observe changes to the do-not-disturb setting.
+ */
+ private final class DoNotDisturbChangeReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateSilentState();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Stopwatch.java b/src/com/android/deskclock/data/Stopwatch.java
index 11482b5..f53af38 100644
--- a/src/com/android/deskclock/data/Stopwatch.java
+++ b/src/com/android/deskclock/data/Stopwatch.java
@@ -111,6 +111,11 @@
return RESET_STOPWATCH;
}
+ /**
+ * @return this Stopwatch if it is not running or an updated version based on wallclock time.
+ * The internals of the stopwatch are updated using the wallclock time which is durable
+ * across reboots.
+ */
Stopwatch updateAfterReboot() {
if (mState != RUNNING) {
return this;
@@ -123,6 +128,11 @@
return new Stopwatch(mState, timeSinceBoot, wallClockTime, mAccumulatedTime + delta);
}
+ /**
+ * @return this Stopwatch if it is not running or an updated version based on the realtime.
+ * The internals of the stopwatch are updated using the realtime clock which is accurate
+ * across wallclock time adjustments.
+ */
Stopwatch updateAfterTimeSet() {
if (mState != RUNNING) {
return this;
@@ -138,4 +148,4 @@
}
return new Stopwatch(mState, timeSinceBoot, wallClockTime, mAccumulatedTime + delta);
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchDAO.java b/src/com/android/deskclock/data/StopwatchDAO.java
index 48ca3dd..5413901 100644
--- a/src/com/android/deskclock/data/StopwatchDAO.java
+++ b/src/com/android/deskclock/data/StopwatchDAO.java
@@ -16,10 +16,8 @@
package com.android.deskclock.data;
-import android.content.Context;
import android.content.SharedPreferences;
-import com.android.deskclock.Utils;
import com.android.deskclock.data.Stopwatch.State;
import java.util.ArrayList;
@@ -57,21 +55,26 @@
/**
* @return the stopwatch from permanent storage or a reset stopwatch if none exists
*/
- static Stopwatch getStopwatch(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static Stopwatch getStopwatch(SharedPreferences prefs) {
final int stateIndex = prefs.getInt(STATE, RESET.ordinal());
final State state = State.values()[stateIndex];
final long lastStartTime = prefs.getLong(LAST_START_TIME, Stopwatch.UNUSED);
final long lastWallClockTime = prefs.getLong(LAST_WALL_CLOCK_TIME, Stopwatch.UNUSED);
final long accumulatedTime = prefs.getLong(ACCUMULATED_TIME, 0);
- return new Stopwatch(state, lastStartTime, lastWallClockTime, accumulatedTime);
+ Stopwatch s = new Stopwatch(state, lastStartTime, lastWallClockTime, accumulatedTime);
+
+ // If the stopwatch reports an illegal (negative) amount of time, remove the bad data.
+ if (s.getTotalTime() < 0) {
+ s = s.reset();
+ setStopwatch(prefs, s);
+ }
+ return s;
}
/**
* @param stopwatch the last state of the stopwatch
*/
- static void setStopwatch(Context context, Stopwatch stopwatch) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void setStopwatch(SharedPreferences prefs, Stopwatch stopwatch) {
final SharedPreferences.Editor editor = prefs.edit();
if (stopwatch.isReset()) {
@@ -92,9 +95,7 @@
/**
* @return a list of recorded laps for the stopwatch
*/
- static List<Lap> getLaps(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
-
+ static List<Lap> getLaps(SharedPreferences prefs) {
// Prepare the container to be filled with laps.
final int lapCount = prefs.getInt(LAP_COUNT, 0);
final List<Lap> laps = new ArrayList<>(lapCount);
@@ -127,8 +128,7 @@
* @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
*/
- static void addLap(Context context, int newLapCount, long accumulatedTime) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void addLap(SharedPreferences prefs, int newLapCount, long accumulatedTime) {
prefs.edit()
.putInt(LAP_COUNT, newLapCount)
.putLong(LAP_ACCUMULATED_TIME + newLapCount, accumulatedTime)
@@ -138,8 +138,7 @@
/**
* Remove the recorded laps for the stopwatch
*/
- static void clearLaps(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void clearLaps(SharedPreferences prefs) {
final SharedPreferences.Editor editor = prefs.edit();
final int lapCount = prefs.getInt(LAP_COUNT, 0);
diff --git a/src/com/android/deskclock/data/StopwatchModel.java b/src/com/android/deskclock/data/StopwatchModel.java
index 13790e4..7d1e4ec 100644
--- a/src/com/android/deskclock/data/StopwatchModel.java
+++ b/src/com/android/deskclock/data/StopwatchModel.java
@@ -21,11 +21,10 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.SharedPreferences;
import android.support.annotation.VisibleForTesting;
import android.support.v4.app.NotificationManagerCompat;
-import com.android.deskclock.Utils;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -37,6 +36,8 @@
private final Context mContext;
+ private final SharedPreferences mPrefs;
+
/** The model from which notification data are fetched. */
private final NotificationModel mNotificationModel;
@@ -51,7 +52,8 @@
private final List<StopwatchListener> mStopwatchListeners = new ArrayList<>();
/** Delegate that builds platform-specific stopwatch notifications. */
- private NotificationBuilder mNotificationBuilder;
+ private final StopwatchNotificationBuilder mNotificationBuilder =
+ new StopwatchNotificationBuilder();
/** The current state of the stopwatch. */
private Stopwatch mStopwatch;
@@ -59,8 +61,9 @@
/** A mutable copy of the recorded stopwatch laps. */
private List<Lap> mLaps;
- StopwatchModel(Context context, NotificationModel notificationModel) {
+ StopwatchModel(Context context, SharedPreferences prefs, NotificationModel notificationModel) {
mContext = context;
+ mPrefs = prefs;
mNotificationModel = notificationModel;
mNotificationManager = NotificationManagerCompat.from(context);
@@ -88,7 +91,7 @@
*/
Stopwatch getStopwatch() {
if (mStopwatch == null) {
- mStopwatch = StopwatchDAO.getStopwatch(mContext);
+ mStopwatch = StopwatchDAO.getStopwatch(mPrefs);
}
return mStopwatch;
@@ -100,7 +103,7 @@
Stopwatch setStopwatch(Stopwatch stopwatch) {
final Stopwatch before = getStopwatch();
if (before != stopwatch) {
- StopwatchDAO.setStopwatch(mContext, stopwatch);
+ StopwatchDAO.setStopwatch(mPrefs, stopwatch);
mStopwatch = stopwatch;
// Refresh the stopwatch notification to reflect the latest stopwatch state.
@@ -141,7 +144,7 @@
final List<Lap> laps = getMutableLaps();
final int lapNumber = laps.size() + 1;
- StopwatchDAO.addLap(mContext, lapNumber, totalTime);
+ StopwatchDAO.addLap(mPrefs, lapNumber, totalTime);
final long prevAccumulatedTime = laps.isEmpty() ? 0 : laps.get(0).getAccumulatedTime();
final long lapTime = totalTime - prevAccumulatedTime;
@@ -167,7 +170,7 @@
*/
@VisibleForTesting
void clearLaps() {
- StopwatchDAO.clearLaps(mContext);
+ StopwatchDAO.clearLaps(mPrefs);
getMutableLaps().clear();
}
@@ -228,30 +231,18 @@
// Otherwise build and post a notification reflecting the latest stopwatch state.
final Notification notification =
- getNotificationBuilder().build(mContext, mNotificationModel, stopwatch);
+ mNotificationBuilder.build(mContext, mNotificationModel, stopwatch);
mNotificationManager.notify(mNotificationModel.getStopwatchNotificationId(), notification);
}
private List<Lap> getMutableLaps() {
if (mLaps == null) {
- mLaps = StopwatchDAO.getLaps(mContext);
+ mLaps = StopwatchDAO.getLaps(mPrefs);
}
return mLaps;
}
- private NotificationBuilder getNotificationBuilder() {
- if (mNotificationBuilder == null) {
- if (Utils.isNOrLater()) {
- mNotificationBuilder = new StopwatchNotificationBuilderN();
- } else {
- mNotificationBuilder = new StopwatchNotificationBuilderPreN();
- }
- }
-
- return mNotificationBuilder;
- }
-
/**
* Update the stopwatch notification in response to a locale change.
*/
@@ -261,17 +252,4 @@
updateNotification();
}
}
-
- /**
- * An API for building platform-specific stopwatch notifications.
- */
- public interface NotificationBuilder {
- /**
- * @param context a context to use for fetching resources
- * @param nm from which notification data are fetched
- * @param stopwatch the stopwatch guaranteed to be running or paused
- * @return a notification reporting the state of the {@code stopwatch}
- */
- Notification build(Context context, NotificationModel nm, Stopwatch stopwatch);
- }
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchNotificationBuilderN.java b/src/com/android/deskclock/data/StopwatchNotificationBuilder.java
similarity index 78%
rename from src/com/android/deskclock/data/StopwatchNotificationBuilderN.java
rename to src/com/android/deskclock/data/StopwatchNotificationBuilder.java
index e8e40b1..91de2ba 100644
--- a/src/com/android/deskclock/data/StopwatchNotificationBuilderN.java
+++ b/src/com/android/deskclock/data/StopwatchNotificationBuilder.java
@@ -16,17 +16,18 @@
package com.android.deskclock.data;
-import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
-import android.graphics.drawable.Icon;
-import android.os.Build;
import android.os.SystemClock;
+import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
+import android.support.v4.app.NotificationCompat.Action;
+import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.content.ContextCompat;
+import android.support.v7.app.NotificationCompat;
import android.widget.RemoteViews;
import com.android.deskclock.R;
@@ -41,12 +42,10 @@
import static android.view.View.VISIBLE;
/**
- * Builds N-style notification to reflect the latest state of the stopwatch and recorded laps.
+ * Builds notification to reflect the latest state of the stopwatch and recorded laps.
*/
-@TargetApi(Build.VERSION_CODES.N)
-class StopwatchNotificationBuilderN implements StopwatchModel.NotificationBuilder {
+class StopwatchNotificationBuilder {
- @Override
public Notification build(Context context, NotificationModel nm, Stopwatch stopwatch) {
@StringRes final int eventLabel = R.string.label_notification;
@@ -67,7 +66,7 @@
final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content);
content.setChronometer(R.id.chronometer, base, null, running);
- final List<Notification.Action> actions = new ArrayList<>(2);
+ final List<Action> actions = new ArrayList<>(2);
if (running) {
// Left button: Pause
@@ -75,10 +74,10 @@
.setAction(StopwatchService.ACTION_PAUSE_STOPWATCH)
.putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_pause_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_pause_24dp;
final CharSequence title1 = res.getText(R.string.sw_pause_button);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, pause);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ actions.add(new Action.Builder(icon1, title1, intent1).build());
// Right button: Add Lap
if (DataModel.getDataModel().canAddMoreLaps()) {
@@ -86,10 +85,10 @@
.setAction(StopwatchService.ACTION_LAP_STOPWATCH)
.putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_sw_lap_24dp);
+ @DrawableRes final int icon2 = R.drawable.ic_sw_lap_24dp;
final CharSequence title2 = res.getText(R.string.sw_lap_button);
final PendingIntent intent2 = Utils.pendingServiceIntent(context, lap);
- actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
+ actions.add(new Action.Builder(icon2, title2, intent2).build());
}
// Show the current lap number if any laps have been recorded.
@@ -108,27 +107,27 @@
.setAction(StopwatchService.ACTION_START_STOPWATCH)
.putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_start_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_start_24dp;
final CharSequence title1 = res.getText(R.string.sw_start_button);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, start);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ actions.add(new Action.Builder(icon1, title1, intent1).build());
// Right button: Reset (dismisses notification and resets stopwatch)
final Intent reset = new Intent(context, StopwatchService.class)
.setAction(StopwatchService.ACTION_RESET_STOPWATCH)
.putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
+ @DrawableRes final int icon2 = R.drawable.ic_reset_24dp;
final CharSequence title2 = res.getText(R.string.sw_reset_button);
final PendingIntent intent2 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
+ actions.add(new Action.Builder(icon2, title2, intent2).build());
// Indicate the stopwatch is paused.
content.setTextViewText(R.id.state, res.getString(R.string.swn_paused));
content.setViewVisibility(R.id.state, VISIBLE);
}
- return new Notification.Builder(context)
+ final Builder notification = new NotificationCompat.Builder(context)
.setLocalOnly(true)
.setOngoing(running)
.setCustomContentView(content)
@@ -136,10 +135,17 @@
.setAutoCancel(stopwatch.isPaused())
.setPriority(Notification.PRIORITY_MAX)
.setSmallIcon(R.drawable.stat_notify_stopwatch)
- .setGroup(nm.getStopwatchNotificationGroupKey())
- .setStyle(new Notification.DecoratedCustomViewStyle())
- .setActions(actions.toArray(new Notification.Action[actions.size()]))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .build();
+ .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
+ .setColor(ContextCompat.getColor(context, R.color.default_background));
+
+ if (Utils.isNOrLater()) {
+ notification.setGroup(nm.getStopwatchNotificationGroupKey());
+ }
+
+ for (Action action : actions) {
+ notification.addAction(action);
+ }
+
+ return notification.build();
}
}
diff --git a/src/com/android/deskclock/data/StopwatchNotificationBuilderPreN.java b/src/com/android/deskclock/data/StopwatchNotificationBuilderPreN.java
deleted file mode 100644
index 50489bc..0000000
--- a/src/com/android/deskclock/data/StopwatchNotificationBuilderPreN.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2016 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.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.SystemClock;
-import android.support.annotation.IdRes;
-import android.support.annotation.StringRes;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.content.ContextCompat;
-import android.widget.RemoteViews;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.stopwatch.StopwatchService;
-
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-
-/**
- * Builds KK, L, or M-style notifications to reflect the latest state of the stopwatch and
- * recorded laps.
- */
-class StopwatchNotificationBuilderPreN implements StopwatchModel.NotificationBuilder {
-
- @Override
- public Notification build(Context context, NotificationModel nm, Stopwatch stopwatch) {
- @StringRes final int eventLabel = R.string.label_notification;
-
- // Intent to load the app when the notification is tapped.
- final Intent showApp = new Intent(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_SHOW_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
- final PendingIntent pendingShowApp = PendingIntent.getService(context, 0, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Compute some values required below.
- final boolean running = stopwatch.isRunning();
- final String pname = context.getPackageName();
- final Resources res = context.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.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.setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
-
- @IdRes final int leftButtonId = R.id.swn_left_button;
- @IdRes 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(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final PendingIntent pendingPause = Utils.pendingServiceIntent(context, pause);
- expanded.setOnClickPendingIntent(leftButtonId, pendingPause);
-
- // Right button: Add Lap
- if (DataModel.getDataModel().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(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_LAP_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final PendingIntent pendingLap = Utils.pendingServiceIntent(context, lap);
- expanded.setOnClickPendingIntent(rightButtonId, pendingLap);
- expanded.setViewVisibility(rightButtonId, VISIBLE);
- } else {
- expanded.setViewVisibility(rightButtonId, INVISIBLE);
- }
-
- // Show the current lap number if any laps have been recorded.
- final int lapCount = DataModel.getDataModel().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(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_START_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final PendingIntent pendingStart = Utils.pendingServiceIntent(context, start);
- expanded.setOnClickPendingIntent(leftButtonId, pendingStart);
-
- // Right button: Reset (dismisses notification and resets stopwatch)
- 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(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_RESET_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
- final PendingIntent pendingReset = Utils.pendingServiceIntent(context, reset);
- expanded.setOnClickPendingIntent(rightButtonId, pendingReset);
-
- // 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);
- }
-
- final Notification notification = new NotificationCompat.Builder(context)
- .setLocalOnly(true)
- .setOngoing(running)
- .setContent(collapsed)
- .setContentIntent(pendingShowApp)
- .setAutoCancel(stopwatch.isPaused())
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setSmallIcon(R.drawable.stat_notify_stopwatch)
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .build();
- notification.bigContentView = expanded;
- return notification;
- }
-
- private static void setTextViewDrawable(RemoteViews rv, int viewId, int drawableId) {
- rv.setTextViewCompoundDrawablesRelative(viewId, drawableId, 0, 0, 0);
- }
-}
diff --git a/src/com/android/deskclock/data/TimeModel.java b/src/com/android/deskclock/data/TimeModel.java
new file mode 100644
index 0000000..5f8b9ad
--- /dev/null
+++ b/src/com/android/deskclock/data/TimeModel.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 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.os.SystemClock;
+import android.text.format.DateFormat;
+
+import java.util.Calendar;
+
+/**
+ * All time data is accessed via this model. This model exists so that time can be mocked for
+ * testing purposes.
+ */
+final class TimeModel {
+
+ private final Context mContext;
+
+ TimeModel(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * @return the current time in milliseconds
+ */
+ long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * @return milliseconds since boot, including time spent in sleep
+ */
+ long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * @return {@code true} if 24 hour time format is selected; {@code false} otherwise
+ */
+ boolean is24HourFormat() {
+ return DateFormat.is24HourFormat(mContext);
+ }
+
+ /**
+ * @return a new Calendar with the {@link #currentTimeMillis}
+ */
+ Calendar getCalendar() {
+ final Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(currentTimeMillis());
+ return calendar;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimeZones.java b/src/com/android/deskclock/data/TimeZones.java
new file mode 100644
index 0000000..60153c7
--- /dev/null
+++ b/src/com/android/deskclock/data/TimeZones.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 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.text.TextUtils;
+
+/**
+ * A read-only domain object representing the timezones from which to choose a "home" timezone.
+ */
+public final class TimeZones {
+
+ private final CharSequence[] mTimeZoneIds;
+ private final CharSequence[] mTimeZoneNames;
+
+ TimeZones(CharSequence[] timeZoneIds, CharSequence[] timeZoneNames) {
+ mTimeZoneIds = timeZoneIds;
+ mTimeZoneNames = timeZoneNames;
+ }
+
+ public CharSequence[] getTimeZoneIds() {
+ return mTimeZoneIds;
+ }
+
+ public CharSequence[] getTimeZoneNames() {
+ return mTimeZoneNames;
+ }
+
+ /**
+ * @param timeZoneId identifies the timezone to locate
+ * @return the timezone name with the {@code timeZoneId}; {@code null} if it does not exist
+ */
+ CharSequence getTimeZoneName(CharSequence timeZoneId) {
+ for (int i = 0; i < mTimeZoneIds.length; i++) {
+ if (TextUtils.equals(timeZoneId, mTimeZoneIds[i])) {
+ return mTimeZoneNames[i];
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param timeZoneId identifies the timezone to locate
+ * @return {@code true} iff the timezone with the given id is present
+ */
+ boolean contains(String timeZoneId) {
+ return getTimeZoneName(timeZoneId) != null;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Timer.java b/src/com/android/deskclock/data/Timer.java
index 11e25cc..71716a3 100644
--- a/src/com/android/deskclock/data/Timer.java
+++ b/src/com/android/deskclock/data/Timer.java
@@ -72,8 +72,8 @@
/** The minimum duration of a timer. */
public static final long MIN_LENGTH = SECOND_IN_MILLIS;
- /** The maximum duration of a timer. */
- public static final long MAX_LENGTH =
+ /** The maximum duration of a new timer created via the user interface. */
+ static final long MAX_LENGTH =
99 * HOUR_IN_MILLIS + 99 * MINUTE_IN_MILLIS + 99 * SECOND_IN_MILLIS;
static final long UNUSED = Long.MIN_VALUE;
@@ -139,14 +139,18 @@
/**
* @return the total amount of time remaining up to this moment; expired and missed timers will
- * return a negative amount
+ * return a negative amount
*/
public long getRemainingTime() {
- if (mState == RUNNING || mState == EXPIRED || mState == MISSED) {
- return mRemainingTime - (now() - mLastStartTime);
+ if (mState == PAUSED || mState == RESET) {
+ return mRemainingTime;
}
- return mRemainingTime;
+ // In practice, "now" can be any value due to device reboots. When the real-time clock
+ // is reset, there is no more guarantee that "now" falls after the last start time. To
+ // ensure the timer is monotonically decreasing, normalize negative time segments to 0,
+ final long timeSinceStart = now() - mLastStartTime;
+ return mRemainingTime - Math.max(0, timeSinceStart);
}
/**
@@ -301,12 +305,11 @@
}
/**
- * @return a copy of this timer with the given {@code length}
+ * @return a copy of this timer with the given {@code length} or this timer if the length could
+ * not be legally adjusted
*/
Timer setLength(long length) {
- if (mLength == length
- || length <= 0L
- || length > MAX_LENGTH) {
+ if (mLength == length || length <= Timer.MIN_LENGTH) {
return this;
}
@@ -325,11 +328,12 @@
}
/**
- * @return a copy of this timer with the given {@code remainingTime}
+ * @return a copy of this timer with the given {@code remainingTime} or this timer if the
+ * remaining time could not be legally adjusted
*/
Timer setRemainingTime(long remainingTime) {
- // Do not allow the remaining time to exceed the maximum.
- if (mRemainingTime == remainingTime || remainingTime > MAX_LENGTH) {
+ // Do not change the remaining time of a reset timer.
+ if (mRemainingTime == remainingTime || mState == RESET) {
return this;
}
@@ -355,11 +359,16 @@
/**
* @return a copy of this timer with an additional minute added to the remaining time and total
- * length, or this Timer if adding a minute would exceed the maximum timer duration
+ * length, or this Timer if the minute could not be added
*/
Timer addMinute() {
- final long remainingTime = (mState == EXPIRED || mState == MISSED) ? 0L : mRemainingTime;
- return setRemainingTime(remainingTime + MINUTE_IN_MILLIS);
+ // Expired and missed timers restart with 60 seconds of remaining time.
+ if (mState == EXPIRED || mState == MISSED) {
+ return setRemainingTime(MINUTE_IN_MILLIS);
+ }
+
+ // Otherwise try to add a minute to the remaining time.
+ return setRemainingTime(mRemainingTime + MINUTE_IN_MILLIS);
}
@Override
@@ -370,7 +379,6 @@
final Timer timer = (Timer) o;
return mId == timer.mId;
-
}
@Override
@@ -381,7 +389,7 @@
/**
* Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top.
*/
- public static Comparator<Timer> ID_COMPARATOR = new Comparator<Timer>() {
+ static Comparator<Timer> ID_COMPARATOR = new Comparator<Timer>() {
@Override
public int compare(Timer timer1, Timer timer2) {
return Integer.compare(timer2.getId(), timer1.getId());
@@ -399,7 +407,7 @@
* <li>{@link State#RESET RESET} timers; ties broken by {@link #getLength()}</li>
* </ol>
*/
- public static Comparator<Timer> EXPIRY_COMPARATOR = new Comparator<Timer>() {
+ static Comparator<Timer> EXPIRY_COMPARATOR = new Comparator<Timer>() {
private final List<State> stateExpiryOrder = Arrays.asList(MISSED, EXPIRED, RUNNING, PAUSED,
RESET);
diff --git a/src/com/android/deskclock/data/TimerDAO.java b/src/com/android/deskclock/data/TimerDAO.java
index 462f4d9..32d36d9 100644
--- a/src/com/android/deskclock/data/TimerDAO.java
+++ b/src/com/android/deskclock/data/TimerDAO.java
@@ -16,10 +16,8 @@
package com.android.deskclock.data;
-import android.content.Context;
import android.content.SharedPreferences;
-import com.android.deskclock.Utils;
import com.android.deskclock.data.Timer.State;
import java.util.ArrayList;
@@ -71,9 +69,7 @@
/**
* @return the timers from permanent storage
*/
- static List<Timer> getTimers(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
-
+ static List<Timer> getTimers(SharedPreferences prefs) {
// Read the set of timer ids.
final Set<String> timerIds = prefs.getStringSet(TIMER_IDS, Collections.<String>emptySet());
final List<Timer> timers = new ArrayList<>(timerIds.size());
@@ -106,8 +102,7 @@
/**
* @param timer the timer to be added
*/
- static Timer addTimer(Context context, Timer timer) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static Timer addTimer(SharedPreferences prefs, Timer timer) {
final SharedPreferences.Editor editor = prefs.edit();
// Fetch the next timer id.
@@ -115,7 +110,7 @@
editor.putInt(NEXT_TIMER_ID, id + 1);
// Add the new timer id to the set of all timer ids.
- final Set<String> timerIds = new HashSet<>(getTimerIds(context));
+ final Set<String> timerIds = new HashSet<>(getTimerIds(prefs));
timerIds.add(String.valueOf(id));
editor.putStringSet(TIMER_IDS, timerIds);
@@ -140,8 +135,7 @@
/**
* @param timer the timer to be updated
*/
- static void updateTimer(Context context, Timer timer) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void updateTimer(SharedPreferences prefs, Timer timer) {
final SharedPreferences.Editor editor = prefs.edit();
// Record the fields of the timer.
@@ -161,14 +155,13 @@
/**
* @param timer the timer to be removed
*/
- static void removeTimer(Context context, Timer timer) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static void removeTimer(SharedPreferences prefs, Timer timer) {
final SharedPreferences.Editor editor = prefs.edit();
final int id = timer.getId();
// Remove the timer id from the set of all timer ids.
- final Set<String> timerIds = new HashSet<>(getTimerIds(context));
+ final Set<String> timerIds = new HashSet<>(getTimerIds(prefs));
timerIds.remove(String.valueOf(id));
if (timerIds.isEmpty()) {
editor.remove(TIMER_IDS);
@@ -190,8 +183,7 @@
editor.apply();
}
- private static Set<String> getTimerIds(Context context) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ private static Set<String> getTimerIds(SharedPreferences prefs) {
return prefs.getStringSet(TIMER_IDS, Collections.<String>emptySet());
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerModel.java b/src/com/android/deskclock/data/TimerModel.java
index f0adbcb..0b8b2ae 100644
--- a/src/com/android/deskclock/data/TimerModel.java
+++ b/src/com/android/deskclock/data/TimerModel.java
@@ -27,8 +27,6 @@
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
import android.net.Uri;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationManagerCompat;
@@ -49,7 +47,7 @@
import java.util.Set;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
-import static android.text.format.DateUtils.*;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static com.android.deskclock.data.Timer.State.EXPIRED;
import static com.android.deskclock.data.Timer.State.RESET;
@@ -66,6 +64,8 @@
private final Context mContext;
+ private final SharedPreferences mPrefs;
+
/** The alarm manager system service that calls back when timers expire. */
private final AlarmManager mAlarmManager;
@@ -75,6 +75,9 @@
/** The model from which notification data are fetched. */
private final NotificationModel mNotificationModel;
+ /** The model from which ringtone data are fetched. */
+ private final RingtoneModel mRingtoneModel;
+
/** Used to create and destroy system notifications related to timers. */
private final NotificationManagerCompat mNotificationManager;
@@ -92,6 +95,9 @@
/** The listeners to notify when a timer is added, updated or removed. */
private final List<TimerListener> mTimerListeners = new ArrayList<>();
+ /** Delegate that builds platform-specific timer notifications. */
+ private final TimerNotificationBuilder mNotificationBuilder = new TimerNotificationBuilder();
+
/**
* The ids of expired timers for which the ringer is ringing. Not all expired timers have their
* ids in this collection. If a timer was already expired when the app was started its id will
@@ -115,9 +121,6 @@
/** A mutable copy of the missed timers. */
private List<Timer> mMissedTimers;
- /** Delegate that builds platform-specific timer notifications. */
- private NotificationBuilder mNotificationBuilder;
-
/**
* The service that keeps this application in the foreground while a heads-up timer
* notification is displayed. Marking the service as foreground prevents the operating system
@@ -125,19 +128,21 @@
*/
private Service mService;
- TimerModel(Context context, SettingsModel settingsModel, NotificationModel notificationModel) {
+ TimerModel(Context context, SharedPreferences prefs, SettingsModel settingsModel,
+ RingtoneModel ringtoneModel, NotificationModel notificationModel) {
mContext = context;
+ mPrefs = prefs;
mSettingsModel = settingsModel;
+ mRingtoneModel = ringtoneModel;
mNotificationModel = notificationModel;
mNotificationManager = NotificationManagerCompat.from(context);
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
// Clear caches affected by preferences when preferences change.
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(mContext);
prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
- // Update stopwatch notification when locale changes.
+ // Update timer notification when locale changes.
final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
}
@@ -173,7 +178,7 @@
/**
* @return all missed timers in their expiration order
*/
- List<Timer> getMissedTimers() {
+ private List<Timer> getMissedTimers() {
return Collections.unmodifiableList(getMutableMissedTimers());
}
@@ -212,7 +217,7 @@
label, deleteAfterUse);
// Add the timer to permanent storage.
- timer = TimerDAO.addTimer(mContext, timer);
+ timer = TimerDAO.addTimer(mPrefs, timer);
// Add the timer to the cache.
getMutableTimers().add(0, timer);
@@ -319,7 +324,6 @@
updateHeadsUpNotification();
}
-
/**
* Update timers after time set.
*/
@@ -435,8 +439,7 @@
// Special case: default ringtone has a title of "Timer Expired".
mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title);
} else {
- final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri);
- mTimerRingtoneTitle = ringtone.getTitle(mContext);
+ mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri);
}
}
}
@@ -445,14 +448,22 @@
}
/**
- * @return whether vibration is enabled for timers.
+ * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+ * {@code 0} implies no crescendo should be applied
+ */
+ long getTimerCrescendoDuration() {
+ return mSettingsModel.getTimerCrescendoDuration();
+ }
+
+ /**
+ * @return {@code true} if the device vibrates when timers expire
*/
boolean getTimerVibrate() {
return mSettingsModel.getTimerVibrate();
}
/**
- * @param enabled whether the
+ * @param enabled {@code true} if the device should vibrate when timers expire
*/
void setTimerVibrate(boolean enabled) {
mSettingsModel.setTimerVibrate(enabled);
@@ -460,7 +471,7 @@
private List<Timer> getMutableTimers() {
if (mTimers == null) {
- mTimers = TimerDAO.getTimers(mContext);
+ mTimers = TimerDAO.getTimers(mPrefs);
Collections.sort(mTimers, Timer.ID_COMPARATOR);
}
@@ -516,7 +527,7 @@
}
// Update the timer in permanent storage.
- TimerDAO.updateTimer(mContext, timer);
+ TimerDAO.updateTimer(mPrefs, timer);
// Update the timer in the cache.
final Timer oldTimer = timers.set(index, timer);
@@ -550,9 +561,9 @@
*
* @param timer an existing timer to be removed
*/
- void doRemoveTimer(Timer timer) {
+ private void doRemoveTimer(Timer timer) {
// Remove the timer from permanent storage.
- TimerDAO.removeTimer(mContext, timer);
+ TimerDAO.removeTimer(mPrefs, timer);
// Remove the timer from the cache.
final List<Timer> timers = getMutableTimers();
@@ -738,7 +749,7 @@
// Otherwise build and post a notification reflecting the latest unexpired timers.
final Notification notification =
- getNotificationBuilder().build(mContext, mNotificationModel, unexpired);
+ mNotificationBuilder.build(mContext, mNotificationModel, unexpired);
final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
mNotificationManager.notify(notificationId, notification);
@@ -762,7 +773,7 @@
return;
}
- final Notification notification = getNotificationBuilder().buildMissed(mContext,
+ final Notification notification = mNotificationBuilder.buildMissed(mContext,
mNotificationModel, missed);
final int notificationId = mNotificationModel.getMissedTimerNotificationId();
mNotificationManager.notify(notificationId, notification);
@@ -788,29 +799,18 @@
}
// Otherwise build and post a foreground notification reflecting the latest expired timers.
- final Notification notification = getNotificationBuilder().buildHeadsUp(mContext, expired);
+ final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired);
final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
mService.startForeground(notificationId, notification);
}
- private NotificationBuilder getNotificationBuilder() {
- if (mNotificationBuilder == null) {
- if (Utils.isNOrLater()) {
- mNotificationBuilder = new TimerNotificationBuilderN();
- } else {
- mNotificationBuilder = new TimerNotificationBuilderPreN();
- }
- }
-
- return mNotificationBuilder;
- }
-
/**
- * Update the stopwatch notification in response to a locale change.
+ * Update the timer notification in response to a locale change.
*/
private final class LocaleChangedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
+ mTimerRingtoneTitle = null;
updateNotification();
updateMissedNotification();
updateHeadsUpNotification();
@@ -841,35 +841,4 @@
am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
}
}
-
- /**
- * An API for building platform-specific timer notifications.
- */
- public interface NotificationBuilder {
-
- int REQUEST_CODE_UPCOMING = 0;
- int REQUEST_CODE_MISSING = 1;
-
- /**
- * @param context a context to use for fetching resources
- * @param nm from which notification data are fetched
- * @param unexpiredTimers all running and paused timers
- * @return a notification reporting the state of the {@code unexpiredTimers}
- */
- Notification build(Context context, NotificationModel nm, List<Timer> unexpiredTimers);
-
- /**
- * @param context a context to use for fetching resources
- * @param expiredTimers all expired timers
- * @return a heads-up notification reporting the state of the {@code expiredTimers}
- */
- Notification buildHeadsUp(Context context, List<Timer> expiredTimers);
-
- /**
- * @param context a context to use for fetching resources
- * @param missedTimers all missed timers
- * @return a heads-up notification reporting the state of the {@code missedTimers}
- */
- Notification buildMissed(Context context, NotificationModel nm, List<Timer> missedTimers);
- }
}
diff --git a/src/com/android/deskclock/data/TimerNotificationBuilderN.java b/src/com/android/deskclock/data/TimerNotificationBuilder.java
similarity index 62%
rename from src/com/android/deskclock/data/TimerNotificationBuilderN.java
rename to src/com/android/deskclock/data/TimerNotificationBuilder.java
index a2009ee..4454857 100644
--- a/src/com/android/deskclock/data/TimerNotificationBuilderN.java
+++ b/src/com/android/deskclock/data/TimerNotificationBuilder.java
@@ -17,18 +17,21 @@
package com.android.deskclock.data;
import android.annotation.TargetApi;
+import android.app.AlarmManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
-import android.graphics.drawable.Icon;
import android.os.Build;
import android.os.SystemClock;
+import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
+import android.support.v7.app.NotificationCompat;
import android.text.TextUtils;
import android.widget.RemoteViews;
+import com.android.deskclock.AlarmUtils;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
import com.android.deskclock.events.Events;
@@ -38,15 +41,19 @@
import java.util.ArrayList;
import java.util.List;
+import static android.support.v4.app.NotificationCompat.Action;
+import static android.support.v4.app.NotificationCompat.Builder;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
/**
- * Builds N-style notification to reflect the latest state of the unexpired timers.
+ * Builds notifications to reflect the latest state of the timers.
*/
-@TargetApi(Build.VERSION_CODES.N)
-class TimerNotificationBuilderN implements TimerModel.NotificationBuilder {
+class TimerNotificationBuilder {
- @Override
+ private static final int REQUEST_CODE_UPCOMING = 0;
+ private static final int REQUEST_CODE_MISSING = 1;
+
public Notification build(Context context, NotificationModel nm, List<Timer> unexpired) {
final Timer timer = unexpired.get(0);
final int count = unexpired.size();
@@ -57,11 +64,8 @@
final long base = getChronometerBase(timer);
final String pname = context.getPackageName();
- final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content);
- content.setChronometerCountDown(R.id.chronometer, true);
- content.setChronometer(R.id.chronometer, base, null, running);
- final List<Notification.Action> actions = new ArrayList<>(2);
+ final List<Action> actions = new ArrayList<>(2);
final CharSequence stateText;
if (count == 1) {
@@ -78,20 +82,20 @@
.setAction(TimerService.ACTION_PAUSE_TIMER)
.putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_pause_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_pause_24dp;
final CharSequence title1 = res.getText(R.string.timer_pause);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, pause);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ actions.add(new Action.Builder(icon1, title1, intent1).build());
// Right Button: +1 Minute
final Intent addMinute = new Intent(context, TimerService.class)
.setAction(TimerService.ACTION_ADD_MINUTE_TIMER)
.putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_add_24dp);
+ @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
final CharSequence title2 = res.getText(R.string.timer_plus_1_min);
final PendingIntent intent2 = Utils.pendingServiceIntent(context, addMinute);
- actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
+ actions.add(new Action.Builder(icon2, title2, intent2).build());
} else {
// Single timer is paused.
@@ -102,20 +106,20 @@
.setAction(TimerService.ACTION_START_TIMER)
.putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_start_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_start_24dp;
final CharSequence title1 = res.getText(R.string.sw_resume_button);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, start);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ actions.add(new Action.Builder(icon1, title1, intent1).build());
// Right Button: Reset
final Intent reset = new Intent(context, TimerService.class)
.setAction(TimerService.ACTION_RESET_TIMER)
.putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
+ @DrawableRes final int icon2 = R.drawable.ic_reset_24dp;
final CharSequence title2 = res.getText(R.string.sw_reset_button);
final PendingIntent intent2 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
+ actions.add(new Action.Builder(icon2, title2, intent2).build());
}
} else {
if (running) {
@@ -128,14 +132,12 @@
final Intent reset = TimerService.createResetUnexpiredTimersIntent(context);
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
final CharSequence title1 = res.getText(R.string.timer_reset_all);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ actions.add(new Action.Builder(icon1, title1, intent1).build());
}
- content.setTextViewText(R.id.state, stateText);
-
// Intent to load the app and show the timer when the notification is tapped.
final Intent showApp = new Intent(context, TimerService.class)
.setAction(TimerService.ACTION_SHOW_TIMER)
@@ -144,40 +146,82 @@
final PendingIntent pendingShowApp =
PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
- return new Notification.Builder(context)
+ final Builder notification = new NotificationCompat.Builder(context)
.setOngoing(true)
.setLocalOnly(true)
.setShowWhen(false)
.setAutoCancel(false)
- .setCustomContentView(content)
.setContentIntent(pendingShowApp)
.setPriority(Notification.PRIORITY_HIGH)
- .setCategory(Notification.CATEGORY_ALARM)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
.setSmallIcon(R.drawable.stat_notify_timer)
- .setGroup(nm.getTimerNotificationGroupKey())
.setSortKey(nm.getTimerNotificationSortKey())
- .setVisibility(Notification.VISIBILITY_PUBLIC)
- .setStyle(new Notification.DecoratedCustomViewStyle())
- .setActions(actions.toArray(new Notification.Action[actions.size()]))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .build();
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
+ .setColor(ContextCompat.getColor(context, R.color.default_background));
+
+ for (Action action : actions) {
+ notification.addAction(action);
+ }
+
+ if (Utils.isNOrLater()) {
+ notification.setCustomContentView(buildChronometer(pname, base, running, stateText))
+ .setGroup(nm.getTimerNotificationGroupKey());
+ } else {
+ final CharSequence contentTextPreN;
+ if (count == 1) {
+ contentTextPreN = TimerStringFormatter.formatTimeRemaining(context,
+ timer.getRemainingTime(), false);
+ } else if (running) {
+ final String timeRemaining = TimerStringFormatter.formatTimeRemaining(context,
+ timer.getRemainingTime(), false);
+ contentTextPreN = context.getString(R.string.next_timer_notif, timeRemaining);
+ } else {
+ contentTextPreN = context.getString(R.string.all_timers_stopped_notif);
+ }
+
+ notification.setContentTitle(stateText).setContentText(contentTextPreN);
+
+ final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ final Intent updateNotification = TimerService.createUpdateNotificationIntent(context);
+ final long remainingTime = timer.getRemainingTime();
+ if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) {
+ // Schedule a callback to update the time-sensitive information of the running timer
+ final PendingIntent pi =
+ PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification,
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+
+ final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS;
+ final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange;
+ TimerModel.schedulePendingIntent(am, triggerTime, pi);
+ } else {
+ // Cancel the update notification callback.
+ final PendingIntent pi = PendingIntent.getService(context, 0, updateNotification,
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
+ if (pi != null) {
+ am.cancel(pi);
+ pi.cancel();
+ }
+ }
+ }
+
+ return notification.build();
}
- @Override
- public Notification buildHeadsUp(Context context, List<Timer> expired) {
+ Notification buildHeadsUp(Context context, List<Timer> expired) {
final Timer timer = expired.get(0);
// First action intent is to reset all timers.
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_stop_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_stop_24dp;
final Intent reset = TimerService.createResetExpiredTimersIntent(context);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
// Generate some descriptive text, a title, and an action name based on the timer count.
final CharSequence stateText;
final int count = expired.size();
- final List<Notification.Action> actions = new ArrayList<>(2);
+ final List<Action> actions = new ArrayList<>(2);
if (count == 1) {
final String label = timer.getLabel();
if (TextUtils.isEmpty(label)) {
@@ -188,30 +232,25 @@
// Left button: Reset single timer
final CharSequence title1 = context.getString(R.string.timer_stop);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ actions.add(new Action.Builder(icon1, title1, intent1).build());
// Right button: Add minute
final Intent addTime = TimerService.createAddMinuteTimerIntent(context, timer.getId());
final PendingIntent intent2 = Utils.pendingServiceIntent(context, addTime);
- final Icon icon2 = Icon.createWithResource(context, R.drawable.ic_add_24dp);
+ @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
final CharSequence title2 = context.getString(R.string.timer_plus_1_min);
- actions.add(new Notification.Action.Builder(icon2, title2, intent2).build());
-
+ actions.add(new Action.Builder(icon2, title2, intent2).build());
} else {
stateText = context.getString(R.string.timer_multi_times_up, count);
// Left button: Reset all timers
final CharSequence title1 = context.getString(R.string.timer_stop_all);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ actions.add(new Action.Builder(icon1, title1, intent1).build());
}
final long base = getChronometerBase(timer);
final String pname = context.getPackageName();
- final RemoteViews contentView = new RemoteViews(pname, R.layout.chronometer_notif_content);
- contentView.setChronometerCountDown(R.id.chronometer, true);
- contentView.setChronometer(R.id.chronometer, base, null, true);
- contentView.setTextViewText(R.id.state, stateText);
// Content intent shows the timer full screen when clicked.
final Intent content = new Intent(context, ExpiredTimersActivity.class);
@@ -222,39 +261,47 @@
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen);
- return new Notification.Builder(context)
+ final Builder notification = new NotificationCompat.Builder(context)
.setOngoing(true)
.setLocalOnly(true)
.setShowWhen(false)
.setAutoCancel(false)
.setContentIntent(contentIntent)
- .setCustomContentView(contentView)
.setPriority(Notification.PRIORITY_MAX)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setSmallIcon(R.drawable.stat_notify_timer)
.setFullScreenIntent(pendingFullScreen, true)
- .setStyle(new Notification.DecoratedCustomViewStyle())
- .setActions(actions.toArray(new Notification.Action[actions.size()]))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .build();
+ .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
+ .setColor(ContextCompat.getColor(context, R.color.default_background));
+
+ for (Action action : actions) {
+ notification.addAction(action);
+ }
+
+ if (Utils.isNOrLater()) {
+ notification.setCustomContentView(buildChronometer(pname, base, true, stateText));
+ } else {
+ final CharSequence contentTextPreN = count == 1
+ ? context.getString(R.string.timer_times_up)
+ : context.getString(R.string.timer_multi_times_up, count);
+
+ notification.setContentTitle(stateText).setContentText(contentTextPreN);
+ }
+
+ return notification.build();
}
- @Override
- public Notification buildMissed(Context context, NotificationModel nm,
+ Notification buildMissed(Context context, NotificationModel nm,
List<Timer> missedTimers) {
final Timer timer = missedTimers.get(0);
final int count = missedTimers.size();
// Compute some values required below.
final long base = getChronometerBase(timer);
- final String pName = context.getPackageName();
+ final String pname = context.getPackageName();
final Resources res = context.getResources();
- final RemoteViews content = new RemoteViews(pName, R.layout.chronometer_notif_content);
- content.setChronometerCountDown(R.id.chronometer, true);
- content.setChronometer(R.id.chronometer, base, null, true);
-
- final List<Notification.Action> actions = new ArrayList<>(1);
+ final Action action;
final CharSequence stateText;
if (count == 1) {
@@ -271,24 +318,22 @@
.setAction(TimerService.ACTION_RESET_TIMER)
.putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
final CharSequence title1 = res.getText(R.string.timer_reset);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ action = new Action.Builder(icon1, title1, intent1).build();
} else {
// Multiple missed timers.
stateText = res.getString(R.string.timer_multi_missed, count);
final Intent reset = TimerService.createResetMissedTimersIntent(context);
- final Icon icon1 = Icon.createWithResource(context, R.drawable.ic_reset_24dp);
+ @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
final CharSequence title1 = res.getText(R.string.timer_reset_all);
final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Notification.Action.Builder(icon1, title1, intent1).build());
+ action = new Action.Builder(icon1, title1, intent1).build();
}
- content.setTextViewText(R.id.state, stateText);
-
// Intent to load the app and show the timer when the notification is tapped.
final Intent showApp = new Intent(context, TimerService.class)
.setAction(TimerService.ACTION_SHOW_TIMER)
@@ -297,24 +342,32 @@
final PendingIntent pendingShowApp =
PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
- return new Notification.Builder(context)
+ final Builder notification = new NotificationCompat.Builder(context)
.setLocalOnly(true)
.setShowWhen(false)
.setAutoCancel(false)
- .setCustomContentView(content)
.setContentIntent(pendingShowApp)
.setPriority(Notification.PRIORITY_HIGH)
- .setCategory(Notification.CATEGORY_ALARM)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
.setSmallIcon(R.drawable.stat_notify_timer)
- .setGroup(nm.getTimerNotificationGroupKey())
- .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSortKey(nm.getTimerNotificationMissedSortKey())
- .setStyle(new Notification.DecoratedCustomViewStyle())
- .setActions(actions.toArray(new Notification.Action[actions.size()]))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .build();
+ .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
+ .addAction(action)
+ .setColor(ContextCompat.getColor(context, R.color.default_background));
+
+ if (Utils.isNOrLater()) {
+ notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
+ .setGroup(nm.getTimerNotificationGroupKey());
+ } else {
+ final CharSequence contentText = AlarmUtils.getFormattedTime(context,
+ timer.getWallClockExpirationTime());
+ notification.setContentText(contentText).setContentTitle(stateText);
+ }
+
+ return notification.build();
}
/**
@@ -330,4 +383,14 @@
// Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now.
return SystemClock.elapsedRealtime() + adjustedRemaining;
}
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private RemoteViews buildChronometer(String pname, long base, boolean running,
+ CharSequence stateText) {
+ final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content);
+ content.setChronometerCountDown(R.id.chronometer, true);
+ content.setChronometer(R.id.chronometer, base, null, running);
+ content.setTextViewText(R.id.state, stateText);
+ return content;
+ }
}
diff --git a/src/com/android/deskclock/data/TimerNotificationBuilderPreN.java b/src/com/android/deskclock/data/TimerNotificationBuilderPreN.java
deleted file mode 100644
index b10bcfb..0000000
--- a/src/com/android/deskclock/data/TimerNotificationBuilderPreN.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * Copyright (C) 2016 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.AlarmManager;
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.os.SystemClock;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.StringRes;
-import android.support.annotation.VisibleForTesting;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.content.ContextCompat;
-import android.text.TextUtils;
-
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.timer.ExpiredTimersActivity;
-import com.android.deskclock.timer.TimerService;
-
-import java.util.List;
-
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-
-/**
- * Builds KK, L, or M-style notifications to reflect the latest state of the unexpired timers.
- */
-class TimerNotificationBuilderPreN implements TimerModel.NotificationBuilder {
-
- @Override
- public Notification build(Context context, NotificationModel nm, List<Timer> unexpired) {
- final Timer timer = unexpired.get(0);
- final long remainingTime = timer.getRemainingTime();
-
- // Generate some descriptive text, a title, and some actions based on timer states.
- final String contentText;
- final String contentTitle;
- @DrawableRes int firstActionIconId, secondActionIconId = 0;
- @StringRes int firstActionTitleId, secondActionTitleId = 0;
- Intent firstActionIntent, secondActionIntent = null;
-
- if (unexpired.size() == 1) {
- contentText = formatElapsedTimeUntilExpiry(context, remainingTime);
-
- if (timer.isRunning()) {
- // Single timer is running.
- if (TextUtils.isEmpty(timer.getLabel())) {
- contentTitle = context.getString(R.string.timer_notification_label);
- } else {
- contentTitle = timer.getLabel();
- }
-
- firstActionIconId = R.drawable.ic_pause_24dp;
- firstActionTitleId = R.string.timer_pause;
- firstActionIntent = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_PAUSE_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
- secondActionIconId = R.drawable.ic_add_24dp;
- secondActionTitleId = R.string.timer_plus_1_min;
- secondActionIntent = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_ADD_MINUTE_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- } else {
- // Single timer is paused.
- contentTitle = context.getString(R.string.timer_paused);
-
- firstActionIconId = R.drawable.ic_start_24dp;
- firstActionTitleId = R.string.sw_resume_button;
- firstActionIntent = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_START_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
- secondActionIconId = R.drawable.ic_reset_24dp;
- secondActionTitleId = R.string.sw_reset_button;
- secondActionIntent = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_RESET_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- }
- } else {
- if (timer.isRunning()) {
- // At least one timer is running.
- final String timeRemaining = formatElapsedTimeUntilExpiry(context, remainingTime);
- contentText = context.getString(R.string.next_timer_notif, timeRemaining);
- contentTitle = context.getString(R.string.timers_in_use, unexpired.size());
- } else {
- // All timers are paused.
- contentText = context.getString(R.string.all_timers_stopped_notif);
- contentTitle = context.getString(R.string.timers_stopped, unexpired.size());
- }
-
- firstActionIconId = R.drawable.ic_reset_24dp;
- firstActionTitleId = R.string.timer_reset_all;
- firstActionIntent = TimerService.createResetUnexpiredTimersIntent(context);
- }
-
- // Intent to load the app and show the timer when the notification is tapped.
- final Intent showApp = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_SHOW_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId())
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification);
-
- final PendingIntent pendingShowApp = PendingIntent.getService(context, 0, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
- .setOngoing(true)
- .setLocalOnly(true)
- .setShowWhen(false)
- .setAutoCancel(false)
- .setContentText(contentText)
- .setContentTitle(contentTitle)
- .setContentIntent(pendingShowApp)
- .setSmallIcon(R.drawable.stat_notify_timer)
- .setSortKey(nm.getTimerNotificationSortKey())
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setColor(ContextCompat.getColor(context, R.color.default_background));
-
- final PendingIntent action1 = Utils.pendingServiceIntent(context, firstActionIntent);
- final String action1Title = context.getString(firstActionTitleId);
- builder.addAction(firstActionIconId, action1Title, action1);
-
- if (secondActionIntent != null) {
- final PendingIntent action2 = Utils.pendingServiceIntent(context, secondActionIntent);
- final String action2Title = context.getString(secondActionTitleId);
- builder.addAction(secondActionIconId, action2Title, action2);
- }
-
- final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- final Intent updateNotification = TimerService.createUpdateNotificationIntent(context);
- if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) {
- // Schedule a callback to update the time-sensitive information of the running timer.
- final PendingIntent pi =
- PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS;
- final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange;
- TimerModel.schedulePendingIntent(am, triggerTime, pi);
- } else {
- // Cancel the update notification callback.
- final PendingIntent pi = PendingIntent.getService(context, 0, updateNotification,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
- if (pi != null) {
- am.cancel(pi);
- pi.cancel();
- }
- }
-
- return builder.build();
- }
-
- @Override
- public Notification buildHeadsUp(Context context, List<Timer> expired) {
- // Generate some descriptive text, a title, and an action name based on the timer count.
- final int timerId;
- final String contentText;
- final String contentTitle;
- final String resetActionTitle;
- final int count = expired.size();
-
- if (count == 1) {
- final Timer timer = expired.get(0);
- timerId = timer.getId();
- resetActionTitle = context.getString(R.string.timer_stop);
- contentText = context.getString(R.string.timer_times_up);
-
- final String label = timer.getLabel();
- if (TextUtils.isEmpty(label)) {
- contentTitle = context.getString(R.string.timer_notification_label);
- } else {
- contentTitle = label;
- }
- } else {
- timerId = -1;
- contentText = context.getString(R.string.timer_multi_times_up, count);
- contentTitle = context.getString(R.string.timer_notification_label);
- resetActionTitle = context.getString(R.string.timer_stop_all);
- }
-
- // Content intent shows the expired timers full screen when clicked.
- final Intent content = new Intent(context, ExpiredTimersActivity.class);
- final PendingIntent pendingContent = Utils.pendingActivityIntent(context, content);
-
- // Full screen intent has flags so it is different than the content intent.
- final Intent fullScreen = new Intent(context, ExpiredTimersActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
- final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen);
-
- // Left button: Reset timer / Reset all timers
- final Intent reset = TimerService.createResetExpiredTimersIntent(context);
- final PendingIntent pendingReset = Utils.pendingServiceIntent(context, reset);
-
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
- .setOngoing(true)
- .setLocalOnly(true)
- .setShowWhen(false)
- .setAutoCancel(false)
- .setContentText(contentText)
- .setContentTitle(contentTitle)
- .setContentIntent(pendingContent)
- .setSmallIcon(R.drawable.stat_notify_timer)
- .setFullScreenIntent(pendingFullScreen, true)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
- .addAction(R.drawable.ic_stop_24dp, resetActionTitle, pendingReset)
- .setColor(ContextCompat.getColor(context, R.color.default_background));
-
- // Add a second action if only a single timer is expired.
- if (count == 1) {
- // Right button: Add minute
- final Intent addMinute = TimerService.createAddMinuteTimerIntent(context, timerId);
- final PendingIntent pendingAddMinute = Utils.pendingServiceIntent(context, addMinute);
- final String addMinuteTitle = context.getString(R.string.timer_plus_1_min);
- builder.addAction(R.drawable.ic_add_24dp, addMinuteTitle, pendingAddMinute);
- }
-
- return builder.build();
- }
-
- @Override
- public Notification buildMissed(Context context, NotificationModel nm,
- List<Timer> missedTimers) {
- final Timer timer = missedTimers.get(0);
- final long expirationTime = timer.getWallClockExpirationTime();
-
- // Generate a title, and some actions based on timer states.
- final String contentTitle;
- final String contentText;
- final @DrawableRes int firstActionIconId;
- final @StringRes int firstActionTitleId;
- final Intent firstActionIntent;
-
- if (missedTimers.size() == 1) {
- // Single timer is missed.
- contentText = AlarmUtils.getFormattedTime(context, expirationTime);
- if (TextUtils.isEmpty(timer.getLabel())) {
- contentTitle = context.getString(R.string.missed_timer_notification_label);
- } else {
- contentTitle = context.getString(R.string.missed_named_timer_notification_label,
- timer.getLabel());
- }
-
- firstActionIconId = R.drawable.ic_reset_24dp;
- firstActionTitleId = R.string.timer_reset;
- firstActionIntent = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_RESET_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
- } else {
- // Multiple missed timers.
- contentText = AlarmUtils.getFormattedTime(context, expirationTime);
- contentTitle = context.getString(R.string.timer_multi_missed, missedTimers.size());
-
- firstActionIconId = R.drawable.ic_reset_24dp;
- firstActionTitleId = R.string.timer_reset_all;
- firstActionIntent = TimerService.createResetMissedTimersIntent(context);
- }
-
- // Intent to load the app and show the timer when the notification is tapped.
- final Intent showApp = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_SHOW_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId())
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification);
-
- final PendingIntent pendingShowApp = PendingIntent.getService(context, 0, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
- .setLocalOnly(true)
- .setShowWhen(false)
- .setAutoCancel(false)
- .setContentText(contentText)
- .setContentTitle(contentTitle)
- .setContentIntent(pendingShowApp)
- .setSmallIcon(R.drawable.stat_notify_timer)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setSortKey(nm.getTimerNotificationMissedSortKey())
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setColor(ContextCompat.getColor(context, R.color.default_background));
-
- final PendingIntent action1 = Utils.pendingServiceIntent(context, firstActionIntent);
- final String action1Title = context.getString(firstActionTitleId);
- builder.addAction(firstActionIconId, action1Title, action1);
-
- return builder.build();
- }
-
- /**
- * Format "7 hours 52 minutes remaining"
- */
- @VisibleForTesting
- static String formatElapsedTimeUntilExpiry(Context context, long remainingTime) {
- final int hours = (int) remainingTime / (int) HOUR_IN_MILLIS;
- final int minutes = (int) remainingTime / ((int) MINUTE_IN_MILLIS) % 60;
-
- String minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, minutes);
- String hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, hours);
-
- // The verb "remaining" may have to change tense for singular subjects in some languages.
- final String verb = context.getString((minutes > 1 || hours > 1)
- ? R.string.timer_remaining_multiple
- : R.string.timer_remaining_single);
-
- final boolean showHours = hours > 0;
- final boolean showMinutes = minutes > 0;
-
- int formatStringId;
- if (showHours) {
- if (showMinutes) {
- formatStringId = R.string.timer_notifications_hours_minutes;
- } else {
- formatStringId = R.string.timer_notifications_hours;
- }
- } else if (showMinutes) {
- formatStringId = R.string.timer_notifications_minutes;
- } else {
- formatStringId = R.string.timer_notifications_less_min;
- }
- return String.format(context.getString(formatStringId), hourSeq, minSeq, verb);
- }
-}
diff --git a/src/com/android/deskclock/data/TimerStringFormatter.java b/src/com/android/deskclock/data/TimerStringFormatter.java
new file mode 100644
index 0000000..cbdc780
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerStringFormatter.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 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.support.annotation.StringRes;
+
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+public class TimerStringFormatter {
+
+ /**
+ * Format "7 hours 52 minutes 14 seconds remaining"
+ */
+ public static String formatTimeRemaining(Context context, long remainingTime,
+ boolean shouldShowSeconds) {
+ int roundedHours = (int) (remainingTime / HOUR_IN_MILLIS);
+ int roundedMinutes = (int) (remainingTime / MINUTE_IN_MILLIS % 60);
+ int roundedSeconds = (int) (remainingTime / SECOND_IN_MILLIS % 60);
+
+ final int seconds;
+ final int minutes;
+ final int hours;
+ if ((remainingTime % SECOND_IN_MILLIS != 0) && shouldShowSeconds) {
+ // Add 1 because there's a partial second.
+ roundedSeconds += 1;
+ if (roundedSeconds == 60) {
+ // Wind back and fix the hours and minutes as needed.
+ seconds = 0;
+ roundedMinutes += 1;
+ if (roundedMinutes == 60) {
+ minutes = 0;
+ roundedHours += 1;
+ hours = roundedHours;
+ } else {
+ minutes = roundedMinutes;
+ hours = roundedHours;
+ }
+ } else {
+ seconds = roundedSeconds;
+ minutes = roundedMinutes;
+ hours = roundedHours;
+ }
+ } else {
+ // Already perfect precision, or we don't want to consider seconds at all.
+ seconds = roundedSeconds;
+ minutes = roundedMinutes;
+ hours = roundedHours;
+ }
+
+ final String minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes,
+ minutes);
+ final String hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours,
+ hours);
+ final String secSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.seconds,
+ seconds);
+
+ // The verb "remaining" may have to change tense for singular subjects in some languages.
+ final String remainingSuffix = context.getString((minutes > 1 || hours > 1 || seconds > 1)
+ ? R.string.timer_remaining_multiple
+ : R.string.timer_remaining_single);
+
+ final boolean showHours = hours > 0;
+ final boolean showMinutes = minutes > 0;
+ final boolean showSeconds = (seconds > 0) && shouldShowSeconds;
+
+ int formatStringId = -1;
+ if (showHours) {
+ if (showMinutes) {
+ if (showSeconds) {
+ formatStringId = R.string.timer_notifications_hours_minutes_seconds;
+ } else {
+ formatStringId = R.string.timer_notifications_hours_minutes;
+ }
+ } else if (showSeconds) {
+ formatStringId = R.string.timer_notifications_hours_seconds;
+ } else {
+ formatStringId = R.string.timer_notifications_hours;
+ }
+ } else if (showMinutes) {
+ if (showSeconds) {
+ formatStringId = R.string.timer_notifications_minutes_seconds;
+ } else {
+ formatStringId = R.string.timer_notifications_minutes;
+ }
+ } else if (showSeconds) {
+ formatStringId = R.string.timer_notifications_seconds;
+ } else if (!shouldShowSeconds) {
+ formatStringId = R.string.timer_notifications_less_min;
+ }
+
+ if (formatStringId == -1) {
+ return null;
+ }
+ return String.format(context.getString(formatStringId), hourSeq, minSeq, remainingSuffix,
+ secSeq);
+ }
+
+ public static String formatString(Context context, @StringRes int stringResId, long currentTime,
+ boolean shouldShowSeconds) {
+ return String.format(context.getString(stringResId),
+ formatTimeRemaining(context, currentTime, shouldShowSeconds));
+ }
+}
diff --git a/src/com/android/deskclock/data/Weekdays.java b/src/com/android/deskclock/data/Weekdays.java
new file mode 100644
index 0000000..9f4f64a
--- /dev/null
+++ b/src/com/android/deskclock/data/Weekdays.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2016 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.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+
+import com.android.deskclock.R;
+
+import java.text.DateFormatSymbols;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Calendar.DAY_OF_WEEK;
+import static java.util.Calendar.FRIDAY;
+import static java.util.Calendar.MONDAY;
+import static java.util.Calendar.SATURDAY;
+import static java.util.Calendar.SUNDAY;
+import static java.util.Calendar.THURSDAY;
+import static java.util.Calendar.TUESDAY;
+import static java.util.Calendar.WEDNESDAY;
+
+/**
+ * This class is responsible for encoding a weekly repeat cycle in a {@link #getBits bitset}. It
+ * also converts between those bits and the {@link Calendar#DAY_OF_WEEK} values for easier mutation
+ * and querying.
+ */
+public final class Weekdays {
+
+ /**
+ * The preferred starting day of the week can differ by locale. This enumerated value is used to
+ * describe the preferred ordering.
+ */
+ public enum Order {
+ SAT_TO_FRI(SATURDAY, SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY),
+ SUN_TO_SAT(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY),
+ MON_TO_SUN(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY);
+
+ private final List<Integer> mCalendarDays;
+
+ Order(Integer... calendarDays) {
+ mCalendarDays = Arrays.asList(calendarDays);
+ }
+
+ public List<Integer> getCalendarDays() {
+ return mCalendarDays;
+ }
+ }
+
+ /** All valid bits set. */
+ private static final int ALL_DAYS = 0x7F;
+
+ /** An instance with all weekdays in the weekly repeat cycle. */
+ public static final Weekdays ALL = Weekdays.fromBits(ALL_DAYS);
+
+ /** An instance with no weekdays in the weekly repeat cycle. */
+ public static final Weekdays NONE = Weekdays.fromBits(0);
+
+ /** Maps calendar weekdays to the bit masks that represent them in this class. */
+ private static final Map<Integer, Integer> sCalendarDayToBit;
+ static {
+ final Map<Integer, Integer> map = new ArrayMap<>(7);
+ map.put(MONDAY, 0x01);
+ map.put(TUESDAY, 0x02);
+ map.put(WEDNESDAY, 0x04);
+ map.put(THURSDAY, 0x08);
+ map.put(FRIDAY, 0x10);
+ map.put(SATURDAY, 0x20);
+ map.put(SUNDAY, 0x40);
+ sCalendarDayToBit = Collections.unmodifiableMap(map);
+ }
+
+ /** An encoded form of a weekly repeat schedule. */
+ private final int mBits;
+
+ private Weekdays(int bits) {
+ // Mask off the unused bits.
+ mBits = ALL_DAYS & bits;
+ }
+
+ /**
+ * @param bits {@link #getBits bits} representing the encoded weekly repeat schedule
+ * @return a Weekdays instance representing the same repeat schedule as the {@code bits}
+ */
+ public static Weekdays fromBits(int bits) {
+ return new Weekdays(bits);
+ }
+
+ /**
+ * @param calendarDays an array containing any or all of the following values
+ * <ul>
+ * <li>{@link Calendar#SUNDAY}</li>
+ * <li>{@link Calendar#MONDAY}</li>
+ * <li>{@link Calendar#TUESDAY}</li>
+ * <li>{@link Calendar#WEDNESDAY}</li>
+ * <li>{@link Calendar#THURSDAY}</li>
+ * <li>{@link Calendar#FRIDAY}</li>
+ * <li>{@link Calendar#SATURDAY}</li>
+ * </ul>
+ * @return a Weekdays instance representing the given {@code calendarDays}
+ */
+ public static Weekdays fromCalendarDays(int... calendarDays) {
+ int bits = 0;
+ for (int calendarDay : calendarDays) {
+ final Integer bit = sCalendarDayToBit.get(calendarDay);
+ if (bit != null) {
+ bits = bits | bit;
+ }
+ }
+ return new Weekdays(bits);
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ * <ul>
+ * <li>{@link Calendar#SUNDAY}</li>
+ * <li>{@link Calendar#MONDAY}</li>
+ * <li>{@link Calendar#TUESDAY}</li>
+ * <li>{@link Calendar#WEDNESDAY}</li>
+ * <li>{@link Calendar#THURSDAY}</li>
+ * <li>{@link Calendar#FRIDAY}</li>
+ * <li>{@link Calendar#SATURDAY}</li>
+ * </ul>
+ * @param on {@code true} if the {@code calendarDay} is on; {@code false} otherwise
+ * @return a WeekDays instance with the {@code calendarDay} mutated
+ */
+ public Weekdays setBit(int calendarDay, boolean on) {
+ final Integer bit = sCalendarDayToBit.get(calendarDay);
+ if (bit == null) {
+ return this;
+ }
+ return new Weekdays(on ? (mBits | bit) : (mBits & ~bit));
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ * <ul>
+ * <li>{@link Calendar#SUNDAY}</li>
+ * <li>{@link Calendar#MONDAY}</li>
+ * <li>{@link Calendar#TUESDAY}</li>
+ * <li>{@link Calendar#WEDNESDAY}</li>
+ * <li>{@link Calendar#THURSDAY}</li>
+ * <li>{@link Calendar#FRIDAY}</li>
+ * <li>{@link Calendar#SATURDAY}</li>
+ * </ul>
+ * @return {@code true} if the given {@code calendarDay}
+ */
+ public boolean isBitOn(int calendarDay) {
+ final Integer bit = sCalendarDayToBit.get(calendarDay);
+ if (bit == null) {
+ throw new IllegalArgumentException(calendarDay + " is not a valid weekday");
+ }
+ return (mBits & bit) > 0;
+ }
+
+ /**
+ * @return the weekly repeat schedule encoded as an integer
+ */
+ public int getBits() { return mBits; }
+
+ /**
+ * @return {@code true} iff at least one weekday is enabled in the repeat schedule
+ */
+ public boolean isRepeating() { return mBits != 0; }
+
+ /**
+ * Note: only the day-of-week is read from the {@code time}. The time fields
+ * are not considered in this computation.
+ *
+ * @param time a timestamp relative to which the answer is given
+ * @return the number of days between the given {@code time} and the previous enabled weekday
+ * which is always between 1 and 7 inclusive; {@code -1} if no weekdays are enabled
+ */
+ public int getDistanceToPreviousDay(Calendar time) {
+ int calendarDay = time.get(DAY_OF_WEEK);
+ for (int count = 1; count <= 7; count++) {
+ calendarDay--;
+ if (calendarDay < Calendar.SUNDAY) {
+ calendarDay = Calendar.SATURDAY;
+ }
+ if (isBitOn(calendarDay)) {
+ return count;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Note: only the day-of-week is read from the {@code time}. The time fields
+ * are not considered in this computation.
+ *
+ * @param time a timestamp relative to which the answer is given
+ * @return the number of days between the given {@code time} and the next enabled weekday which
+ * is always between 0 and 6 inclusive; {@code -1} if no weekdays are enabled
+ */
+ public int getDistanceToNextDay(Calendar time) {
+ int calendarDay = time.get(DAY_OF_WEEK);
+ for (int count = 0; count < 7; count++) {
+ if (isBitOn(calendarDay)) {
+ return count;
+ }
+
+ calendarDay++;
+ if (calendarDay > Calendar.SATURDAY) {
+ calendarDay = Calendar.SUNDAY;
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Weekdays weekdays = (Weekdays) o;
+ return mBits == weekdays.mBits;
+ }
+
+ @Override
+ public int hashCode() {
+ return mBits;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder(19);
+ builder.append("[");
+ if (isBitOn(MONDAY)) {
+ builder.append(builder.length() > 1 ? " M" : "M");
+ }
+ if (isBitOn(TUESDAY)) {
+ builder.append(builder.length() > 1 ? " T" : "T");
+ }
+ if (isBitOn(WEDNESDAY)) {
+ builder.append(builder.length() > 1 ? " W" : "W");
+ }
+ if (isBitOn(THURSDAY)) {
+ builder.append(builder.length() > 1 ? " Th" : "Th");
+ }
+ if (isBitOn(FRIDAY)) {
+ builder.append(builder.length() > 1 ? " F" : "F");
+ }
+ if (isBitOn(SATURDAY)) {
+ builder.append(builder.length() > 1 ? " Sa" : "Sa");
+ }
+ if (isBitOn(SUNDAY)) {
+ builder.append(builder.length() > 1 ? " Su" : "Su");
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ /**
+ * @param context for accessing resources
+ * @param order the order in which to present the weekdays
+ * @return the enabled weekdays in the given {@code order}
+ */
+ public String toString(Context context, Order order) {
+ return toString(context, order, false /* forceLongNames */);
+ }
+
+ /**
+ * @param context for accessing resources
+ * @param order the order in which to present the weekdays
+ * @return the enabled weekdays in the given {@code order} in a manner that
+ * is most appropriate for talk-back
+ */
+ public String toAccessibilityString(Context context, Order order) {
+ return toString(context, order, true /* forceLongNames */);
+ }
+
+ @VisibleForTesting
+ int getCount() {
+ int count = 0;
+ for (int calendarDay = SUNDAY; calendarDay <= SATURDAY; calendarDay++) {
+ if (isBitOn(calendarDay)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * @param context for accessing resources
+ * @param order the order in which to present the weekdays
+ * @param forceLongNames if {@code true} the un-abbreviated weekdays are used
+ * @return the enabled weekdays in the given {@code order}
+ */
+ private String toString(Context context, Order order, boolean forceLongNames) {
+ if (!isRepeating()) {
+ return "";
+ }
+
+ if (mBits == ALL_DAYS) {
+ return context.getString(R.string.every_day);
+ }
+
+ final boolean longNames = forceLongNames || getCount() <= 1;
+ final DateFormatSymbols dfs = new DateFormatSymbols();
+ final String[] weekdays = longNames ? dfs.getWeekdays() : dfs.getShortWeekdays();
+
+ final String separator = context.getString(R.string.day_concat);
+
+ final StringBuilder builder = new StringBuilder(40);
+ for (int calendarDay : order.getCalendarDays()) {
+ if (isBitOn(calendarDay)) {
+ if (builder.length() > 0) {
+ builder.append(separator);
+ }
+ builder.append(weekdays[calendarDay]);
+ }
+ }
+ return builder.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/WidgetDAO.java b/src/com/android/deskclock/data/WidgetDAO.java
index 3416055..da7dc19 100644
--- a/src/com/android/deskclock/data/WidgetDAO.java
+++ b/src/com/android/deskclock/data/WidgetDAO.java
@@ -16,11 +16,8 @@
package com.android.deskclock.data;
-import android.content.Context;
import android.content.SharedPreferences;
-import com.android.deskclock.Utils;
-
/**
* This class encapsulates the transfer of data between widget objects and their permanent storage
* in {@link SharedPreferences}.
@@ -37,8 +34,7 @@
* @param count the number of widgets of the given type
* @return the delta between the new count and the old count
*/
- static int updateWidgetCount(Context context, Class widgetProviderClass, int count) {
- final SharedPreferences prefs = Utils.getDefaultSharedPreferences(context);
+ static int updateWidgetCount(SharedPreferences prefs, Class widgetProviderClass, int count) {
final String key = widgetProviderClass.getSimpleName() + WIDGET_COUNT;
final int oldCount = prefs.getInt(key, 0);
if (count == 0) {
diff --git a/src/com/android/deskclock/data/WidgetModel.java b/src/com/android/deskclock/data/WidgetModel.java
index 6d6c915..27fdd2e 100644
--- a/src/com/android/deskclock/data/WidgetModel.java
+++ b/src/com/android/deskclock/data/WidgetModel.java
@@ -16,7 +16,7 @@
package com.android.deskclock.data;
-import android.content.Context;
+import android.content.SharedPreferences;
import android.support.annotation.StringRes;
import com.android.deskclock.R;
@@ -27,10 +27,10 @@
*/
final class WidgetModel {
- private final Context mContext;
+ private final SharedPreferences mPrefs;
- WidgetModel(Context context) {
- mContext = context;
+ WidgetModel(SharedPreferences prefs) {
+ mPrefs = prefs;
}
/**
@@ -39,7 +39,7 @@
* @param eventCategoryId identifies the category of event to send
*/
void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
- int delta = WidgetDAO.updateWidgetCount(mContext, widgetClass, count);
+ int delta = WidgetDAO.updateWidgetCount(mPrefs, widgetClass, count);
for (; delta > 0; delta--) {
Events.sendEvent(eventCategoryId, R.string.action_create, 0);
}
diff --git a/src/com/android/deskclock/events/EventTracker.java b/src/com/android/deskclock/events/EventTracker.java
index 97955c9..d7b30c0 100644
--- a/src/com/android/deskclock/events/EventTracker.java
+++ b/src/com/android/deskclock/events/EventTracker.java
@@ -20,14 +20,12 @@
public interface EventTracker {
/**
- * Send category, action and label describing an event to Log system.
+ * Record the event in some form or fashion.
*
- * @param category string resource id indicating Alarm, Clock, Timer or Stopwatch or 0 for no
- * category
- * @param action string resource id indicating how the entity was altered;
- * e.g. create, delete, fire, etc or 0 for no action
- * @param label string resource id indicating where the action originated;
- * e.g. DeskClock (UI), Intent, Notification, etc. or 0 for no label
+ * @param category indicates what entity raised the event: Alarm, Clock, Timer or Stopwatch
+ * @param action indicates how the entity was altered; e.g. create, delete, fire, etc.
+ * @param label indicates where the action originated; e.g. DeskClock (UI), Intent,
+ * Notification, etc.; 0 indicates no label could be established
*/
void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label);
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/Events.java b/src/com/android/deskclock/events/Events.java
index 66b4bf6..01e7701 100644
--- a/src/com/android/deskclock/events/Events.java
+++ b/src/com/android/deskclock/events/Events.java
@@ -19,25 +19,16 @@
import android.support.annotation.StringRes;
import com.android.deskclock.R;
+import com.android.deskclock.controller.Controller;
-import java.util.ArrayList;
-import java.util.Collection;
-
+/**
+ * This thin layer over {@link Controller#sendEvent} eases the API usage.
+ */
public final class Events {
/** Extra describing the entity responsible for the action being performed. */
public static final String EXTRA_EVENT_LABEL = "com.android.deskclock.extra.EVENT_LABEL";
- private static final Collection<EventTracker> sEventTrackers = new ArrayList<>();
-
- public static void addEventTracker(EventTracker eventTracker) {
- sEventTrackers.add(eventTracker);
- }
-
- public static void removeEventTracker(EventTracker eventTracker) {
- sEventTrackers.remove(eventTracker);
- }
-
/**
* Tracks an alarm event.
*
@@ -99,8 +90,6 @@
*/
public static void sendEvent(@StringRes int category, @StringRes int action,
@StringRes int label) {
- for (EventTracker eventTracker : sEventTrackers) {
- eventTracker.sendEvent(category, action, label);
- }
+ Controller.getController().sendEvent(category, action, label);
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/ShortcutEventTracker.java b/src/com/android/deskclock/events/ShortcutEventTracker.java
index 444a476..c56a925 100644
--- a/src/com/android/deskclock/events/ShortcutEventTracker.java
+++ b/src/com/android/deskclock/events/ShortcutEventTracker.java
@@ -31,13 +31,11 @@
@TargetApi(Build.VERSION_CODES.N_MR1)
public final class ShortcutEventTracker implements EventTracker {
- private final Context mContext;
private final ShortcutManager mShortcutManager;
private final Set<String> shortcuts = new ArraySet<>(5);
public ShortcutEventTracker(Context context) {
- mContext = context;
- mShortcutManager = mContext.getSystemService(ShortcutManager.class);
+ mShortcutManager = context.getSystemService(ShortcutManager.class);
final UiDataModel uidm = UiDataModel.getUiDataModel();
shortcuts.add(uidm.getShortcutId(R.string.category_alarm, R.string.action_create));
shortcuts.add(uidm.getShortcutId(R.string.category_timer, R.string.action_create));
@@ -53,11 +51,4 @@
mShortcutManager.reportShortcutUsed(shortcutId);
}
}
-
- /**
- * @return Resource string represented by a given resource id, null if resId is invalid (0).
- */
- private String safeGetString(@StringRes int resId) {
- return resId == 0 ? null : mContext.getString(resId);
- }
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/Alarm.java b/src/com/android/deskclock/provider/Alarm.java
index d96d683..fc8aebd 100644
--- a/src/com/android/deskclock/provider/Alarm.java
+++ b/src/com/android/deskclock/provider/Alarm.java
@@ -30,6 +30,7 @@
import com.android.deskclock.R;
import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Weekdays;
import java.util.Calendar;
import java.util.LinkedList;
@@ -118,7 +119,7 @@
values.put(ENABLED, alarm.enabled ? 1 : 0);
values.put(HOUR, alarm.hour);
values.put(MINUTES, alarm.minutes);
- values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBitSet());
+ values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBits());
values.put(VIBRATE, alarm.vibrate ? 1 : 0);
values.put(LABEL, alarm.label);
values.put(DELETE_AFTER_USE, alarm.deleteAfterUse);
@@ -152,7 +153,31 @@
*/
public static CursorLoader getAlarmsCursorLoader(Context context) {
return new CursorLoader(context, ALARMS_WITH_INSTANCES_URI,
- QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER);
+ QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) {
+ @Override
+ public void onContentChanged() {
+ // There is a bug in Loader which can result in stale data if a loader is stopped
+ // immediately after a call to onContentChanged. As a workaround we stop the
+ // loader before delivering onContentChanged to ensure mContentChanged is set to
+ // true before forceLoad is called.
+ if (isStarted() && !isAbandoned()) {
+ stopLoading();
+ super.onContentChanged();
+ startLoading();
+ } else {
+ super.onContentChanged();
+ }
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ // Prime the ringtone title cache for later access. Most alarms will refer to
+ // system ringtones.
+ DataModel.getDataModel().loadRingtoneTitles();
+
+ return super.loadInBackground();
+ }
+ };
}
/**
@@ -209,16 +234,13 @@
}
public static boolean isTomorrow(Alarm alarm, Calendar now) {
- final int alarmHour = alarm.hour;
- final int currHour = now.get(Calendar.HOUR_OF_DAY);
- // If the alarm is not snoozed and the time is less than the current time, it must be
- // firing tomorrow.
- // If the alarm is snoozed, return "false" to indicate that this alarm is firing today.
- // (The alarm must have already rung today in order to be snoozed, and this function is only
- // called on non-repeating alarms.)
- return alarm.instanceState != AlarmInstance.SNOOZE_STATE
- && (alarmHour < currHour
- || (alarmHour == currHour && alarm.minutes <= now.get(Calendar.MINUTE)));
+ if (alarm.instanceState == AlarmInstance.SNOOZE_STATE) {
+ return false;
+ }
+
+ final int totalAlarmMinutes = alarm.hour * 60 + alarm.minutes;
+ final int totalNowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE);
+ return totalAlarmMinutes <= totalNowMinutes;
}
public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) {
@@ -257,7 +279,7 @@
public boolean enabled;
public int hour;
public int minutes;
- public DaysOfWeek daysOfWeek;
+ public Weekdays daysOfWeek;
public boolean vibrate;
public String label;
public Uri alert;
@@ -275,7 +297,7 @@
this.hour = hour;
this.minutes = minutes;
this.vibrate = true;
- this.daysOfWeek = new DaysOfWeek(0);
+ this.daysOfWeek = Weekdays.NONE;
this.label = "";
this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
this.deleteAfterUse = false;
@@ -286,7 +308,7 @@
enabled = c.getInt(ENABLED_INDEX) == 1;
hour = c.getInt(HOUR_INDEX);
minutes = c.getInt(MINUTES_INDEX);
- daysOfWeek = new DaysOfWeek(c.getInt(DAYS_OF_WEEK_INDEX));
+ daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX));
vibrate = c.getInt(VIBRATE_INDEX) == 1;
label = c.getString(LABEL_INDEX);
deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1;
@@ -310,7 +332,7 @@
enabled = p.readInt() == 1;
hour = p.readInt();
minutes = p.readInt();
- daysOfWeek = new DaysOfWeek(p.readInt());
+ daysOfWeek = Weekdays.fromBits(p.readInt());
vibrate = p.readInt() == 1;
label = p.readString();
alert = p.readParcelable(null);
@@ -344,7 +366,7 @@
p.writeInt(enabled ? 1 : 0);
p.writeInt(hour);
p.writeInt(minutes);
- p.writeInt(daysOfWeek.getBitSet());
+ p.writeInt(daysOfWeek.getBits());
p.writeInt(vibrate ? 1 : 0);
p.writeString(label);
p.writeParcelable(alert, flags);
@@ -366,8 +388,8 @@
/**
*
- * @param currentTime
- * @return Previous firing time, or null if this is a one-time alarm.
+ * @param currentTime the current time
+ * @return previous firing time, or null if this is a one-time alarm.
*/
public Calendar getPreviousAlarmTime(Calendar currentTime) {
final Calendar previousInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
@@ -379,7 +401,7 @@
previousInstanceTime.set(Calendar.SECOND, 0);
previousInstanceTime.set(Calendar.MILLISECOND, 0);
- int subtractDays = daysOfWeek.calculateDaysToPreviousAlarm(previousInstanceTime);
+ final int subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime);
if (subtractDays > 0) {
previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays);
return previousInstanceTime;
@@ -404,7 +426,7 @@
}
// The day of the week might be invalid, so find next valid one
- int addDays = daysOfWeek.calculateDaysToNextAlarm(nextInstanceTime);
+ final int addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime);
if (addDays > 0) {
nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
}
diff --git a/src/com/android/deskclock/provider/AlarmInstance.java b/src/com/android/deskclock/provider/AlarmInstance.java
index 1c7650e..9fb7a7b 100644
--- a/src/com/android/deskclock/provider/AlarmInstance.java
+++ b/src/com/android/deskclock/provider/AlarmInstance.java
@@ -27,9 +27,8 @@
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
-import com.android.deskclock.Utils;
import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.settings.SettingsActivity;
+import com.android.deskclock.data.DataModel;
import java.util.Calendar;
import java.util.LinkedList;
@@ -52,11 +51,6 @@
private static final int MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12;
/**
- * Default timeout for alarms in minutes.
- */
- private static final String DEFAULT_ALARM_TIMEOUT_SETTING = "10";
-
- /**
* AlarmInstances start with an invalid id when it hasn't been saved to the database.
*/
public static final long INVALID_ID = -1;
@@ -278,12 +272,6 @@
return deletedRows == 1;
}
- /**
- * @param context
- * @param contentResolver provides access to the content model
- * @param alarmId identifies the alarm in question
- * @param instanceId identifies the instance to keep; all other instances will be removed
- */
public static void deleteOtherInstances(Context context, ContentResolver contentResolver,
long alarmId, long instanceId) {
final List<AlarmInstance> instances = getInstancesByAlarmId(contentResolver, alarmId);
@@ -442,13 +430,10 @@
/**
* Return the time when the alarm should stop firing and be marked as missed.
*
- * @param context to figure out the timeout setting
* @return the time when alarm should be silence, or null if never
*/
- public Calendar getTimeout(Context context) {
- String timeoutSetting = Utils.getDefaultSharedPreferences(context)
- .getString(SettingsActivity.KEY_AUTO_SILENCE, DEFAULT_ALARM_TIMEOUT_SETTING);
- int timeoutMinutes = Integer.parseInt(timeoutSetting);
+ public Calendar getTimeout() {
+ final int timeoutMinutes = DataModel.getDataModel().getAlarmTimeout();
// Alarm silence has been set to "None"
if (timeoutMinutes < 0) {
diff --git a/src/com/android/deskclock/provider/ClockContract.java b/src/com/android/deskclock/provider/ClockContract.java
index bb22af3..5335ccc 100644
--- a/src/com/android/deskclock/provider/ClockContract.java
+++ b/src/com/android/deskclock/provider/ClockContract.java
@@ -118,7 +118,7 @@
* Days of the week encoded as a bit set.
* <p>Type: INTEGER</p>
*
- * {@link DaysOfWeek}
+ * {@link com.android.deskclock.data.Weekdays}
*/
String DAYS_OF_WEEK = "daysofweek";
diff --git a/src/com/android/deskclock/provider/ClockDatabaseHelper.java b/src/com/android/deskclock/provider/ClockDatabaseHelper.java
index b75670d..b6fc900 100644
--- a/src/com/android/deskclock/provider/ClockDatabaseHelper.java
+++ b/src/com/android/deskclock/provider/ClockDatabaseHelper.java
@@ -26,6 +26,7 @@
import android.text.TextUtils;
import com.android.deskclock.LogUtils;
+import com.android.deskclock.data.Weekdays;
import java.util.Calendar;
@@ -163,7 +164,7 @@
alarm.id = cursor.getLong(0);
alarm.hour = cursor.getInt(1);
alarm.minutes = cursor.getInt(2);
- alarm.daysOfWeek = new DaysOfWeek(cursor.getInt(3));
+ alarm.daysOfWeek = Weekdays.fromBits(cursor.getInt(3));
alarm.enabled = cursor.getInt(4) == 1;
alarm.vibrate = cursor.getInt(5) == 1;
alarm.label = cursor.getString(6);
diff --git a/src/com/android/deskclock/provider/ClockProvider.java b/src/com/android/deskclock/provider/ClockProvider.java
index 7b9da87..51be7e8 100644
--- a/src/com/android/deskclock/provider/ClockProvider.java
+++ b/src/com/android/deskclock/provider/ClockProvider.java
@@ -28,6 +28,7 @@
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Build;
+import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -100,10 +101,15 @@
ALARMS_TABLE_NAME + "." + AlarmsColumns._ID + " = " + InstancesColumns.ALARM_ID + ")";
private static final String ALARM_JOIN_INSTANCE_WHERE_STATEMENT =
- InstancesColumns.ALARM_STATE + " IS NULL OR " +
- InstancesColumns.ALARM_STATE + " = (SELECT MIN(" + InstancesColumns.ALARM_STATE +
- ") FROM " + INSTANCES_TABLE_NAME + " WHERE " + InstancesColumns.ALARM_ID +
- " = " + ALARMS_TABLE_NAME + "." + AlarmsColumns._ID + ")";
+ INSTANCES_TABLE_NAME + "." + InstancesColumns._ID + " IS NULL OR " +
+ INSTANCES_TABLE_NAME + "." + InstancesColumns._ID + " = (" +
+ "SELECT " + InstancesColumns._ID +
+ " FROM " + INSTANCES_TABLE_NAME +
+ " WHERE " + InstancesColumns.ALARM_ID +
+ " = " + ALARMS_TABLE_NAME + "." + AlarmsColumns._ID +
+ " ORDER BY " + InstancesColumns.ALARM_STATE + ", " +
+ InstancesColumns.YEAR + ", " + InstancesColumns.MONTH + ", " +
+ InstancesColumns.DAY + " LIMIT 1)";
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
@@ -139,8 +145,8 @@
}
@Override
- public Cursor query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs,
- String sort) {
+ public Cursor query(@NonNull Uri uri, String[] projectionIn, String selection,
+ String[] selectionArgs, String sort) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
@@ -184,7 +190,7 @@
}
@Override
- public String getType(Uri uri) {
+ public String getType(@NonNull Uri uri) {
int match = sURIMatcher.match(uri);
switch (match) {
case ALARMS:
@@ -201,7 +207,7 @@
}
@Override
- public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
+ public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) {
int count;
String alarmId;
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
@@ -228,7 +234,7 @@
}
@Override
- public Uri insert(Uri uri, ContentValues initialValues) {
+ public Uri insert(@NonNull Uri uri, ContentValues initialValues) {
long rowId;
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
switch (sURIMatcher.match(uri)) {
@@ -248,7 +254,7 @@
}
@Override
- public int delete(Uri uri, String where, String[] whereArgs) {
+ public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
int count;
String primaryKey;
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
diff --git a/src/com/android/deskclock/provider/DaysOfWeek.java b/src/com/android/deskclock/provider/DaysOfWeek.java
deleted file mode 100644
index 9673be5..0000000
--- a/src/com/android/deskclock/provider/DaysOfWeek.java
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Copyright (C) 2013 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.provider;
-
-import android.content.Context;
-
-import com.android.deskclock.R;
-
-import java.text.DateFormatSymbols;
-import java.util.Calendar;
-import java.util.HashSet;
-
-/*
- * Days of week code as a single int.
- * 0x00: no day
- * 0x01: Monday
- * 0x02: Tuesday
- * 0x04: Wednesday
- * 0x08: Thursday
- * 0x10: Friday
- * 0x20: Saturday
- * 0x40: Sunday
- */
-public final class DaysOfWeek {
- // Number if days in the week.
- public static final int DAYS_IN_A_WEEK = 7;
-
- // Value when all days are set
- public static final int ALL_DAYS_SET = 0x7f;
-
- // Value when no days are set
- public static final int NO_DAYS_SET = 0;
-
- /**
- * Need to have monday start at index 0 to be backwards compatible. This converts
- * Calendar.DAY_OF_WEEK constants to our internal bit structure.
- */
- static int convertDayToBitIndex(int day) {
- return (day + 5) % DAYS_IN_A_WEEK;
- }
-
- /**
- * Need to have monday start at index 0 to be backwards compatible. This converts
- * our bit structure to Calendar.DAY_OF_WEEK constant value.
- */
- static int convertBitIndexToDay(int bitIndex) {
- return (bitIndex + 1) % DAYS_IN_A_WEEK + 1;
- }
-
- // Bitmask of all repeating days
- private int mBitSet;
-
- public DaysOfWeek(int bitSet) {
- mBitSet = bitSet;
- }
-
- public String toString(Context context, int firstDay) {
- return toString(context, firstDay, false /* forAccessibility */);
- }
-
- public String toAccessibilityString(Context context, int firstDay) {
- return toString(context, firstDay, true /* forAccessibility */);
- }
-
- private String toString(Context context, int firstDay, boolean forAccessibility) {
- StringBuilder ret = new StringBuilder();
-
- // no days
- if (mBitSet == NO_DAYS_SET) {
- return "";
- }
-
- // every day
- if (mBitSet == ALL_DAYS_SET) {
- return context.getText(R.string.every_day).toString();
- }
-
- // count selected days
- int dayCount = 0;
- int bitSet = mBitSet;
- while (bitSet > 0) {
- if ((bitSet & 1) == 1) dayCount++;
- bitSet >>= 1;
- }
-
- // short or long form?
- DateFormatSymbols dfs = new DateFormatSymbols();
- String[] dayList = (forAccessibility || dayCount <= 1) ?
- dfs.getWeekdays() :
- dfs.getShortWeekdays();
-
- // In this system, Mon = 0, Sun = 6, etc.
- // startDay is stored corresponding to Calendar.DAY_OF_WEEK where Sun = 0, Mon = 2, etc.
- final int startDay = convertDayToBitIndex(firstDay);
-
- // selected days, starting from user-selected start day of week
- // iterate starting from user-selected start of day
- for (int bitIndex = startDay; bitIndex < DAYS_IN_A_WEEK + startDay; ++bitIndex) {
- if ((mBitSet & (1 << (bitIndex % DAYS_IN_A_WEEK))) != 0) {
- ret.append(dayList[convertBitIndexToDay(bitIndex)]);
- dayCount -= 1;
- if (dayCount > 0) ret.append(context.getText(R.string.day_concat));
- }
- }
- return ret.toString();
- }
-
- /**
- * Enables or disable certain days of the week.
- *
- * @param daysOfWeek Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, etc.
- */
- public void setDaysOfWeek(boolean value, int ... daysOfWeek) {
- for (int day : daysOfWeek) {
- setBit(convertDayToBitIndex(day), value);
- }
- }
-
- private boolean isBitEnabled(int bitIndex) {
- return ((mBitSet & (1 << bitIndex)) > 0);
- }
-
- private void setBit(int bitIndex, boolean set) {
- if (set) {
- mBitSet |= (1 << bitIndex);
- } else {
- mBitSet &= ~(1 << bitIndex);
- }
- }
-
- public void setBitSet(int bitSet) {
- mBitSet = bitSet;
- }
-
- public int getBitSet() {
- return mBitSet;
- }
-
- /**
- * Returns set of Calendar.MONDAY, Calendar.TUESDAY, etc based on the current mBitSet value
- */
- public HashSet<Integer> getSetDays() {
- final HashSet<Integer> result = new HashSet<Integer>();
- for (int bitIndex = 0; bitIndex < DAYS_IN_A_WEEK; bitIndex++) {
- if (isBitEnabled(bitIndex)) {
- result.add(convertBitIndexToDay(bitIndex));
- }
- }
- return result;
- }
-
- public boolean isRepeating() {
- return mBitSet != NO_DAYS_SET;
- }
-
- /**
- * Returns number of days backwards from today to previous alarm.
- * ex:
- * Daily alarm, current = Tuesday -> 1
- * Weekly alarm on Wednesday, current = Tuesday -> 6
- * One time alarm -> -1
- *
- * @param current must be set to today
- */
- public int calculateDaysToPreviousAlarm(Calendar current) {
- if (!isRepeating()) {
- return -1;
- }
-
- // We only use this on preemptively dismissed alarms, and alarms can only fire once a day,
- // so there is no chance that the previous fire time is on the same day. Start dayCount on
- // previous day.
- int dayCount = -1;
- int currentDayIndex = convertDayToBitIndex(current.get(Calendar.DAY_OF_WEEK));
- for (; dayCount >= -DAYS_IN_A_WEEK; dayCount--) {
- int previousAlarmBitIndex = (currentDayIndex + dayCount);
- if (previousAlarmBitIndex < 0) {
- // Ex. previousAlarmBitIndex = -1 means the day before index 0 = index 6
- previousAlarmBitIndex = previousAlarmBitIndex + DAYS_IN_A_WEEK;
- }
- if (isBitEnabled(previousAlarmBitIndex)) {
- break;
- }
- }
- // return a positive value
- return dayCount * -1;
- }
-
- /**
- * Returns number of days from today until next alarm.
- *
- * @param current must be set to today or the day after the currentTime
- */
- public int calculateDaysToNextAlarm(Calendar current) {
- if (!isRepeating()) {
- return -1;
- }
-
- int dayCount = 0;
- int currentDayIndex = convertDayToBitIndex(current.get(Calendar.DAY_OF_WEEK));
- for (; dayCount < DAYS_IN_A_WEEK; dayCount++) {
- int nextAlarmBitIndex = (currentDayIndex + dayCount) % DAYS_IN_A_WEEK;
- if (isBitEnabled(nextAlarmBitIndex)) {
- break;
- }
- }
- return dayCount;
- }
-
- public void clearAllDays() {
- mBitSet = NO_DAYS_SET;
- }
-
- @Override
- public String toString() {
- return "DaysOfWeek{" +
- "mBitSet=" + mBitSet +
- '}';
- }
-}
diff --git a/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java
new file mode 100644
index 0000000..4b3ecab
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.net.Uri;
+
+import com.android.deskclock.ItemAdapter;
+
+import static android.support.v7.widget.RecyclerView.NO_ID;
+
+final class AddCustomRingtoneHolder extends ItemAdapter.ItemHolder<Uri> {
+
+ AddCustomRingtoneHolder() {
+ super(null, NO_ID);
+ }
+
+ @Override
+ public int getItemViewType() {
+ return AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.java b/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.java
new file mode 100644
index 0000000..5cc6fad
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.deskclock.ItemAdapter.ItemViewHolder;
+import com.android.deskclock.R;
+
+import static android.view.View.GONE;
+
+final class AddCustomRingtoneViewHolder extends ItemViewHolder<AddCustomRingtoneHolder>
+ implements View.OnClickListener {
+
+ static final int VIEW_TYPE_ADD_NEW = Integer.MIN_VALUE;
+ static final int CLICK_ADD_NEW = VIEW_TYPE_ADD_NEW;
+
+ private AddCustomRingtoneViewHolder(View itemView) {
+ super(itemView);
+ itemView.setOnClickListener(this);
+
+ final View selectedView = itemView.findViewById(R.id.sound_image_selected);
+ selectedView.setVisibility(GONE);
+
+ final TextView nameView = (TextView) itemView.findViewById(R.id.ringtone_name);
+ nameView.setText(itemView.getContext().getString(R.string.add_new_sound));
+ nameView.setAlpha(0.63f);
+
+ final ImageView imageView = (ImageView) itemView.findViewById(R.id.ringtone_image);
+ imageView.setImageResource(R.drawable.ic_add_white_24dp);
+ imageView.setAlpha(0.63f);
+ }
+
+ @Override
+ public void onClick(View view) {
+ notifyItemClicked(AddCustomRingtoneViewHolder.CLICK_ADD_NEW);
+ }
+
+ public static class Factory implements ItemViewHolder.Factory {
+
+ private final LayoutInflater mInflater;
+
+ Factory(LayoutInflater inflater) {
+ mInflater = inflater;
+ }
+
+ @Override
+ public ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
+ final View itemView = mInflater.inflate(R.layout.ringtone_item_sound, parent, false);
+ return new AddCustomRingtoneViewHolder(itemView);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java
new file mode 100644
index 0000000..619d45f
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import com.android.deskclock.data.CustomRingtone;
+
+class CustomRingtoneHolder extends RingtoneHolder {
+
+ CustomRingtoneHolder(CustomRingtone ringtone) {
+ super(ringtone.getUri(), ringtone.getTitle(), ringtone.hasPermissions());
+ }
+
+ @Override
+ public int getItemViewType() {
+ return RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/HeaderHolder.java b/src/com/android/deskclock/ringtone/HeaderHolder.java
new file mode 100644
index 0000000..5620842
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/HeaderHolder.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.net.Uri;
+import android.support.annotation.StringRes;
+
+import com.android.deskclock.ItemAdapter;
+
+import static android.support.v7.widget.RecyclerView.NO_ID;
+
+final class HeaderHolder extends ItemAdapter.ItemHolder<Uri> {
+
+ private final @StringRes int mTextResId;
+
+ HeaderHolder(@StringRes int textResId) {
+ super(null, NO_ID);
+ mTextResId = textResId;
+ }
+
+ @StringRes int getTextResId() {
+ return mTextResId;
+ }
+
+ @Override
+ public int getItemViewType() {
+ return HeaderViewHolder.VIEW_TYPE_ITEM_HEADER;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/HeaderViewHolder.java b/src/com/android/deskclock/ringtone/HeaderViewHolder.java
new file mode 100644
index 0000000..eb88bb0
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/HeaderViewHolder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.R;
+
+final class HeaderViewHolder extends ItemAdapter.ItemViewHolder<HeaderHolder> {
+
+ static final int VIEW_TYPE_ITEM_HEADER = R.layout.ringtone_item_header;
+
+ private final TextView mItemHeader;
+
+ private HeaderViewHolder(View itemView) {
+ super(itemView);
+ mItemHeader = (TextView) itemView.findViewById(R.id.ringtone_item_header);
+ }
+
+ @Override
+ protected void onBindItemView(HeaderHolder itemHolder) {
+ mItemHeader.setText(itemHolder.getTextResId());
+ }
+
+ public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
+
+ private final LayoutInflater mInflater;
+
+ Factory(LayoutInflater inflater) {
+ mInflater = inflater;
+ }
+
+ @Override
+ public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
+ return new HeaderViewHolder(mInflater.inflate(viewType, parent, false));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneHolder.java b/src/com/android/deskclock/ringtone/RingtoneHolder.java
new file mode 100644
index 0000000..5873a83
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneHolder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.net.Uri;
+
+import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+
+import static android.support.v7.widget.RecyclerView.NO_ID;
+
+abstract class RingtoneHolder extends ItemAdapter.ItemHolder<Uri> {
+
+ private final String mName;
+ private final boolean mHasPermissions;
+ private boolean mSelected;
+ private boolean mPlaying;
+
+ RingtoneHolder(Uri uri, String name) {
+ this(uri, name, true);
+ }
+
+ RingtoneHolder(Uri uri, String name, boolean hasPermissions) {
+ super(uri, NO_ID);
+ mName = name;
+ mHasPermissions = hasPermissions;
+ }
+
+ long getId() { return itemId; }
+ boolean hasPermissions() { return mHasPermissions; }
+ Uri getUri() { return item; }
+
+ boolean isSilent() { return Utils.RINGTONE_SILENT.equals(getUri()); }
+
+ boolean isSelected() { return mSelected; }
+ void setSelected(boolean selected) { mSelected = selected; }
+
+ boolean isPlaying() { return mPlaying; }
+ void setPlaying(boolean playing) { mPlaying = playing; }
+
+ String getName() {
+ return mName != null ? mName : DataModel.getDataModel().getRingtoneTitle(getUri());
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneLoader.java b/src/com/android/deskclock/ringtone/RingtoneLoader.java
new file mode 100644
index 0000000..bace949
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneLoader.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.media.RingtoneManager;
+import android.net.Uri;
+
+import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.LogUtils;
+import com.android.deskclock.R;
+import com.android.deskclock.data.CustomRingtone;
+import com.android.deskclock.data.DataModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.media.AudioManager.STREAM_ALARM;
+import static com.android.deskclock.Utils.RINGTONE_SILENT;
+
+/**
+ * Assembles the list of ItemHolders that back the RecyclerView used to choose a ringtone.
+ */
+class RingtoneLoader extends AsyncTaskLoader<List<ItemAdapter.ItemHolder<Uri>>> {
+
+ private final Uri mDefaultRingtoneUri;
+ private final String mDefaultRingtoneTitle;
+ private List<CustomRingtone> mCustomRingtones;
+
+ RingtoneLoader(Context context, Uri defaultRingtoneUri, String defaultRingtoneTitle) {
+ super(context);
+ mDefaultRingtoneUri = defaultRingtoneUri;
+ mDefaultRingtoneTitle = defaultRingtoneTitle;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ super.onStartLoading();
+
+ mCustomRingtones = DataModel.getDataModel().getCustomRingtones();
+ forceLoad();
+ }
+
+ @Override
+ public List<ItemAdapter.ItemHolder<Uri>> loadInBackground() {
+ // Prime the ringtone title cache for later access.
+ DataModel.getDataModel().loadRingtoneTitles();
+ DataModel.getDataModel().loadRingtonePermissions();
+
+ // Fetch the standard system ringtones.
+ final RingtoneManager ringtoneManager = new RingtoneManager(getContext());
+ ringtoneManager.setType(STREAM_ALARM);
+
+ Cursor systemRingtoneCursor;
+ try {
+ systemRingtoneCursor = ringtoneManager.getCursor();
+ } catch (Exception e) {
+ LogUtils.e("Could not get system ringtone cursor");
+ systemRingtoneCursor = new MatrixCursor(new String[] {});
+ }
+ final int systemRingtoneCount = systemRingtoneCursor.getCount();
+ // item count = # system ringtones + # custom ringtones + 2 headers + Add new music item
+ final int itemCount = systemRingtoneCount + mCustomRingtones.size() + 3;
+
+ final List<ItemAdapter.ItemHolder<Uri>> itemHolders = new ArrayList<>(itemCount);
+
+ // Add the item holder for the Music heading.
+ itemHolders.add(new HeaderHolder(R.string.your_sounds));
+
+ // Add an item holder for each custom ringtone and also cache a pretty name.
+ for (CustomRingtone ringtone : mCustomRingtones) {
+ itemHolders.add(new CustomRingtoneHolder(ringtone));
+ }
+
+ // Add an item holder for the "Add new" music ringtone.
+ itemHolders.add(new AddCustomRingtoneHolder());
+
+ // Add an item holder for the Ringtones heading.
+ itemHolders.add(new HeaderHolder(R.string.device_sounds));
+
+ // Add an item holder for the silent ringtone.
+ itemHolders.add(new SystemRingtoneHolder(RINGTONE_SILENT, null));
+
+ // Add an item holder for the system default alarm sound.
+ itemHolders.add(new SystemRingtoneHolder(mDefaultRingtoneUri, mDefaultRingtoneTitle));
+
+ // Add an item holder for each system ringtone.
+ for (int i = 0; i < systemRingtoneCount; i++) {
+ final Uri ringtoneUri = ringtoneManager.getRingtoneUri(i);
+ itemHolders.add(new SystemRingtoneHolder(ringtoneUri, null));
+ }
+
+ return itemHolders;
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ mCustomRingtones = null;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtonePickerActivity.java b/src/com/android/deskclock/ringtone/RingtonePickerActivity.java
new file mode 100644
index 0000000..46d1e2a
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtonePickerActivity.java
@@ -0,0 +1,674 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.LoaderManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.android.deskclock.BaseActivity;
+import com.android.deskclock.DropShadowController;
+import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.ItemAdapter.OnItemClickedListener;
+import com.android.deskclock.LogUtils;
+import com.android.deskclock.R;
+import com.android.deskclock.RingtonePreviewKlaxon;
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
+import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
+import com.android.deskclock.actionbarmenu.OptionsMenuManager;
+import com.android.deskclock.alarms.AlarmUpdateHandler;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.provider.Alarm;
+
+import java.util.List;
+
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+import static android.media.RingtoneManager.TYPE_ALARM;
+import static android.provider.OpenableColumns.DISPLAY_NAME;
+import static com.android.deskclock.ItemAdapter.ItemViewHolder.Factory;
+import static com.android.deskclock.ringtone.AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW;
+import static com.android.deskclock.ringtone.HeaderViewHolder.VIEW_TYPE_ITEM_HEADER;
+import static com.android.deskclock.ringtone.RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND;
+import static com.android.deskclock.ringtone.RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND;
+
+/**
+ * This activity presents a set of ringtones from which the user may select one. The set includes:
+ * <ul>
+ * <li>system ringtones from the Android framework</li>
+ * <li>a ringtone representing pure silence</li>
+ * <li>a ringtone representing a default ringtone</li>
+ * <li>user-selected audio files available as ringtones</li>
+ * </ul>
+ */
+public class RingtonePickerActivity extends BaseActivity
+ implements LoaderManager.LoaderCallbacks<List<ItemAdapter.ItemHolder<Uri>>> {
+
+ /** Key to an extra that defines resource id to the title of this activity. */
+ private static final String EXTRA_TITLE = "extra_title";
+
+ /** Key to an extra that identifies the alarm to which the selected ringtone is attached. */
+ private static final String EXTRA_ALARM_ID = "extra_alarm_id";
+
+ /** Key to an extra that identifies the selected ringtone. */
+ private static final String EXTRA_RINGTONE_URI = "extra_ringtone_uri";
+
+ /** Key to an extra that defines the uri representing the default ringtone. */
+ private static final String EXTRA_DEFAULT_RINGTONE_URI = "extra_default_ringtone_uri";
+
+ /** Key to an extra that defines the name of the default ringtone. */
+ private static final String EXTRA_DEFAULT_RINGTONE_NAME = "extra_default_ringtone_name";
+
+ /** Key to an instance state value indicating if the selected ringtone is currently playing. */
+ private static final String STATE_KEY_PLAYING = "extra_is_playing";
+
+ /** The controller that shows the drop shadow when content is not scrolled to the top. */
+ private DropShadowController mDropShadowController;
+
+ /** Generates the items in the activity context menu. */
+ private OptionsMenuManager mOptionsMenuManager;
+
+ /** Displays a set of selectable ringtones. */
+ private RecyclerView mRecyclerView;
+
+ /** Stores the set of ItemHolders that wrap the selectable ringtones. */
+ private ItemAdapter<ItemAdapter.ItemHolder<Uri>> mRingtoneAdapter;
+
+ /** The title of the default ringtone. */
+ private String mDefaultRingtoneTitle;
+
+ /** The uri of the default ringtone. */
+ private Uri mDefaultRingtoneUri;
+
+ /** The uri of the ringtone to select after data is loaded. */
+ private Uri mSelectedRingtoneUri;
+
+ /** {@code true} indicates the {@link #mSelectedRingtoneUri} must be played after data load. */
+ private boolean mIsPlaying;
+
+ /** Identifies the alarm to receive the selected ringtone; -1 indicates there is no alarm. */
+ private long mAlarmId;
+
+ /** The location of the custom ringtone to be removed. */
+ private int mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION;
+
+ /**
+ * @return an intent that launches the ringtone picker to edit the ringtone of the given
+ * {@code alarm}
+ */
+ public static Intent createAlarmRingtonePickerIntent(Context context, Alarm alarm) {
+ return new Intent(context, RingtonePickerActivity.class)
+ .putExtra(EXTRA_TITLE, R.string.alarm_sound)
+ .putExtra(EXTRA_ALARM_ID, alarm.id)
+ .putExtra(EXTRA_RINGTONE_URI, alarm.alert)
+ .putExtra(EXTRA_DEFAULT_RINGTONE_URI, RingtoneManager.getDefaultUri(TYPE_ALARM))
+ .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_alarm_ringtone_title);
+ }
+
+ /**
+ * @return an intent that launches the ringtone picker to edit the ringtone of all timers
+ */
+ public static Intent createTimerRingtonePickerIntent(Context context) {
+ final DataModel dataModel = DataModel.getDataModel();
+ return new Intent(context, RingtonePickerActivity.class)
+ .putExtra(EXTRA_TITLE, R.string.timer_sound)
+ .putExtra(EXTRA_RINGTONE_URI, dataModel.getTimerRingtoneUri())
+ .putExtra(EXTRA_DEFAULT_RINGTONE_URI, dataModel.getDefaultTimerRingtoneUri())
+ .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_timer_ringtone_title);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.ringtone_picker);
+ setVolumeControlStream(AudioManager.STREAM_ALARM);
+
+ mOptionsMenuManager = new OptionsMenuManager();
+ mOptionsMenuManager.addMenuItemController(new NavUpMenuItemController(this))
+ .addMenuItemController(MenuItemControllerFactory.getInstance()
+ .buildMenuItemControllers(this));
+
+ final Context context = getApplicationContext();
+ final Intent intent = getIntent();
+
+ if (savedInstanceState != null) {
+ mIsPlaying = savedInstanceState.getBoolean(STATE_KEY_PLAYING);
+ mSelectedRingtoneUri = savedInstanceState.getParcelable(EXTRA_RINGTONE_URI);
+ }
+
+ if (mSelectedRingtoneUri == null) {
+ mSelectedRingtoneUri = intent.getParcelableExtra(EXTRA_RINGTONE_URI);
+ }
+
+ mAlarmId = intent.getLongExtra(EXTRA_ALARM_ID, -1);
+ mDefaultRingtoneUri = intent.getParcelableExtra(EXTRA_DEFAULT_RINGTONE_URI);
+ final int defaultRingtoneTitleId = intent.getIntExtra(EXTRA_DEFAULT_RINGTONE_NAME, 0);
+ mDefaultRingtoneTitle = context.getString(defaultRingtoneTitleId);
+
+ final LayoutInflater inflater = getLayoutInflater();
+ final OnItemClickedListener listener = new ItemClickWatcher();
+ final Factory ringtoneFactory = new RingtoneViewHolder.Factory(inflater);
+ final Factory headerFactory = new HeaderViewHolder.Factory(inflater);
+ final Factory addNewFactory = new AddCustomRingtoneViewHolder.Factory(inflater);
+ mRingtoneAdapter = new ItemAdapter<>();
+ mRingtoneAdapter.withViewTypes(headerFactory, null, VIEW_TYPE_ITEM_HEADER)
+ .withViewTypes(addNewFactory, listener, VIEW_TYPE_ADD_NEW)
+ .withViewTypes(ringtoneFactory, listener, VIEW_TYPE_SYSTEM_SOUND)
+ .withViewTypes(ringtoneFactory, listener, VIEW_TYPE_CUSTOM_SOUND);
+
+ mRecyclerView = (RecyclerView) findViewById(R.id.ringtone_content);
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+ mRecyclerView.setAdapter(mRingtoneAdapter);
+ mRecyclerView.setItemAnimator(null);
+
+ mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ if (mIndexOfRingtoneToRemove != RecyclerView.NO_POSITION) {
+ closeContextMenu();
+ }
+ }
+ });
+
+ final int titleResourceId = intent.getIntExtra(EXTRA_TITLE, 0);
+ setTitle(context.getString(titleResourceId));
+
+ getLoaderManager().initLoader(0 /* id */, null /* args */, this /* callback */);
+
+ registerForContextMenu(mRecyclerView);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ final View dropShadow = findViewById(R.id.drop_shadow);
+ mDropShadowController = new DropShadowController(dropShadow, mRecyclerView);
+ }
+
+ @Override
+ protected void onPause() {
+ mDropShadowController.stop();
+ mDropShadowController = null;
+
+ if (mSelectedRingtoneUri != null) {
+ if (mAlarmId != -1) {
+ final Context context = getApplicationContext();
+ final ContentResolver cr = getContentResolver();
+
+ // Start a background task to fetch the alarm whose ringtone must be updated.
+ new AsyncTask<Void, Void, Alarm>() {
+ @Override
+ protected Alarm doInBackground(Void... parameters) {
+ final Alarm alarm = Alarm.getAlarm(cr, mAlarmId);
+ if (alarm != null) {
+ alarm.alert = mSelectedRingtoneUri;
+ }
+ return alarm;
+ }
+
+ @Override
+ protected void onPostExecute(Alarm alarm) {
+ // Update the default ringtone for future new alarms.
+ DataModel.getDataModel().setDefaultAlarmRingtoneUri(alarm.alert);
+
+ // Start a second background task to persist the updated alarm.
+ new AlarmUpdateHandler(context, null, null)
+ .asyncUpdateAlarm(alarm, false, true);
+ }
+ }.execute();
+ } else {
+ DataModel.getDataModel().setTimerRingtoneUri(mSelectedRingtoneUri);
+ }
+ }
+
+ super.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ if (!isChangingConfigurations()) {
+ stopPlayingRingtone(getSelectedRingtoneHolder(), false);
+ }
+ super.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putBoolean(STATE_KEY_PLAYING, mIsPlaying);
+ outState.putParcelable(EXTRA_RINGTONE_URI, mSelectedRingtoneUri);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ mOptionsMenuManager.onCreateOptionsMenu(menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ mOptionsMenuManager.onPrepareOptionsMenu(menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public Loader<List<ItemAdapter.ItemHolder<Uri>>> onCreateLoader(int id, Bundle args) {
+ return new RingtoneLoader(getApplicationContext(), mDefaultRingtoneUri,
+ mDefaultRingtoneTitle);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader,
+ List<ItemAdapter.ItemHolder<Uri>> itemHolders) {
+ // Update the adapter with fresh data.
+ mRingtoneAdapter.setItems(itemHolders);
+
+ // Attempt to select the requested ringtone.
+ final RingtoneHolder toSelect = getRingtoneHolder(mSelectedRingtoneUri);
+ if (toSelect != null) {
+ toSelect.setSelected(true);
+ mSelectedRingtoneUri = toSelect.getUri();
+ toSelect.notifyItemChanged();
+
+ // Start playing the ringtone if indicated.
+ if (mIsPlaying) {
+ startPlayingRingtone(toSelect);
+ }
+ } else {
+ // Clear the selection since it does not exist in the data.
+ RingtonePreviewKlaxon.stop(this);
+ mSelectedRingtoneUri = null;
+ mIsPlaying = false;
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader) {}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode != RESULT_OK) {
+ return;
+ }
+
+ final Uri uri = data == null ? null : data.getData();
+ if (uri == null) {
+ return;
+ }
+
+ // Bail if the permission to read (playback) the audio at the uri was not granted.
+ final int flags = data.getFlags() & FLAG_GRANT_READ_URI_PERMISSION;
+ if (flags != FLAG_GRANT_READ_URI_PERMISSION) {
+ return;
+ }
+
+ // Start a task to fetch the display name of the audio content and add the custom ringtone.
+ new AddCustomRingtoneTask(uri).execute();
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ // Find the ringtone to be removed.
+ final List<ItemAdapter.ItemHolder<Uri>> items = mRingtoneAdapter.getItems();
+ final RingtoneHolder toRemove = (RingtoneHolder) items.get(mIndexOfRingtoneToRemove);
+ mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION;
+
+ // Launch the confirmation dialog.
+ final FragmentManager manager = getFragmentManager();
+ final boolean hasPermissions = toRemove.hasPermissions();
+ ConfirmRemoveCustomRingtoneDialogFragment.show(manager, toRemove.getUri(), hasPermissions);
+ return true;
+ }
+
+ private RingtoneHolder getRingtoneHolder(Uri uri) {
+ for (ItemAdapter.ItemHolder<Uri> itemHolder : mRingtoneAdapter.getItems()) {
+ if (itemHolder instanceof RingtoneHolder) {
+ final RingtoneHolder ringtoneHolder = (RingtoneHolder) itemHolder;
+ if (ringtoneHolder.getUri().equals(uri)) {
+ return ringtoneHolder;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ RingtoneHolder getSelectedRingtoneHolder() {
+ return getRingtoneHolder(mSelectedRingtoneUri);
+ }
+
+ /**
+ * The given {@code ringtone} will be selected as a side-effect of playing the ringtone.
+ *
+ * @param ringtone the ringtone to be played
+ */
+ private void startPlayingRingtone(RingtoneHolder ringtone) {
+ if (!ringtone.isPlaying() && !ringtone.isSilent()) {
+ RingtonePreviewKlaxon.start(getApplicationContext(), ringtone.getUri());
+ ringtone.setPlaying(true);
+ mIsPlaying = true;
+ }
+ if (!ringtone.isSelected()) {
+ ringtone.setSelected(true);
+ mSelectedRingtoneUri = ringtone.getUri();
+ }
+ ringtone.notifyItemChanged();
+ }
+
+ /**
+ * @param ringtone the ringtone to stop playing
+ * @param deselect {@code true} indicates the ringtone should also be deselected;
+ * {@code false} indicates its selection state should remain unchanged
+ */
+ private void stopPlayingRingtone(RingtoneHolder ringtone, boolean deselect) {
+ if (ringtone == null) {
+ return;
+ }
+
+ if (ringtone.isPlaying()) {
+ RingtonePreviewKlaxon.stop(this);
+ ringtone.setPlaying(false);
+ mIsPlaying = false;
+ }
+ if (deselect && ringtone.isSelected()) {
+ ringtone.setSelected(false);
+ mSelectedRingtoneUri = null;
+ }
+ ringtone.notifyItemChanged();
+ }
+
+ /**
+ * Proceeds with removing the custom ringtone with the given uri.
+ *
+ * @param toRemove identifies the custom ringtone to be removed
+ */
+ private void removeCustomRingtone(Uri toRemove) {
+ new RemoveCustomRingtoneTask(toRemove).execute();
+ }
+
+ /**
+ * This DialogFragment informs the user of the side-effects of removing a custom ringtone while
+ * it is in use by alarms and/or timers and prompts them to confirm the removal.
+ */
+ public static class ConfirmRemoveCustomRingtoneDialogFragment extends DialogFragment {
+
+ private static final String ARG_RINGTONE_URI_TO_REMOVE = "arg_ringtone_uri_to_remove";
+ private static final String ARG_RINGTONE_HAS_PERMISSIONS = "arg_ringtone_has_permissions";
+
+ static void show(FragmentManager manager, Uri toRemove, boolean hasPermissions) {
+ if (manager.isDestroyed()) {
+ return;
+ }
+
+ final Bundle args = new Bundle();
+ args.putParcelable(ARG_RINGTONE_URI_TO_REMOVE, toRemove);
+ args.putBoolean(ARG_RINGTONE_HAS_PERMISSIONS, hasPermissions);
+
+ final DialogFragment fragment = new ConfirmRemoveCustomRingtoneDialogFragment();
+ fragment.setArguments(args);
+ fragment.setCancelable(hasPermissions);
+ fragment.show(manager, "confirm_ringtone_remove");
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Bundle arguments = getArguments();
+ final Uri toRemove = arguments.getParcelable(ARG_RINGTONE_URI_TO_REMOVE);
+
+ final DialogInterface.OnClickListener okListener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ((RingtonePickerActivity) getActivity()).removeCustomRingtone(toRemove);
+ }
+ };
+
+ if (arguments.getBoolean(ARG_RINGTONE_HAS_PERMISSIONS)) {
+ return new AlertDialog.Builder(getActivity())
+ .setPositiveButton(R.string.remove_sound, okListener)
+ .setNegativeButton(android.R.string.cancel, null /* listener */)
+ .setMessage(R.string.confirm_remove_custom_ringtone)
+ .create();
+ } else {
+ return new AlertDialog.Builder(getActivity())
+ .setPositiveButton(R.string.remove_sound, okListener)
+ .setMessage(R.string.custom_ringtone_lost_permissions)
+ .create();
+ }
+ }
+ }
+
+ /**
+ * This click handler alters selection and playback of ringtones. It also launches the system
+ * file chooser to search for openable audio files that may serve as ringtones.
+ */
+ private class ItemClickWatcher implements OnItemClickedListener {
+ @Override
+ public void onItemClicked(ItemAdapter.ItemViewHolder<?> viewHolder, int id) {
+ switch (id) {
+ case AddCustomRingtoneViewHolder.CLICK_ADD_NEW:
+ stopPlayingRingtone(getSelectedRingtoneHolder(), false);
+ startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT)
+ .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("audio/*"), 0);
+ break;
+
+ case RingtoneViewHolder.CLICK_NORMAL:
+ final RingtoneHolder oldSelection = getSelectedRingtoneHolder();
+ final RingtoneHolder newSelection = (RingtoneHolder) viewHolder.getItemHolder();
+
+ // Tapping the existing selection toggles playback of the ringtone.
+ if (oldSelection == newSelection) {
+ if (newSelection.isPlaying()) {
+ stopPlayingRingtone(newSelection, false);
+ } else {
+ startPlayingRingtone(newSelection);
+ }
+ } else {
+ // Tapping a new selection changes the selection and playback.
+ stopPlayingRingtone(oldSelection, true);
+ startPlayingRingtone(newSelection);
+ }
+ break;
+
+ case RingtoneViewHolder.CLICK_LONG_PRESS:
+ mIndexOfRingtoneToRemove = viewHolder.getAdapterPosition();
+ break;
+
+ case RingtoneViewHolder.CLICK_NO_PERMISSIONS:
+ ConfirmRemoveCustomRingtoneDialogFragment.show(getFragmentManager(),
+ ((RingtoneHolder) viewHolder.getItemHolder()).getUri(), false);
+ break;
+ }
+ }
+ }
+
+ /**
+ * This task locates a displayable string in the background that is fit for use as the title of
+ * the audio content. It adds a custom ringtone using the uri and title on the main thread.
+ */
+ private final class AddCustomRingtoneTask extends AsyncTask<Void, Void, String> {
+
+ private final Uri mUri;
+ private final Context mContext;
+
+ private AddCustomRingtoneTask(Uri uri) {
+ mUri = uri;
+ mContext = getApplicationContext();
+ }
+
+ @Override
+ protected String doInBackground(Void... voids) {
+ final ContentResolver contentResolver = mContext.getContentResolver();
+
+ // Take the long-term permission to read (playback) the audio at the uri.
+ contentResolver.takePersistableUriPermission(mUri, FLAG_GRANT_READ_URI_PERMISSION);
+
+ try (Cursor cursor = contentResolver.query(mUri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ // If the file was a media file, return its title.
+ final int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
+ if (titleIndex != -1) {
+ return cursor.getString(titleIndex);
+ }
+
+ // If the file was a simple openable, return its display name.
+ final int displayNameIndex = cursor.getColumnIndex(DISPLAY_NAME);
+ if (displayNameIndex != -1) {
+ String title = cursor.getString(displayNameIndex);
+ final int dotIndex = title.lastIndexOf(".");
+ if (dotIndex > 0) {
+ title = title.substring(0, dotIndex);
+ }
+ return title;
+ }
+ } else {
+ LogUtils.e("No ringtone for uri: %s", mUri);
+ }
+ } catch (Exception e) {
+ LogUtils.e("Unable to locate title for custom ringtone: " + mUri, e);
+ }
+
+ return mContext.getString(R.string.unknown_ringtone_title);
+ }
+
+ @Override
+ protected void onPostExecute(String title) {
+ // Add the new custom ringtone to the data model.
+ DataModel.getDataModel().addCustomRingtone(mUri, title);
+
+ // When the loader completes, it must play the new ringtone.
+ mSelectedRingtoneUri = mUri;
+ mIsPlaying = true;
+
+ // Reload the data to reflect the change in the UI.
+ getLoaderManager().restartLoader(0 /* id */, null /* args */,
+ RingtonePickerActivity.this /* callback */);
+ }
+ }
+
+ /**
+ * Removes a custom ringtone with the given uri. Taking this action has side-effects because
+ * all alarms that use the custom ringtone are reassigned to the Android system default alarm
+ * ringtone. If the application's default alarm ringtone is being removed, it is reset to the
+ * Android system default alarm ringtone. If the application's timer ringtone is being removed,
+ * it is reset to the application's default timer ringtone.
+ */
+ private final class RemoveCustomRingtoneTask extends AsyncTask<Void, Void, Void> {
+
+ private final Uri mRemoveUri;
+ private Uri mSystemDefaultRingtoneUri;
+
+ private RemoveCustomRingtoneTask(Uri removeUri) {
+ mRemoveUri = removeUri;
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ mSystemDefaultRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
+
+ // Update all alarms that use the custom ringtone to use the system default.
+ final ContentResolver cr = getContentResolver();
+ final List<Alarm> alarms = Alarm.getAlarms(cr, null);
+ for (Alarm alarm : alarms) {
+ if (mRemoveUri.equals(alarm.alert)) {
+ alarm.alert = mSystemDefaultRingtoneUri;
+ // Start a second background task to persist the updated alarm.
+ new AlarmUpdateHandler(RingtonePickerActivity.this, null, null)
+ .asyncUpdateAlarm(alarm, false, true);
+ }
+ }
+
+ try {
+ // Release the permission to read (playback) the audio at the uri.
+ cr.releasePersistableUriPermission(mRemoveUri, FLAG_GRANT_READ_URI_PERMISSION);
+ } catch (SecurityException ignore) {
+ // If the file was already deleted from the file system, a SecurityException is
+ // thrown indicating this app did not hold the read permission being released.
+ LogUtils.w("SecurityException while releasing read permission for " + mRemoveUri);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void v) {
+ // Reset the default alarm ringtone if it was just removed.
+ if (mRemoveUri.equals(DataModel.getDataModel().getDefaultAlarmRingtoneUri())) {
+ DataModel.getDataModel().setDefaultAlarmRingtoneUri(mSystemDefaultRingtoneUri);
+ }
+
+ // Reset the timer ringtone if it was just removed.
+ if (mRemoveUri.equals(DataModel.getDataModel().getTimerRingtoneUri())) {
+ final Uri timerRingtoneUri = DataModel.getDataModel().getDefaultTimerRingtoneUri();
+ DataModel.getDataModel().setTimerRingtoneUri(timerRingtoneUri);
+ }
+
+ // Remove the corresponding custom ringtone.
+ DataModel.getDataModel().removeCustomRingtone(mRemoveUri);
+
+ // Find the ringtone to be removed from the adapter.
+ final RingtoneHolder toRemove = getRingtoneHolder(mRemoveUri);
+ if (toRemove == null) {
+ return;
+ }
+
+ // If the ringtone to remove is also the selected ringtone, adjust the selection.
+ if (toRemove.isSelected()) {
+ stopPlayingRingtone(toRemove, false);
+ final RingtoneHolder defaultRingtone = getRingtoneHolder(mDefaultRingtoneUri);
+ if (defaultRingtone != null) {
+ defaultRingtone.setSelected(true);
+ mSelectedRingtoneUri = defaultRingtone.getUri();
+ defaultRingtone.notifyItemChanged();
+ }
+ }
+
+ // Remove the ringtone from the adapter.
+ mRingtoneAdapter.removeItem(toRemove);
+ }
+ }
+}
diff --git a/src/com/android/deskclock/ringtone/RingtoneViewHolder.java b/src/com/android/deskclock/ringtone/RingtoneViewHolder.java
new file mode 100644
index 0000000..396b41d
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneViewHolder.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.graphics.PorterDuff;
+import android.support.v4.content.ContextCompat;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.deskclock.AnimatorUtils;
+import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.R;
+import com.android.deskclock.ThemeUtils;
+import com.android.deskclock.Utils;
+
+import static android.view.View.GONE;
+import static android.view.View.OnClickListener;
+import static android.view.View.OnCreateContextMenuListener;
+import static android.view.View.VISIBLE;
+
+final class RingtoneViewHolder extends ItemAdapter.ItemViewHolder<RingtoneHolder>
+ implements OnClickListener, OnCreateContextMenuListener {
+
+ static final int VIEW_TYPE_SYSTEM_SOUND = R.layout.ringtone_item_sound;
+ static final int VIEW_TYPE_CUSTOM_SOUND = -R.layout.ringtone_item_sound;
+ static final int CLICK_NORMAL = 0;
+ static final int CLICK_LONG_PRESS = -1;
+ static final int CLICK_NO_PERMISSIONS = -2;
+
+ private final View mSelectedView;
+ private final TextView mNameView;
+ private final ImageView mImageView;
+
+ private RingtoneViewHolder(View itemView) {
+ super(itemView);
+ itemView.setOnClickListener(this);
+
+ mSelectedView = itemView.findViewById(R.id.sound_image_selected);
+ mNameView = (TextView) itemView.findViewById(R.id.ringtone_name);
+ mImageView = (ImageView) itemView.findViewById(R.id.ringtone_image);
+ }
+
+ @Override
+ protected void onBindItemView(RingtoneHolder itemHolder) {
+ mNameView.setText(itemHolder.getName());
+ final boolean opaque = itemHolder.isSelected() || !itemHolder.hasPermissions();
+ mNameView.setAlpha(opaque ? 1f : .63f);
+ mImageView.setAlpha(opaque ? 1f : .63f);
+ mImageView.clearColorFilter();
+
+ final int itemViewType = getItemViewType();
+ if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
+ if (!itemHolder.hasPermissions()) {
+ mImageView.setImageResource(R.drawable.ic_ringtone_not_found);
+ final int colorAccent = ThemeUtils.resolveColor(itemView.getContext(),
+ R.attr.colorAccent);
+ mImageView.setColorFilter(colorAccent, PorterDuff.Mode.SRC_ATOP);
+ } else {
+ mImageView.setImageResource(R.drawable.placeholder_album_artwork);
+ }
+ } else if (itemHolder.item == Utils.RINGTONE_SILENT) {
+ mImageView.setImageResource(R.drawable.ic_ringtone_silent);
+ } else if (itemHolder.isPlaying()) {
+ mImageView.setImageResource(R.drawable.ic_ringtone_active);
+ } else {
+ mImageView.setImageResource(R.drawable.ic_ringtone);
+ }
+ AnimatorUtils.startDrawableAnimation(mImageView);
+
+ mSelectedView.setVisibility(itemHolder.isSelected() ? VISIBLE : GONE);
+
+ final int bgColorId = itemHolder.isSelected() ? R.color.white_08p : R.color.transparent;
+ itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), bgColorId));
+
+ if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
+ itemView.setOnCreateContextMenuListener(this);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (getItemHolder().hasPermissions()) {
+ notifyItemClicked(RingtoneViewHolder.CLICK_NORMAL);
+ } else {
+ notifyItemClicked(RingtoneViewHolder.CLICK_NO_PERMISSIONS);
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu contextMenu, View view,
+ ContextMenu.ContextMenuInfo contextMenuInfo) {
+ notifyItemClicked(RingtoneViewHolder.CLICK_LONG_PRESS);
+ contextMenu.add(Menu.NONE, 0, Menu.NONE, R.string.remove_sound);
+ }
+
+ public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
+
+ private final LayoutInflater mInflater;
+
+ Factory(LayoutInflater inflater) {
+ mInflater = inflater;
+ }
+
+ @Override
+ public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
+ final View itemView = mInflater.inflate(R.layout.ringtone_item_sound, parent, false);
+ return new RingtoneViewHolder(itemView);
+ }
+ }
+}
diff --git a/src/com/android/deskclock/ringtone/SystemRingtoneHolder.java b/src/com/android/deskclock/ringtone/SystemRingtoneHolder.java
new file mode 100644
index 0000000..ff531ff
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/SystemRingtoneHolder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2016 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.ringtone;
+
+import android.net.Uri;
+
+final class SystemRingtoneHolder extends RingtoneHolder {
+
+ SystemRingtoneHolder(Uri uri, String name) {
+ super(uri, name);
+ }
+
+ @Override
+ public int getItemViewType() {
+ return RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/AlarmVolumePreference.java b/src/com/android/deskclock/settings/AlarmVolumePreference.java
index 990b895..9cb04c2 100644
--- a/src/com/android/deskclock/settings/AlarmVolumePreference.java
+++ b/src/com/android/deskclock/settings/AlarmVolumePreference.java
@@ -16,9 +16,12 @@
package com.android.deskclock.settings;
+import android.annotation.TargetApi;
+import android.app.NotificationManager;
import android.content.Context;
import android.database.ContentObserver;
import android.media.AudioManager;
+import android.os.Build;
import android.provider.Settings;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder;
@@ -29,9 +32,11 @@
import com.android.deskclock.R;
import com.android.deskclock.RingtonePreviewKlaxon;
+import com.android.deskclock.Utils;
import com.android.deskclock.data.DataModel;
import static android.content.Context.AUDIO_SERVICE;
+import static android.content.Context.NOTIFICATION_SERVICE;
import static android.media.AudioManager.STREAM_ALARM;
public class AlarmVolumePreference extends Preference {
@@ -116,7 +121,20 @@
}
private void onSeekbarChanged() {
+ mSeekbar.setEnabled(doesDoNotDisturbAllowAlarmPlayback());
mAlarmIcon.setImageResource(mSeekbar.getProgress() == 0 ?
R.drawable.ic_alarm_off_24dp : R.drawable.ic_alarm_small);
}
+
+ private boolean doesDoNotDisturbAllowAlarmPlayback() {
+ return !Utils.isNOrLater() || doesDoNotDisturbAllowAlarmPlaybackNPlus();
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private boolean doesDoNotDisturbAllowAlarmPlaybackNPlus() {
+ final NotificationManager notificationManager = (NotificationManager)
+ getContext().getSystemService(NOTIFICATION_SERVICE);
+ return notificationManager.getCurrentInterruptionFilter() !=
+ NotificationManager.INTERRUPTION_FILTER_NONE;
+ }
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/CrescendoLengthDialogFragment.java b/src/com/android/deskclock/settings/CrescendoLengthDialogFragment.java
deleted file mode 100644
index f94273f..0000000
--- a/src/com/android/deskclock/settings/CrescendoLengthDialogFragment.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.settings;
-
-import android.os.Bundle;
-import android.support.v14.preference.PreferenceDialogFragment;
-import android.support.v7.preference.Preference;
-import android.view.View;
-import android.widget.NumberPicker;
-import android.widget.TextView;
-
-import com.android.deskclock.NumberPickerCompat;
-import com.android.deskclock.R;
-import com.android.deskclock.uidata.UiDataModel;
-
-public class CrescendoLengthDialogFragment extends PreferenceDialogFragment {
-
- private static final int CRESCENDO_TIME_STEP = 5;
-
- private NumberPickerCompat mNumberPickerView;
-
- public static PreferenceDialogFragment newInstance(Preference preference) {
- final PreferenceDialogFragment fragment = new CrescendoLengthDialogFragment();
-
- final Bundle bundle = new Bundle();
- bundle.putString(ARG_KEY, preference.getKey());
- fragment.setArguments(bundle);
-
- return fragment;
- }
-
- @Override
- protected void onBindDialogView(View view) {
- final CrescendoLengthDialogPreference preference =
- (CrescendoLengthDialogPreference) getPreference();
- final int crescendoSeconds = preference.getPersistedCrescendoLength();
-
- final TextView unitView = (TextView) view.findViewById(R.id.title);
- unitView.setText(R.string.crescendo_picker_label);
- updateUnits(unitView, crescendoSeconds);
-
- final String[] displayedValues = new String[13];
- displayedValues[0] = getString(R.string.no_crescendo_duration);
- for (int i = 1; i < displayedValues.length; i++) {
- final int length = i * CRESCENDO_TIME_STEP;
- displayedValues[i] = UiDataModel.getUiDataModel().getFormattedNumber(length);
- }
-
- mNumberPickerView = (NumberPickerCompat) view.findViewById(R.id.seconds_picker);
- mNumberPickerView.setDisplayedValues(displayedValues);
- mNumberPickerView.setMinValue(0);
- mNumberPickerView.setMaxValue(displayedValues.length - 1);
- mNumberPickerView.setValue(crescendoSeconds / CRESCENDO_TIME_STEP);
-
- mNumberPickerView.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
- @Override
- public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
- updateUnits(unitView, newVal);
- }
- });
- mNumberPickerView.setOnAnnounceValueChangedListener(
- new NumberPickerCompat.OnAnnounceValueChangedListener() {
- @Override
- public void onAnnounceValueChanged(NumberPicker picker, int value,
- String displayedValue) {
- final String announceString;
- if (value == 0) {
- announceString = getString(R.string.no_crescendo_duration);
- } else {
- announceString = getString(R.string.crescendo_duration, displayedValue);
- }
- picker.announceForAccessibility(announceString);
- }
- });
- }
-
- @Override
- public void onDialogClosed(boolean positiveResult) {
- if (positiveResult) {
- final CrescendoLengthDialogPreference preference =
- (CrescendoLengthDialogPreference) getPreference();
- preference.persistCrescendoLength(mNumberPickerView.getValue() * CRESCENDO_TIME_STEP);
- preference.updateSummary();
- }
- }
-
- private void updateUnits(TextView unitView, int crescendoSeconds) {
- final int visibility = crescendoSeconds == 0 ? View.INVISIBLE : View.VISIBLE;
- unitView.setVisibility(visibility);
- }
-}
diff --git a/src/com/android/deskclock/settings/CrescendoLengthDialogPreference.java b/src/com/android/deskclock/settings/CrescendoLengthDialogPreference.java
deleted file mode 100644
index 191a4e1..0000000
--- a/src/com/android/deskclock/settings/CrescendoLengthDialogPreference.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.settings;
-
-import android.content.Context;
-import android.support.v7.preference.DialogPreference;
-import android.util.AttributeSet;
-
-import com.android.deskclock.R;
-import com.android.deskclock.uidata.UiDataModel;
-
-public class CrescendoLengthDialogPreference extends DialogPreference {
-
- private static final String DEFAULT_CRESCENDO_TIME = "0";
-
- public CrescendoLengthDialogPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public int getPersistedCrescendoLength() {
- return Integer.parseInt(getPersistedString(DEFAULT_CRESCENDO_TIME));
- }
-
- public void persistCrescendoLength(int crescendoSeconds) {
- persistString(Integer.toString(crescendoSeconds));
- }
-
- public void updateSummary() {
- final int crescendoSeconds = getPersistedCrescendoLength();
- if (crescendoSeconds == 0) {
- setSummary(getContext().getString(R.string.no_crescendo_duration));
- } else {
- final String length = UiDataModel.getUiDataModel().getFormattedNumber(crescendoSeconds);
- setSummary(getContext().getString(R.string.crescendo_duration, length));
- }
- }
-}
diff --git a/src/com/android/deskclock/settings/SettingsActivity.java b/src/com/android/deskclock/settings/SettingsActivity.java
index 4a01e18..59a7b73 100644
--- a/src/com/android/deskclock/settings/SettingsActivity.java
+++ b/src/com/android/deskclock/settings/SettingsActivity.java
@@ -16,38 +16,32 @@
package com.android.deskclock.settings;
-import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
-import android.net.Uri;
import android.os.Bundle;
+import android.os.Vibrator;
import android.provider.Settings;
-import android.support.annotation.NonNull;
-import android.support.v14.preference.PreferenceDialogFragment;
-import android.support.v14.preference.PreferenceFragment;
import android.support.v7.preference.ListPreference;
+import android.support.v7.preference.ListPreferenceDialogFragmentCompat;
import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceDialogFragmentCompat;
+import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.TwoStatePreference;
-import android.text.format.DateUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.android.deskclock.BaseActivity;
import com.android.deskclock.DropShadowController;
-import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
-import com.android.deskclock.RingtonePickerDialogFragment;
import com.android.deskclock.Utils;
-import com.android.deskclock.actionbarmenu.OptionsMenuManager;
import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
+import com.android.deskclock.actionbarmenu.OptionsMenuManager;
import com.android.deskclock.data.DataModel;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.TimeZone;
+import com.android.deskclock.data.TimeZones;
+import com.android.deskclock.data.Weekdays;
+import com.android.deskclock.ringtone.RingtonePickerActivity;
/**
* Settings for the Alarm Clock.
@@ -61,6 +55,7 @@
public static final String KEY_TIMER_VIBRATE = "timer_vibrate";
public static final String KEY_AUTO_SILENCE = "auto_silence";
public static final String KEY_CLOCK_STYLE = "clock_style";
+ public static final String KEY_CLOCK_DISPLAY_SECONDS = "display_clock_seconds";
public static final String KEY_HOME_TZ = "home_time_zone";
public static final String KEY_AUTO_HOME_CLOCK = "automatic_home_clock";
public static final String KEY_DATE_TIME = "date_time";
@@ -92,7 +87,7 @@
// Create the prefs fragment in code to ensure it's created before PreferenceDialogFragment
if (savedInstanceState == null) {
- getFragmentManager().beginTransaction()
+ getSupportFragmentManager().beginTransaction()
.replace(R.id.main, new PrefsFragment(), PREFS_FRAGMENT_TAG)
.disallowAddToBackStack()
.commit();
@@ -105,7 +100,7 @@
final View dropShadow = findViewById(R.id.drop_shadow);
final PrefsFragment fragment =
- (PrefsFragment) getFragmentManager().findFragmentById(R.id.main);
+ (PrefsFragment) getSupportFragmentManager().findFragmentById(R.id.main);
mDropShadowController = new DropShadowController(dropShadow, fragment.getListView());
}
@@ -133,8 +128,7 @@
|| super.onOptionsItemSelected(item);
}
- public static class PrefsFragment extends PreferenceFragment implements
- RingtonePickerDialogFragment.OnRingtoneSelectedListener,
+ public static class PrefsFragment extends PreferenceFragmentCompat implements
Preference.OnPreferenceChangeListener,
Preference.OnPreferenceClickListener {
@@ -142,6 +136,10 @@
public void onCreatePreferences(Bundle bundle, String rootKey) {
getPreferenceManager().setStorageDeviceProtected();
addPreferencesFromResource(R.xml.settings);
+ final Preference timerVibrate = findPreference(KEY_TIMER_VIBRATE);
+ final boolean hasVibrator = ((Vibrator) timerVibrate.getContext()
+ .getSystemService(VIBRATOR_SERVICE)).hasVibrator();
+ timerVibrate.setVisible(hasVibrator);
loadTimeZoneList();
}
@@ -160,54 +158,35 @@
}
@Override
- public void onDisplayPreferenceDialog(Preference preference) {
- final String key = preference.getKey();
- switch (key) {
- case KEY_ALARM_SNOOZE:
- showDialog(SnoozeLengthDialogFragment.newInstance(preference));
- break;
- case KEY_ALARM_CRESCENDO:
- case KEY_TIMER_CRESCENDO:
- showDialog(CrescendoLengthDialogFragment.newInstance(preference));
- break;
- default:
- super.onDisplayPreferenceDialog(preference);
- }
- }
-
- @Override
public boolean onPreferenceChange(Preference pref, Object newValue) {
- final int idx;
switch (pref.getKey()) {
- case KEY_AUTO_SILENCE:
- String delay = (String) newValue;
- updateAutoSnoozeSummary((ListPreference) pref, delay);
+ case KEY_ALARM_CRESCENDO:
+ case KEY_HOME_TZ:
+ case KEY_ALARM_SNOOZE:
+ case KEY_TIMER_CRESCENDO:
+ final ListPreference preference = (ListPreference) pref;
+ final int index = preference.findIndexOfValue((String) newValue);
+ preference.setSummary(preference.getEntries()[index]);
break;
case KEY_CLOCK_STYLE:
- final ListPreference clockStylePref = (ListPreference) pref;
- idx = clockStylePref.findIndexOfValue((String) newValue);
- clockStylePref.setSummary(clockStylePref.getEntries()[idx]);
+ case KEY_WEEK_START:
+ case KEY_VOLUME_BUTTONS:
+ final SimpleMenuPreference simpleMenuPreference = (SimpleMenuPreference) pref;
+ final int i = simpleMenuPreference.findIndexOfValue((String) newValue);
+ pref.setSummary(simpleMenuPreference.getEntries()[i]);
break;
- case KEY_HOME_TZ:
- final ListPreference homeTimezonePref = (ListPreference) pref;
- idx = homeTimezonePref.findIndexOfValue((String) newValue);
- homeTimezonePref.setSummary(homeTimezonePref.getEntries()[idx]);
+ case KEY_CLOCK_DISPLAY_SECONDS:
+ DataModel.getDataModel().setDisplayClockSeconds((boolean) newValue);
+ break;
+ case KEY_AUTO_SILENCE:
+ final String delay = (String) newValue;
+ updateAutoSnoozeSummary((ListPreference) pref, delay);
break;
case KEY_AUTO_HOME_CLOCK:
final boolean autoHomeClockEnabled = ((TwoStatePreference) pref).isChecked();
final Preference homeTimeZonePref = findPreference(KEY_HOME_TZ);
homeTimeZonePref.setEnabled(!autoHomeClockEnabled);
break;
- case KEY_VOLUME_BUTTONS:
- final ListPreference volumeButtonsPref = (ListPreference) pref;
- final int index = volumeButtonsPref.findIndexOfValue((String) newValue);
- volumeButtonsPref.setSummary(volumeButtonsPref.getEntries()[index]);
- break;
- case KEY_WEEK_START:
- final ListPreference weekStartPref = (ListPreference) pref;
- idx = weekStartPref.findIndexOfValue((String) newValue);
- weekStartPref.setSummary(weekStartPref.getEntries()[idx]);
- break;
case KEY_TIMER_VIBRATE:
final TwoStatePreference timerVibratePref = (TwoStatePreference) pref;
DataModel.getDataModel().setTimerVibrate(timerVibratePref.isChecked());
@@ -223,8 +202,8 @@
@Override
public boolean onPreferenceClick(Preference pref) {
- final Activity activity = getActivity();
- if (activity == null) {
+ final Context context = getActivity();
+ if (context == null) {
return false;
}
@@ -235,74 +214,50 @@
startActivity(dialogIntent);
return true;
case KEY_TIMER_RINGTONE:
- new RingtonePickerDialogFragment.Builder()
- .setTitle(R.string.timer_ringtone_title)
- .setDefaultRingtoneTitle(R.string.default_timer_ringtone_title)
- .setDefaultRingtoneUri(
- DataModel.getDataModel().getDefaultTimerRingtoneUri())
- .setExistingRingtoneUri(DataModel.getDataModel().getTimerRingtoneUri())
- .show(getChildFragmentManager(), PREFERENCE_DIALOG_FRAGMENT_TAG);
- default:
- return false;
+ startActivity(RingtonePickerActivity.createTimerRingtonePickerIntent(context));
+ return true;
}
+
+ return false;
}
@Override
- public void onRingtoneSelected(String tag, Uri ringtoneUri) {
- DataModel.getDataModel().setTimerRingtoneUri(ringtoneUri);
+ public void onDisplayPreferenceDialog(Preference preference) {
+ // Only single-selection lists are currently supported.
+ final PreferenceDialogFragmentCompat f;
+ if (preference instanceof ListPreference) {
+ f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
+ } else {
+ throw new IllegalArgumentException("Unsupported DialogPreference type");
+ }
+ showDialog(f);
+ }
- // Manually call onPreferenceChange since PreferenceFragment doesn't listen to
- // external changes via SharedPreferences.
- onPreferenceChange(findPreference(KEY_TIMER_RINGTONE), ringtoneUri);
+ private void showDialog(PreferenceDialogFragmentCompat fragment) {
+ // Don't show dialog if one is already shown.
+ if (getFragmentManager().findFragmentByTag(PREFERENCE_DIALOG_FRAGMENT_TAG) != null) {
+ return;
+ }
+ // Always set the target fragment, this is required by PreferenceDialogFragment
+ // internally.
+ fragment.setTargetFragment(this, 0);
+ // Don't use getChildFragmentManager(), it causes issues on older platforms when the
+ // target fragment is being restored after an orientation change.
+ fragment.show(getFragmentManager(), PREFERENCE_DIALOG_FRAGMENT_TAG);
}
/**
* Reconstruct the timezone list.
*/
private void loadTimeZoneList() {
- final CharSequence[][] timezones = getAllTimezones();
+ final TimeZones timezones = DataModel.getDataModel().getTimeZones();
final ListPreference homeTimezonePref = (ListPreference) findPreference(KEY_HOME_TZ);
- homeTimezonePref.setEntryValues(timezones[0]);
- homeTimezonePref.setEntries(timezones[1]);
+ homeTimezonePref.setEntryValues(timezones.getTimeZoneIds());
+ homeTimezonePref.setEntries(timezones.getTimeZoneNames());
homeTimezonePref.setSummary(homeTimezonePref.getEntry());
homeTimezonePref.setOnPreferenceChangeListener(this);
}
- /**
- * Returns an array of ids/time zones. This returns a double indexed array
- * of ids and time zones for Calendar. It is an inefficient method and
- * shouldn't be called often, but can be used for one time generation of
- * this list.
- *
- * @return double array of tz ids and tz names
- */
- public CharSequence[][] getAllTimezones() {
- final Resources res = getResources();
- final String[] ids = res.getStringArray(R.array.timezone_values);
- final String[] labels = res.getStringArray(R.array.timezone_labels);
-
- int minLength = ids.length;
- if (ids.length != labels.length) {
- minLength = Math.min(minLength, labels.length);
- LogUtils.e("Timezone ids and labels have different length!");
- }
-
- final long currentTimeMillis = System.currentTimeMillis();
- final List<TimeZoneRow> timezones = new ArrayList<>(minLength);
- for (int i = 0; i < minLength; i++) {
- timezones.add(new TimeZoneRow(ids[i], labels[i], currentTimeMillis));
- }
- Collections.sort(timezones);
-
- final CharSequence[][] timeZones = new CharSequence[2][timezones.size()];
- int i = 0;
- for (TimeZoneRow row : timezones) {
- timeZones[0][i] = row.mId;
- timeZones[1][i++] = row.mDisplayName;
- }
- return timeZones;
- }
-
private void refresh() {
final ListPreference autoSilencePref =
(ListPreference) findPreference(KEY_AUTO_SILENCE);
@@ -310,10 +265,19 @@
updateAutoSnoozeSummary(autoSilencePref, delay);
autoSilencePref.setOnPreferenceChangeListener(this);
- final ListPreference clockStylePref = (ListPreference) findPreference(KEY_CLOCK_STYLE);
+ final SimpleMenuPreference clockStylePref = (SimpleMenuPreference)
+ findPreference(KEY_CLOCK_STYLE);
clockStylePref.setSummary(clockStylePref.getEntry());
clockStylePref.setOnPreferenceChangeListener(this);
+ final SimpleMenuPreference volumeButtonsPref = (SimpleMenuPreference)
+ findPreference(KEY_VOLUME_BUTTONS);
+ volumeButtonsPref.setSummary(volumeButtonsPref.getEntry());
+ volumeButtonsPref.setOnPreferenceChangeListener(this);
+
+ final Preference clockSecondsPref = findPreference(KEY_CLOCK_DISPLAY_SECONDS);
+ clockSecondsPref.setOnPreferenceChangeListener(this);
+
final Preference autoHomeClockPref = findPreference(KEY_AUTO_HOME_CLOCK);
final boolean autoHomeClockEnabled =
((TwoStatePreference) autoHomeClockPref).isChecked();
@@ -321,26 +285,22 @@
final ListPreference homeTimezonePref = (ListPreference) findPreference(KEY_HOME_TZ);
homeTimezonePref.setEnabled(autoHomeClockEnabled);
- homeTimezonePref.setSummary(homeTimezonePref.getEntry());
- homeTimezonePref.setOnPreferenceChangeListener(this);
+ refreshListPreference(homeTimezonePref);
- final ListPreference volumeButtonsPref =
- (ListPreference) findPreference(KEY_VOLUME_BUTTONS);
- volumeButtonsPref.setSummary(volumeButtonsPref.getEntry());
- volumeButtonsPref.setOnPreferenceChangeListener(this);
-
- ((SnoozeLengthDialogPreference) findPreference(KEY_ALARM_SNOOZE)).updateSummary();
- ((CrescendoLengthDialogPreference) findPreference(KEY_ALARM_CRESCENDO)).updateSummary();
- ((CrescendoLengthDialogPreference) findPreference(KEY_TIMER_CRESCENDO)).updateSummary();
+ refreshListPreference((ListPreference) findPreference(KEY_ALARM_CRESCENDO));
+ refreshListPreference((ListPreference) findPreference(KEY_TIMER_CRESCENDO));
+ refreshListPreference((ListPreference) findPreference(KEY_ALARM_SNOOZE));
final Preference dateAndTimeSetting = findPreference(KEY_DATE_TIME);
dateAndTimeSetting.setOnPreferenceClickListener(this);
- final ListPreference weekStartPref = (ListPreference) findPreference(KEY_WEEK_START);
+ final SimpleMenuPreference weekStartPref = (SimpleMenuPreference)
+ findPreference(KEY_WEEK_START);
// Set the default value programmatically
- final String value = weekStartPref.getValue();
- final int idx = weekStartPref.findIndexOfValue(
- value == null ? String.valueOf(Utils.DEFAULT_WEEK_START) : value);
+ final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
+ final Integer firstDay = weekdayOrder.getCalendarDays().get(0);
+ final String value = String.valueOf(firstDay);
+ final int idx = weekStartPref.findIndexOfValue(value);
weekStartPref.setValueIndex(idx);
weekStartPref.setSummary(weekStartPref.getEntries()[idx]);
weekStartPref.setOnPreferenceChangeListener(this);
@@ -350,6 +310,11 @@
timerRingtonePref.setSummary(DataModel.getDataModel().getTimerRingtoneTitle());
}
+ private void refreshListPreference(ListPreference preference) {
+ preference.setSummary(preference.getEntry());
+ preference.setOnPreferenceChangeListener(this);
+ }
+
private void updateAutoSnoozeSummary(ListPreference listPref, String delay) {
int i = Integer.parseInt(delay);
if (i == -1) {
@@ -359,54 +324,5 @@
R.plurals.auto_silence_summary, i));
}
}
-
- private void showDialog(PreferenceDialogFragment fragment) {
- // Always set the target fragment, this is required by PreferenceDialogFragment
- // internally.
- fragment.setTargetFragment(this, 0);
- // Don't use getChildFragmentManager(), it causes issues on older platforms when the
- // target fragment is being restored after an orientation change.
- fragment.show(getFragmentManager(), PREFERENCE_DIALOG_FRAGMENT_TAG);
- }
-
- private static class TimeZoneRow implements Comparable<TimeZoneRow> {
-
- public final String mId;
- public final String mDisplayName;
- public final int mOffset;
-
- public TimeZoneRow(String id, String name, long currentTimeMillis) {
- final TimeZone tz = TimeZone.getTimeZone(id);
- final boolean useDaylightTime = tz.useDaylightTime();
- mId = id;
- mOffset = tz.getOffset(currentTimeMillis);
- mDisplayName = buildGmtDisplayName(name, useDaylightTime);
- }
-
- @Override
- public int compareTo(@NonNull TimeZoneRow another) {
- return mOffset - another.mOffset;
- }
-
- public String buildGmtDisplayName(String displayName, boolean useDaylightTime) {
- final int p = Math.abs(mOffset);
- final StringBuilder name = new StringBuilder("(GMT");
- name.append(mOffset < 0 ? '-' : '+');
-
- name.append(p / DateUtils.HOUR_IN_MILLIS);
- name.append(':');
-
- int min = p / 60000;
- min %= 60;
-
- if (min < 10) {
- name.append('0');
- }
- name.append(min);
- name.append(") ");
- name.append(displayName);
- return name.toString();
- }
- }
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/SimpleMenuPreference.java b/src/com/android/deskclock/settings/SimpleMenuPreference.java
new file mode 100644
index 0000000..fa04ef7
--- /dev/null
+++ b/src/com/android/deskclock/settings/SimpleMenuPreference.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 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.settings;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.preference.DropDownPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
+
+/**
+ * Bend {@link DropDownPreference} to support
+ * <a href="https://material.google.com/components/menus.html#menus-behavior">Simple Menus</a>.
+ */
+public class SimpleMenuPreference extends DropDownPreference {
+
+ private SimpleMenuAdapter mAdapter;
+
+ public SimpleMenuPreference(Context context) {
+ this(context, null);
+ }
+
+ public SimpleMenuPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.dropdownPreferenceStyle);
+ }
+
+ public SimpleMenuPreference(Context context, AttributeSet attrs, int defStyle) {
+ this(context, attrs, defStyle, 0);
+ }
+
+ public SimpleMenuPreference(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected ArrayAdapter createAdapter() {
+ mAdapter = new SimpleMenuAdapter(getContext(), R.layout.simple_menu_dropdown_item);
+ return mAdapter;
+ }
+
+ private static void restoreOriginalOrder(CharSequence[] array,
+ int lastSelectedOriginalPosition) {
+ final CharSequence item = array[0];
+ System.arraycopy(array, 1, array, 0, lastSelectedOriginalPosition);
+ array[lastSelectedOriginalPosition] = item;
+ }
+
+ private static void swapSelectedToFront(CharSequence[] array, int position) {
+ final CharSequence item = array[position];
+ System.arraycopy(array, 0, array, 1, position);
+ array[0] = item;
+ }
+
+ private static void setSelectedPosition(CharSequence[] array, int lastSelectedOriginalPosition,
+ int position) {
+ final CharSequence item = array[position];
+ restoreOriginalOrder(array, lastSelectedOriginalPosition);
+ final int originalPosition = Utils.indexOf(array, item);
+ swapSelectedToFront(array, originalPosition);
+ }
+
+ @Override
+ public void setSummary(CharSequence summary) {
+ final CharSequence[] entries = getEntries();
+ final int index = Utils.indexOf(entries, summary);
+ if (index == -1) {
+ throw new IllegalArgumentException("Illegal Summary");
+ }
+ final int lastSelectedOriginalPosition = mAdapter.getLastSelectedOriginalPosition();
+ mAdapter.setSelectedPosition(index);
+ setSelectedPosition(entries, lastSelectedOriginalPosition, index);
+ setSelectedPosition(getEntryValues(), lastSelectedOriginalPosition, index);
+ super.setSummary(summary);
+ }
+
+ private final static class SimpleMenuAdapter extends ArrayAdapter<CharSequence> {
+
+ /** The original position of the last selected element */
+ private int mLastSelectedOriginalPosition = 0;
+
+ SimpleMenuAdapter(Context context, int resource) {
+ super(context, resource);
+ }
+
+ private void restoreOriginalOrder() {
+ final CharSequence item = getItem(0);
+ remove(item);
+ insert(item, mLastSelectedOriginalPosition);
+ }
+
+ private void swapSelectedToFront(int position) {
+ final CharSequence item = getItem(position);
+ remove(item);
+ insert(item, 0);
+ mLastSelectedOriginalPosition = position;
+ }
+
+ int getLastSelectedOriginalPosition() {
+ return mLastSelectedOriginalPosition;
+ }
+
+ void setSelectedPosition(int position) {
+ setNotifyOnChange(false);
+ final CharSequence item = getItem(position);
+ restoreOriginalOrder();
+ final int originalPosition = getPosition(item);
+ swapSelectedToFront(originalPosition);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public View getDropDownView(int position, View convertView, @NonNull ViewGroup parent) {
+ final View view = super.getDropDownView(position, convertView, parent);
+ if (position == 0) {
+ view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white_08p));
+ } else {
+ view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.transparent));
+ }
+ return view;
+ }
+ }
+}
diff --git a/src/com/android/deskclock/settings/SnoozeLengthDialogFragment.java b/src/com/android/deskclock/settings/SnoozeLengthDialogFragment.java
deleted file mode 100644
index aaf30fd..0000000
--- a/src/com/android/deskclock/settings/SnoozeLengthDialogFragment.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.settings;
-
-import android.os.Bundle;
-import android.support.v14.preference.PreferenceDialogFragment;
-import android.support.v7.preference.Preference;
-import android.view.View;
-import android.widget.NumberPicker;
-import android.widget.TextView;
-
-import com.android.deskclock.NumberPickerCompat;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-public class SnoozeLengthDialogFragment extends PreferenceDialogFragment {
-
- private NumberPickerCompat mNumberPickerView;
-
- public static PreferenceDialogFragment newInstance(Preference preference) {
- final PreferenceDialogFragment fragment = new SnoozeLengthDialogFragment();
-
- final Bundle bundle = new Bundle();
- bundle.putString(ARG_KEY, preference.getKey());
- fragment.setArguments(bundle);
-
- return fragment;
- }
-
- @Override
- protected void onBindDialogView(View view) {
- final SnoozeLengthDialogPreference preference =
- (SnoozeLengthDialogPreference) getPreference();
- final int snoozeMinutes = preference.getPersistedSnoozeLength();
-
- final CharSequence units =
- getResources().getQuantityText(R.plurals.snooze_picker_label, snoozeMinutes);
- final TextView unitView = (TextView) view.findViewById(R.id.title);
- unitView.setText(units);
-
- mNumberPickerView = (NumberPickerCompat) view.findViewById(R.id.minutes_picker);
- mNumberPickerView.setMinValue(1);
- mNumberPickerView.setMaxValue(30);
- mNumberPickerView.setValue(snoozeMinutes);
-
- mNumberPickerView.setOnAnnounceValueChangedListener(
- new NumberPickerCompat.OnAnnounceValueChangedListener() {
- @Override
- public void onAnnounceValueChanged(NumberPicker picker, int value,
- String displayedValue) {
- final String announceString = Utils.getNumberFormattedQuantityString(
- getActivity(), R.plurals.snooze_duration, value);
- picker.announceForAccessibility(announceString);
- }
- });
- }
-
- @Override
- public void onDialogClosed(boolean positiveResult) {
- if (positiveResult) {
- final SnoozeLengthDialogPreference preference =
- (SnoozeLengthDialogPreference) getPreference();
- preference.persistSnoozeLength(mNumberPickerView.getValue());
- preference.updateSummary();
- }
- }
-}
diff --git a/src/com/android/deskclock/settings/SnoozeLengthDialogPreference.java b/src/com/android/deskclock/settings/SnoozeLengthDialogPreference.java
deleted file mode 100644
index 0de0fcb..0000000
--- a/src/com/android/deskclock/settings/SnoozeLengthDialogPreference.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.settings;
-
-import android.content.Context;
-import android.support.v7.preference.DialogPreference;
-import android.util.AttributeSet;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-public class SnoozeLengthDialogPreference extends DialogPreference {
-
- private static final String DEFAULT_SNOOZE_TIME = "10";
-
- public SnoozeLengthDialogPreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public int getPersistedSnoozeLength() {
- return Integer.parseInt(getPersistedString(DEFAULT_SNOOZE_TIME));
- }
-
- public void persistSnoozeLength(int snoozeMinutes) {
- persistString(Integer.toString(snoozeMinutes));
- }
-
- public void updateSummary() {
- final int value = getPersistedSnoozeLength();
- final CharSequence summary = Utils.getNumberFormattedQuantityString(
- getContext(), R.plurals.snooze_duration, value);
- setSummary(summary);
- }
-}
diff --git a/src/com/android/deskclock/stopwatch/LapsAdapter.java b/src/com/android/deskclock/stopwatch/LapsAdapter.java
index 4bdb1e9..cb755ff 100644
--- a/src/com/android/deskclock/stopwatch/LapsAdapter.java
+++ b/src/com/android/deskclock/stopwatch/LapsAdapter.java
@@ -33,7 +33,6 @@
import java.text.DecimalFormatSymbols;
import java.util.List;
-import java.util.Locale;
/**
* Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
@@ -197,14 +196,22 @@
builder.append("\n");
// Loop through the laps in the order they were recorded; reverse of display order.
+ final String separator = DecimalFormatSymbols.getInstance().getDecimalSeparator() + " ";
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(separator);
+ final long lapTime = lap.getLapTime();
+ builder.append(formatTime(lapTime, lapTime, " "));
builder.append("\n");
}
+
+ // Append the final lap
+ builder.append(laps.size() + 1);
+ builder.append(separator);
+ final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
+ builder.append(formatTime(lapTime, lapTime, " "));
+ builder.append("\n");
}
return builder.toString();
@@ -216,12 +223,12 @@
* @return e.g. "# 7" if {@code lapCount} less than 10; "# 07" if {@code lapCount} is 10 or more
*/
@VisibleForTesting
- static String formatLapNumber(int lapCount, int lapNumber) {
+ String formatLapNumber(int lapCount, int lapNumber) {
if (lapCount < 10) {
- return String.format(Locale.getDefault(), "# %d", lapNumber);
+ return mContext.getString(R.string.lap_number_single_digit, lapNumber);
+ } else {
+ return mContext.getString(R.string.lap_number_double_digit, lapNumber);
}
-
- return String.format(Locale.getDefault(), "# %02d", lapNumber);
}
/**
diff --git a/src/com/android/deskclock/stopwatch/StopwatchCircleView.java b/src/com/android/deskclock/stopwatch/StopwatchCircleView.java
index f386598..31ae20d 100644
--- a/src/com/android/deskclock/stopwatch/StopwatchCircleView.java
+++ b/src/com/android/deskclock/stopwatch/StopwatchCircleView.java
@@ -26,6 +26,7 @@
import android.view.View;
import com.android.deskclock.R;
+import com.android.deskclock.ThemeUtils;
import com.android.deskclock.Utils;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Lap;
@@ -81,7 +82,7 @@
mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize);
mRemainderColor = Color.WHITE;
- mCompletedColor = Utils.obtainStyledColor(context, R.attr.colorAccent, Color.RED);
+ mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
diff --git a/src/com/android/deskclock/stopwatch/StopwatchFragment.java b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
index 1b1a641..0026b06 100644
--- a/src/com/android/deskclock/stopwatch/StopwatchFragment.java
+++ b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
@@ -17,16 +17,15 @@
package com.android.deskclock.stopwatch;
import android.annotation.SuppressLint;
+import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
+import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Canvas;
-import android.graphics.drawable.Animatable;
-import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
-import android.os.SystemClock;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v4.graphics.ColorUtils;
@@ -35,40 +34,48 @@
import android.support.v7.widget.SimpleItemAnimator;
import android.transition.TransitionManager;
import android.view.LayoutInflater;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
-import android.widget.ImageButton;
+import android.widget.Button;
import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.deskclock.AnimatorUtils;
import com.android.deskclock.DeskClockFragment;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
+import com.android.deskclock.StopwatchTextController;
+import com.android.deskclock.ThemeUtils;
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.data.StopwatchListener;
import com.android.deskclock.events.Events;
-import com.android.deskclock.timer.CountingTimerView;
import com.android.deskclock.uidata.TabListener;
import com.android.deskclock.uidata.UiDataModel;
import com.android.deskclock.uidata.UiDataModel.Tab;
+import static android.R.attr.state_activated;
+import static android.R.attr.state_pressed;
import static android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_IMMEDIATE;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_MORPH;
import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
/**
* Fragment that shows the stopwatch and recorded laps.
*/
public final class StopwatchFragment extends DeskClockFragment {
- /** Milliseconds between redraws. */
- private static final int REDRAW_PERIOD = 25;
+
+ /** Milliseconds between redraws while running. */
+ private static final int REDRAW_PERIOD_RUNNING = 25;
+
+ /** Milliseconds between redraws while paused. */
+ private static final int REDRAW_PERIOD_PAUSED = 500;
/** Keep the screen on when this tab is selected. */
private final TabListener mTabWatcher = new TabWatcher();
@@ -91,14 +98,20 @@
/** Draws the reference lap while the stopwatch is running. */
private StopwatchCircleView mTime;
+ /** The View containing both TextViews of the stopwatch. */
+ private View mStopwatchWrapper;
+
/** Displays the recorded lap times. */
private RecyclerView mLapsList;
- /** Displays the current stopwatch time. */
- private CountingTimerView mTimeText;
+ /** Displays the current stopwatch time (seconds and above only). */
+ private TextView mMainTimeText;
- /** Number of laps the stopwatch is tracking. */
- private int mLapCount;
+ /** Displays the current stopwatch time (hundredths only). */
+ private TextView mHundredthsTimeText;
+
+ /** Formats and displays the text in the stopwatch. */
+ private StopwatchTextController mStopwatchTextController;
/** The public no-arg constructor required by all fragments. */
public StopwatchFragment() {
@@ -112,7 +125,7 @@
mGradientItemDecoration = new GradientItemDecoration(getActivity());
final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false);
- mTime = (StopwatchCircleView) v.findViewById(R.id.stopwatch_time);
+ mTime = (StopwatchCircleView) v.findViewById(R.id.stopwatch_circle);
mLapsList = (RecyclerView) v.findViewById(R.id.laps_list);
((SimpleItemAnimator) mLapsList.getItemAnimator()).setSupportsChangeAnimations(false);
mLapsList.setLayoutManager(mLapsLayoutManager);
@@ -124,17 +137,33 @@
final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
mLapsList.addOnLayoutChangeListener(scrollPositionWatcher);
mLapsList.addOnScrollListener(scrollPositionWatcher);
+ } else {
+ setTabScrolledToTop(true);
}
mLapsList.setAdapter(mLapsAdapter);
// Timer text serves as a virtual start/stop button.
- mTimeText = (CountingTimerView) v.findViewById(R.id.stopwatch_time_text);
- mTimeText.setShowBoundingCircle(mTime != null);
- mTimeText.setVirtualButtonEnabled(true);
- mTimeText.registerVirtualButtonAction(new ToggleStopwatchRunnable());
+ mMainTimeText = (TextView) v.findViewById(R.id.stopwatch_time_text);
+ mHundredthsTimeText = (TextView) v.findViewById(R.id.stopwatch_hundredths_text);
+ mStopwatchTextController = new StopwatchTextController(mMainTimeText, mHundredthsTimeText);
+ mStopwatchWrapper = v.findViewById(R.id.stopwatch_time_wrapper);
DataModel.getDataModel().addStopwatchListener(mStopwatchWatcher);
+ mStopwatchWrapper.setOnClickListener(new TimeClickListener());
+ if (mTime != null) {
+ mStopwatchWrapper.setOnTouchListener(new CircleTouchListener());
+ }
+
+ final Context c = mMainTimeText.getContext();
+ final int colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent);
+ final int textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary);
+ final ColorStateList timeTextColor = new ColorStateList(
+ new int[][] { { -state_activated, -state_pressed }, {} },
+ new int[] { textColorPrimary, colorAccent });
+ mMainTimeText.setTextColor(timeTextColor);
+ mHundredthsTimeText.setTextColor(timeTextColor);
+
return v;
}
@@ -142,8 +171,22 @@
public void onStart() {
super.onStart();
+ final Activity activity = getActivity();
+ final Intent intent = activity.getIntent();
+ if (intent != null) {
+ final String action = intent.getAction();
+ if (StopwatchService.ACTION_START_STOPWATCH.equals(action)) {
+ DataModel.getDataModel().startStopwatch();
+ // Consume the intent
+ activity.setIntent(null);
+ } else if (StopwatchService.ACTION_PAUSE_STOPWATCH.equals(action)) {
+ DataModel.getDataModel().pauseStopwatch();
+ // Consume the intent
+ activity.setIntent(null);
+ }
+ }
+
// Conservatively assume the data in the adapter has changed while the fragment was paused.
- mLapCount = mLapsAdapter.getItemCount();
mLapsAdapter.notifyDataSetChanged();
// Synchronize the user interface with the data model.
@@ -159,7 +202,6 @@
// Stop all updates while the fragment is not visible.
stopUpdatingTime();
- mTimeText.blinkTimeStr(false);
// Stop watching for page changes away from this fragment.
UiDataModel.getUiDataModel().removeTabListener(mTabWatcher);
@@ -181,99 +223,81 @@
}
@Override
- public void onLeftButtonClick(@NonNull ImageButton left) {
+ public void onLeftButtonClick(@NonNull Button left) {
+ doReset();
+ }
+
+ @Override
+ public void onRightButtonClick(@NonNull Button right) {
switch (getStopwatch().getState()) {
case RUNNING:
doAddLap();
break;
case PAUSED:
- doReset();
+ doShare();
break;
}
}
- @Override
- public void onRightButtonClick(@NonNull ImageButton right) {
- doShare();
- }
-
- @Override
- public void onUpdateFab(@NonNull ImageView fab) {
+ private void updateFab(@NonNull ImageView fab, boolean animate) {
if (getStopwatch().isRunning()) {
- fab.setImageResource(R.drawable.ic_pause_white_24dp);
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_play_pause_animation);
+ } else {
+ fab.setImageResource(R.drawable.ic_play_pause);
+ }
fab.setContentDescription(fab.getResources().getString(R.string.sw_pause_button));
} else {
- fab.setImageResource(R.drawable.ic_start_white_24dp);
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_pause_play_animation);
+ } else {
+ fab.setImageResource(R.drawable.ic_pause_play);
+ }
fab.setContentDescription(fab.getResources().getString(R.string.sw_start_button));
}
fab.setVisibility(VISIBLE);
}
- @Override
- public void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
- right.setImageResource(R.drawable.ic_share);
- right.setContentDescription(right.getResources().getString(R.string.sw_share_button));
-
- switch (getStopwatch().getState()) {
- case RESET:
- left.setEnabled(false);
- left.setVisibility(INVISIBLE);
- right.setVisibility(INVISIBLE);
- break;
- case RUNNING:
- final boolean canRecordLaps = canRecordMoreLaps();
- left.setImageResource(R.drawable.ic_lap);
- left.setContentDescription(left.getResources().getString(R.string.sw_lap_button));
- left.setEnabled(canRecordLaps);
- left.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE);
- right.setVisibility(INVISIBLE);
- break;
- case PAUSED:
- left.setEnabled(true);
- left.setImageResource(R.drawable.ic_reset);
- left.setContentDescription(left.getResources().getString(R.string.sw_reset_button));
- left.setVisibility(VISIBLE);
- right.setVisibility(VISIBLE);
- break;
- }
+ public void onUpdateFab(@NonNull ImageView fab) {
+ updateFab(fab, false);
}
@Override
- public void onMorphFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
- right.setImageResource(R.drawable.ic_share);
- right.setContentDescription(right.getResources().getString(R.string.sw_share_button));
+ public void onMorphFab(@NonNull ImageView fab) {
+ // Update the fab's drawable to match the current timer state.
+ updateFab(fab, Utils.isNOrLater());
+ // Animate the drawable.
+ AnimatorUtils.startDrawableAnimation(fab);
+ }
+
+ @Override
+ public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
+ final Resources resources = getResources();
+ left.setClickable(true);
+ left.setText(R.string.sw_reset_button);
+ left.setContentDescription(resources.getString(R.string.sw_reset_button));
switch (getStopwatch().getState()) {
case RESET:
- left.setEnabled(false);
left.setVisibility(INVISIBLE);
+ right.setClickable(true);
right.setVisibility(INVISIBLE);
break;
- case RUNNING: {
- final boolean canRecordLaps = canRecordMoreLaps();
- updateLapIcon(left);
- left.setContentDescription(left.getResources().getString(R.string.sw_lap_button));
- left.setEnabled(canRecordLaps);
- left.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE);
- right.setVisibility(INVISIBLE);
- final Drawable icon = left.getDrawable();
- if (icon instanceof Animatable) {
- ((Animatable) icon).start();
- }
- break;
- }
- case PAUSED: {
- left.setEnabled(true);
- updateResetIcon(left);
- left.setContentDescription(left.getResources().getString(R.string.sw_reset_button));
+ case RUNNING:
left.setVisibility(VISIBLE);
- right.setVisibility(VISIBLE);
- final Drawable icon = left.getDrawable();
- if (icon instanceof Animatable) {
- ((Animatable) icon).start();
- }
+ final boolean canRecordLaps = canRecordMoreLaps();
+ right.setText(R.string.sw_lap_button);
+ right.setContentDescription(resources.getString(R.string.sw_lap_button));
+ right.setClickable(canRecordLaps);
+ right.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE);
break;
- }
+ case PAUSED:
+ left.setVisibility(VISIBLE);
+ right.setClickable(true);
+ right.setVisibility(VISIBLE);
+ right.setText(R.string.sw_share_button);
+ right.setContentDescription(resources.getString(R.string.sw_share_button));
+ break;
}
}
@@ -289,27 +313,6 @@
}
}
- private void updateLapIcon(ImageButton button) {
- if (Utils.isLMR1OrLater() && button.getVisibility() == VISIBLE) {
- final int newLapCount = mLapsAdapter.getItemCount();
- if (newLapCount == mLapCount) {
- button.setImageResource(R.drawable.ic_reset_lap_animation);
- } else {
- button.setImageResource(R.drawable.ic_lap_animation);
- }
- } else {
- button.setImageResource(R.drawable.ic_lap);
- }
- }
-
- private void updateResetIcon(ImageButton button) {
- if (Utils.isLMR1OrLater()) {
- button.setImageResource(R.drawable.ic_lap_reset_animation);
- } else {
- button.setImageResource(R.drawable.ic_reset);
- }
- }
-
/**
* Start the stopwatch.
*/
@@ -330,14 +333,23 @@
* Reset the stopwatch.
*/
private void doReset() {
+ final Stopwatch.State priorState = getStopwatch().getState();
Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock);
DataModel.getDataModel().resetStopwatch();
+ mMainTimeText.setAlpha(1f);
+ mHundredthsTimeText.setAlpha(1f);
+ if (priorState == Stopwatch.State.RUNNING) {
+ updateFab(FAB_MORPH);
+ }
}
/**
* Send stopwatch time and lap times to an external sharing application.
*/
private void doShare() {
+ // Disable the fab buttons to avoid double-taps on the share button.
+ updateFab(BUTTONS_DISABLE);
+
final String[] subjects = getResources().getStringArray(R.array.sw_share_strings);
final String subject = subjects[(int) (Math.random() * subjects.length)];
final String text = mLapsAdapter.getShareText();
@@ -357,7 +369,8 @@
try {
context.startActivity(shareChooserIntent);
} catch (ActivityNotFoundException anfe) {
- LogUtils.e("No compatible receiver is found");
+ LogUtils.e("Cannot share lap data because no suitable receiving Activity exists");
+ updateFab(BUTTONS_IMMEDIATE);
}
}
@@ -374,7 +387,7 @@
}
// Update button states.
- updateFab(FAB_AND_BUTTONS_MORPH);
+ updateFab(BUTTONS_IMMEDIATE);
if (lap.getLapNumber() == 1) {
// Child views from prior lap sets hang around and blit to the screen when adding the
@@ -393,7 +406,6 @@
// Ensure the newly added lap is visible on screen.
mLapsList.scrollToPosition(0);
- mLapCount = mLapsAdapter.getItemCount();
}
/**
@@ -464,14 +476,14 @@
private void startUpdatingTime() {
// Ensure only one copy of the runnable is ever scheduled by first stopping updates.
stopUpdatingTime();
- mTimeText.post(mTimeUpdateRunnable);
+ mMainTimeText.post(mTimeUpdateRunnable);
}
/**
* Remove the runnable that updates times within the UI.
*/
private void stopUpdatingTime() {
- mTimeText.removeCallbacks(mTimeUpdateRunnable);
+ mMainTimeText.removeCallbacks(mTimeUpdateRunnable);
}
/**
@@ -483,9 +495,7 @@
// Compute the total time of the stopwatch.
final Stopwatch stopwatch = getStopwatch();
final long totalTime = stopwatch.getTotalTime();
-
- // Update the total time display.
- mTimeText.setTime(totalTime, true);
+ mStopwatchTextController.setTimeString(totalTime);
// Update the current lap.
final boolean currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0;
@@ -497,7 +507,7 @@
/**
* Synchronize the UI state with the model data.
*/
- private void updateUI(UpdateType updateType) {
+ private void updateUI(@UpdateFabFlag int updateTypes) {
adjustWakeLock();
// Draw the latest stopwatch and current lap times.
@@ -507,20 +517,16 @@
mTime.update();
}
- // Start updates if the stopwatch is running.
final Stopwatch stopwatch = getStopwatch();
- if (stopwatch.isRunning()) {
+ if (!stopwatch.isReset()) {
startUpdatingTime();
}
- // Blink text iff the stopwatch is paused.
- mTimeText.blinkTimeStr(stopwatch.isPaused());
-
// Adjust the visibility of the list of laps.
showOrHideLaps(stopwatch.isReset());
// Update button states.
- updateFab(updateType);
+ updateFab(updateTypes);
}
/**
@@ -530,27 +536,33 @@
private final class TimeUpdateRunnable implements Runnable {
@Override
public void run() {
- final long startTime = SystemClock.elapsedRealtime();
+ final long startTime = Utils.now();
updateTime();
- if (getStopwatch().isRunning()) {
- // Try to maintain a consistent period of time between redraws.
- final long endTime = SystemClock.elapsedRealtime();
- final long delay = Math.max(0, startTime + REDRAW_PERIOD - endTime);
+ // Blink text iff the stopwatch is paused and not pressed.
+ final View touchTarget = mTime != null ? mTime : mStopwatchWrapper;
+ final Stopwatch stopwatch = getStopwatch();
+ final boolean blink = stopwatch.isPaused()
+ && startTime % 1000 < 500
+ && !touchTarget.isPressed();
- mTimeText.postDelayed(this, delay);
+ if (blink) {
+ mMainTimeText.setAlpha(0f);
+ mHundredthsTimeText.setAlpha(0f);
+ } else {
+ mMainTimeText.setAlpha(1f);
+ mHundredthsTimeText.setAlpha(1f);
}
- }
- }
- /**
- * Tapping the stopwatch text also toggles the stopwatch state, just like the fab.
- */
- private final class ToggleStopwatchRunnable implements Runnable {
- @Override
- public void run() {
- toggleStopwatchState();
+ if (!stopwatch.isReset()) {
+ final long period = stopwatch.isPaused()
+ ? REDRAW_PERIOD_PAUSED
+ : REDRAW_PERIOD_RUNNING;
+ final long endTime = Utils.now();
+ final long delay = Math.max(0, startTime + period - endTime);
+ mMainTimeText.postDelayed(this, delay);
+ }
}
}
@@ -571,10 +583,15 @@
@Override
public void stopwatchUpdated(Stopwatch before, Stopwatch after) {
if (after.isReset()) {
- mLapCount = 0;
+ // Ensure the drop shadow is hidden when the stopwatch is reset.
+ setTabScrolledToTop(true);
+ if (DataModel.getDataModel().isApplicationInForeground()) {
+ updateUI(BUTTONS_IMMEDIATE);
+ }
+ return;
}
if (DataModel.getDataModel().isApplicationInForeground()) {
- updateUI(FAB_AND_BUTTONS_MORPH);
+ updateUI(FAB_MORPH | BUTTONS_IMMEDIATE);
}
}
@@ -584,6 +601,44 @@
}
/**
+ * Toggles stopwatch state when user taps stopwatch.
+ */
+ private final class TimeClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View view) {
+ if (getStopwatch().isRunning()) {
+ DataModel.getDataModel().pauseStopwatch();
+ } else {
+ DataModel.getDataModel().startStopwatch();
+ }
+ }
+ }
+
+ /**
+ * Checks if the user is pressing inside of the stopwatch circle.
+ */
+ private final class CircleTouchListener implements View.OnTouchListener {
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ final int actionMasked = event.getActionMasked();
+ if (actionMasked != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ final float rX = view.getWidth() / 2f;
+ final float rY = (view.getHeight() - view.getPaddingBottom()) / 2f;
+ final float r = Math.min(rX, rY);
+
+ final float x = event.getX() - rX;
+ final float y = event.getY() - rY;
+
+ final boolean inCircle = Math.pow(x / r, 2.0) + Math.pow(y / r, 2.0) <= 1.0;
+
+ // Consume the event if it is outside the circle
+ return !inCircle;
+ }
+ }
+
+ /**
* Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls
* the recyclerview or when the size/position of elements within the recyclerview changes.
*/
@@ -646,9 +701,9 @@
/** The height of the gradient; sized relative to the fab height. */
private final int mGradientHeight;
- public GradientItemDecoration(Context context) {
+ GradientItemDecoration(Context context) {
mGradient.setOrientation(TOP_BOTTOM);
- updateGradientColors(UiDataModel.getUiDataModel().getWindowBackgroundColor());
+ updateGradientColors(ThemeUtils.resolveColor(context, android.R.attr.windowBackground));
final Resources resources = context.getResources();
final float fabHeight = resources.getDimensionPixelSize(R.dimen.fab_height);
@@ -672,7 +727,7 @@
*
* @param baseColor a base color to which the gradient tint should be applied
*/
- public void updateGradientColors(@ColorInt int baseColor) {
+ void updateGradientColors(@ColorInt int baseColor) {
// Compute the tinted colors that form the gradient.
for (int i = 0; i < mGradientColors.length; i++) {
mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i]);
@@ -682,4 +737,4 @@
mGradient.setColors(mGradientColors);
}
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java
index 40b062a..44113b0 100644
--- a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java
+++ b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java
@@ -78,7 +78,7 @@
super.onFinishInflate();
mLapsListView = findViewById(R.id.laps_list);
- mStopwatchView = findViewById(R.id.stopwatch);
+ mStopwatchView = findViewById(R.id.stopwatch_time_wrapper);
}
@Override
diff --git a/src/com/android/deskclock/timer/CountingTimerView.java b/src/com/android/deskclock/timer/CountingTimerView.java
deleted file mode 100644
index e276543..0000000
--- a/src/com/android/deskclock/timer/CountingTimerView.java
+++ /dev/null
@@ -1,638 +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.timer;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Typeface;
-import android.support.annotation.PluralsRes;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.accessibility.AccessibilityManager;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.Locale;
-
-/**
- * Class to measure and draw the time in the {@link com.android.deskclock.CircleTimerView}.
- * This class manages and sums the work of the four members mBigHours, mBigMinutes,
- * mBigSeconds and mMedHundredths. Those members are each tasked with measuring, sizing and
- * drawing digits (and optional label) of the time set in {@link #setTime(long, boolean)}
- */
-public class CountingTimerView extends View {
-
- private static final float TEXT_SIZE_TO_WIDTH_RATIO = 0.85f;
- // This is the ratio of the font height needed to vertically offset the font for alignment
- // from the center.
- private static final float FONT_VERTICAL_OFFSET = 0.14f;
- // Ratio of the space trailing the Hours and Minutes
- private static final float HOURS_MINUTES_SPACING = 0.4f;
- // Ratio of the space leading the Hundredths
- private static final float HUNDREDTHS_SPACING = 0.5f;
-
- /** Reusable StringBuilder to assemble talk back announcements when the time is updated. */
- private static final StringBuilder sTalkBackBuilder = new StringBuilder(50);
-
- // Radial offset of the enclosing circle
- private final float mRadiusOffset;
-
- private String mHours, mMinutes, mSeconds, mHundredths;
-
- private boolean mShowTimeStr = true;
- private final Paint mPaintBigThin = new Paint();
- private final Paint mPaintMed = new Paint();
- private final float mBigFontSize, mSmallFontSize;
- // Hours and minutes are signed for when a timer goes past the set time and thus negative
- private final SignedTime mBigHours, mBigMinutes;
- // Seconds are always shown with minutes, so are never signed
- private final UnsignedTime mBigSeconds;
- private final Hundredths mMedHundredths;
- private float mTextHeight = 0;
- private float mTotalTextWidth;
- private boolean mRemeasureText = true;
-
- private int mDefaultColor;
- private final int mPressedColor;
- private final int mWhiteColor;
- private final int mAccentColor;
- private final AccessibilityManager mAccessibilityManager;
-
- // Fields for the text serving as a virtual button.
- private boolean mVirtualButtonEnabled = false;
- private boolean mVirtualButtonPressedOn = false;
-
- // Whether or not a bounding circle exists into which the text must be made to fit.
- // If no such circle exists, the entire width of this component is available for text display.
- private boolean mShowBoundingCircle;
-
- Runnable mBlinkThread = new Runnable() {
- private boolean mVisible = true;
- @Override
- public void run() {
- mVisible = !mVisible;
- CountingTimerView.this.showTime(mVisible);
- postDelayed(mBlinkThread, 500);
- }
- };
-
- /**
- * Class to measure and draw the digit pairs of hours, minutes, seconds or hundredths. Digits
- * may have an optional label. for hours, minutes and seconds, this label trails the digits
- * and for seconds, precedes the digits.
- */
- static class UnsignedTime {
- protected Paint mPaint;
- protected float mEm;
- protected float mWidth = 0;
- private final String mWidest;
- protected final float mSpacingRatio;
- private float mLabelWidth = 0;
-
- public UnsignedTime(Paint paint, float spacingRatio, String allDigits) {
- mPaint = paint;
- mSpacingRatio = spacingRatio;
-
- if (TextUtils.isEmpty(allDigits)) {
- LogUtils.wtf("Locale digits missing - using English");
- allDigits = "0123456789";
- }
-
- float widths[] = new float[allDigits.length()];
- int ll = mPaint.getTextWidths(allDigits, widths);
- int largest = 0;
- for (int ii = 1; ii < ll; ii++) {
- if (widths[ii] > widths[largest]) {
- largest = ii;
- }
- }
-
- mEm = widths[largest];
- mWidest = allDigits.substring(largest, largest + 1);
- }
-
- public UnsignedTime(UnsignedTime unsignedTime, float spacingRatio) {
- this.mPaint = unsignedTime.mPaint;
- this.mEm = unsignedTime.mEm;
- this.mWidth = unsignedTime.mWidth;
- this.mWidest = unsignedTime.mWidest;
- this.mSpacingRatio = spacingRatio;
- }
-
- protected void updateWidth(final String time) {
- mEm = mPaint.measureText(mWidest);
- mLabelWidth = mSpacingRatio * mEm;
- mWidth = time.length() * mEm;
- }
-
- protected void resetWidth() {
- mWidth = mLabelWidth = 0;
- }
-
- public float calcTotalWidth(final String time) {
- if (time != null) {
- updateWidth(time);
- return mWidth + mLabelWidth;
- } else {
- resetWidth();
- return 0;
- }
- }
-
- public float getLabelWidth() {
- return mLabelWidth;
- }
-
- /**
- * Draws each character with a fixed spacing from time starting at ii.
- * @param canvas the canvas on which the time segment will be drawn
- * @param time time segment
- * @param ii what character to start the draw
- * @param x offset
- * @param y offset
- * @return X location for the next segment
- */
- protected float drawTime(Canvas canvas, final String time, int ii, float x, float y) {
- float textEm = mEm / 2f;
- while (ii < time.length()) {
- x += textEm;
- canvas.drawText(time.substring(ii, ii + 1), x, y, mPaint);
- x += textEm;
- ii++;
- }
- return x;
- }
-
- /**
- * Draw this time segment and append the intra-segment spacing to the x
- * @param canvas the canvas on which the time segment will be drawn
- * @param time time segment
- * @param x offset
- * @param y offset
- * @return X location for the next segment
- */
- public float draw(Canvas canvas, final String time, float x, float y) {
- return drawTime(canvas, time, 0, x, y) + getLabelWidth();
- }
- }
-
- /**
- * Special derivation to handle the hundredths painting with the label in front.
- */
- static class Hundredths extends UnsignedTime {
- public Hundredths(Paint paint, float spacingRatio, final String allDigits) {
- super(paint, spacingRatio, allDigits);
- }
-
- /**
- * Draw this time segment after prepending the intra-segment spacing to the x location.
- * {@link UnsignedTime#draw(android.graphics.Canvas, String, float, float)}
- */
- @Override
- public float draw(Canvas canvas, final String time, float x, float y) {
- return drawTime(canvas, time, 0, x + getLabelWidth(), y);
- }
- }
-
- /**
- * Special derivation to handle a negative number
- */
- static class SignedTime extends UnsignedTime {
- private float mMinusWidth = 0;
-
- public SignedTime (UnsignedTime unsignedTime, float spacingRatio) {
- super(unsignedTime, spacingRatio);
- }
-
- @Override
- protected void updateWidth(final String time) {
- super.updateWidth(time);
- if (time.contains("-")) {
- mMinusWidth = mPaint.measureText("-");
- mWidth += (mMinusWidth - mEm);
- } else {
- mMinusWidth = 0;
- }
- }
-
- @Override
- protected void resetWidth() {
- super.resetWidth();
- mMinusWidth = 0;
- }
-
- /**
- * Draws each character with a fixed spacing from time, handling the special negative
- * number case.
- * {@link UnsignedTime#draw(android.graphics.Canvas, String, float, float)}
- */
- @Override
- public float draw(Canvas canvas, final String time, float x, float y) {
- int ii = 0;
- if (mMinusWidth != 0f) {
- float minusWidth = mMinusWidth / 2;
- x += minusWidth;
- //TODO:hyphen is too thick when painted
- canvas.drawText(time.substring(0, 1), x, y, mPaint);
- x += minusWidth;
- ii++;
- }
- return drawTime(canvas, time, ii, x, y) + getLabelWidth();
- }
- }
-
- @SuppressWarnings("unused")
- public CountingTimerView(Context context) {
- this(context, null);
- }
-
- public CountingTimerView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mAccessibilityManager =
- (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
- Resources r = context.getResources();
- mDefaultColor = mWhiteColor = Color.WHITE;
- mPressedColor = mAccentColor = Utils.obtainStyledColor(
- context, R.attr.colorAccent, Color.RED);
- mBigFontSize = r.getDimension(R.dimen.big_font_size);
- mSmallFontSize = r.getDimension(R.dimen.small_font_size);
-
- mPaintBigThin.setAntiAlias(true);
- mPaintBigThin.setStyle(Paint.Style.STROKE);
- mPaintBigThin.setTextAlign(Paint.Align.CENTER);
- mPaintBigThin.setTypeface(Typeface.create("sans-serif-thin", Typeface.NORMAL));
-
- mPaintMed.setAntiAlias(true);
- mPaintMed.setStyle(Paint.Style.STROKE);
- mPaintMed.setTextAlign(Paint.Align.CENTER);
- mPaintMed.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
-
- resetTextSize();
- setTextColor(mDefaultColor);
-
- // allDigits will contain ten digits: "0123456789" in the default locale
- final String allDigits = String.format(Locale.getDefault(), "%010d", 123456789);
- mBigSeconds = new UnsignedTime(mPaintBigThin, 0.f, allDigits);
- mBigHours = new SignedTime(mBigSeconds, HOURS_MINUTES_SPACING);
- mBigMinutes = new SignedTime(mBigSeconds, HOURS_MINUTES_SPACING);
- mMedHundredths = new Hundredths(mPaintMed, HUNDREDTHS_SPACING, allDigits);
-
- mRadiusOffset = Utils.calculateRadiusOffset(r);
- }
-
- protected void resetTextSize() {
- mTextHeight = mBigFontSize;
- mPaintBigThin.setTextSize(mBigFontSize);
- mPaintMed.setTextSize(mSmallFontSize);
- }
-
- protected void setTextColor(int textColor) {
- mPaintBigThin.setColor(textColor);
- mPaintMed.setColor(textColor);
- }
-
- public void setShowBoundingCircle(boolean showBoundingCircle) {
- mShowBoundingCircle = showBoundingCircle;
- requestLayout();
- }
-
- /**
- * Update the time to display. Separates that time into the hours, minutes, seconds and
- * hundredths. If update is true, the view is invalidated so that it will draw again.
- *
- * @param time new time to display - in milliseconds
- * @param showHundredths flag to show hundredths resolution
- */
- // TODO:showHundredths S/B attribute or setter - i.e. unchanging over object life
- public void setTime(long time, boolean showHundredths) {
- final int oldLength = getDigitsLength();
- boolean neg = false, showNeg = false;
- if (time < 0) {
- time = -time;
- neg = showNeg = true;
- }
-
- int hours = (int) (time / DateUtils.HOUR_IN_MILLIS);
- int remainder = (int) (time % DateUtils.HOUR_IN_MILLIS);
-
- int minutes = (int) (remainder / DateUtils.MINUTE_IN_MILLIS);
- remainder = (int) (remainder % DateUtils.MINUTE_IN_MILLIS);
-
- int seconds = (int) (remainder / DateUtils.SECOND_IN_MILLIS);
- remainder = (int) (remainder % DateUtils.SECOND_IN_MILLIS);
-
- int hundredths = remainder / 10;
-
- if (hours > 999) {
- hours = 0;
- }
-
- // The time can be between 0 and -1 seconds, but the "truncated" equivalent time of hours
- // and minutes and seconds could be zero, so since we do not show fractions of seconds
- // when counting down, do not show the minus sign.
- // TODO:does it matter that we do not look at showHundredths?
- if (hours == 0 && minutes == 0 && seconds == 0) {
- showNeg = false;
- }
-
- // If not showing hundredths, round up to the next second.
- if (!showHundredths) {
- if (!neg && hundredths != 0) {
- seconds++;
- if (seconds == 60) {
- seconds = 0;
- minutes++;
- if (minutes == 60) {
- minutes = 0;
- hours++;
- }
- }
- }
- }
-
- // Hours may be empty.
- final UiDataModel uiDataModel = UiDataModel.getUiDataModel();
- if (hours > 0) {
- final int hoursLength = hours >= 10 ? 2 : 1;
- mHours = uiDataModel.getFormattedNumber(showNeg, hours, hoursLength);
- } else {
- mHours = null;
- }
-
- // Minutes are never empty and forced to two digits when hours exist.
- final boolean showNegMinutes = showNeg && hours == 0;
- final int minutesLength = minutes >= 10 || hours > 0 ? 2 : 1;
- mMinutes = uiDataModel.getFormattedNumber(showNegMinutes, minutes, minutesLength);
-
- // Seconds are always two digits
- mSeconds = uiDataModel.getFormattedNumber(seconds, 2);
-
- // Hundredths are optional but forced to two digits when displayed.
- if (showHundredths) {
- mHundredths = uiDataModel.getFormattedNumber(hundredths, 2);
- } else {
- mHundredths = null;
- }
-
- int newLength = getDigitsLength();
- if (oldLength != newLength) {
- if (oldLength > newLength) {
- resetTextSize();
- }
- mRemeasureText = true;
- }
-
- setContentDescription(getTimeStringForAccessibility(hours, minutes, seconds, showNeg,
- getResources()));
- postInvalidateOnAnimation();
- }
-
- private int getDigitsLength() {
- return ((mHours == null) ? 0 : mHours.length())
- + ((mMinutes == null) ? 0 : mMinutes.length())
- + ((mSeconds == null) ? 0 : mSeconds.length())
- + ((mHundredths == null) ? 0 : mHundredths.length());
- }
-
- private void calcTotalTextWidth() {
- mTotalTextWidth = mBigHours.calcTotalWidth(mHours) + mBigMinutes.calcTotalWidth(mMinutes)
- + mBigSeconds.calcTotalWidth(mSeconds)
- + mMedHundredths.calcTotalWidth(mHundredths);
- }
-
- /**
- * Adjust the size of the fonts to fit within the the circle and painted object in
- * {@link com.android.deskclock.CircleTimerView#onDraw(android.graphics.Canvas)}
- */
- private void setTotalTextWidth() {
- calcTotalTextWidth();
-
- int width;
- if (mShowBoundingCircle) {
- // A bounding circle exists, so the available width in which to fit the timer text is
- // the smaller of the width or height, which is also equal to the circle's diameter.
- width = Math.min(getWidth(), getHeight());
- } else {
- // A bounding circle does not exist, so pretend that the entire width of this component
- // is the diameter of a theoretical bounding circle.
- width = getWidth();
- }
-
- if (width != 0) {
- // Shrink 'width' to account for circle stroke and other painted objects.
- // Note on the "4 *": (1) To reduce divisions, using the diameter instead of the radius.
- // (2) The radius of the enclosing circle is reduced by mRadiusOffset and the
- // text needs to fit within a circle further reduced by mRadiusOffset.
- width -= (int) (4 * mRadiusOffset + 0.5f);
-
- final float wantDiameter2 = TEXT_SIZE_TO_WIDTH_RATIO * width * width;
- float totalDiameter2 = getHypotenuseSquared();
-
- // If the hypotenuse of the bounding box is too large, reduce all the paint text sizes
- while (totalDiameter2 > wantDiameter2) {
- // Convergence is slightly difficult due to quantization in the mTotalTextWidth
- // calculation. Reducing the ratio by 1% converges more quickly without excessive
- // loss of quality.
- float sizeRatio = 0.99f * (float) Math.sqrt(wantDiameter2/totalDiameter2);
- mPaintBigThin.setTextSize(mPaintBigThin.getTextSize() * sizeRatio);
- mPaintMed.setTextSize(mPaintMed.getTextSize() * sizeRatio);
- // Recalculate the new total text height and half-width
- mTextHeight = mPaintBigThin.getTextSize();
- calcTotalTextWidth();
- totalDiameter2 = getHypotenuseSquared();
- }
- }
- }
-
- /**
- * Calculate the square of the diameter to use in {@link CountingTimerView#setTotalTextWidth()}
- */
- private float getHypotenuseSquared() {
- return mTotalTextWidth * mTotalTextWidth + mTextHeight * mTextHeight;
- }
-
- public void blinkTimeStr(boolean blink) {
- if (blink) {
- removeCallbacks(mBlinkThread);
- post(mBlinkThread);
- } else {
- removeCallbacks(mBlinkThread);
- showTime(true);
- }
- }
-
- public void showTime(boolean visible) {
- mShowTimeStr = visible;
- invalidate();
- }
-
- public void setTimeStrTextColor(boolean active, boolean forceUpdate) {
- mDefaultColor = active ? mAccentColor : mWhiteColor;
- setTextColor(mDefaultColor);
- if (forceUpdate) {
- invalidate();
- }
- }
-
- private static String getTimeStringForAccessibility(int hours, int minutes, int seconds,
- boolean showNeg, Resources r) {
- sTalkBackBuilder.setLength(0);
- if (showNeg) {
- // This must be followed by a non-zero number or it will be audible as "hyphen"
- // instead of "minus".
- sTalkBackBuilder.append('-');
- }
- if (showNeg && hours == 0 && minutes == 0) {
- // Non-negative time will always have minutes, eg. "0 minutes 7 seconds", but negative
- // time must start with non-zero digit, eg. -0m7s will be audible as just "-7 seconds"
- sTalkBackBuilder.append(getQuantityString(r, R.plurals.Nseconds_description, seconds));
- } else if (hours == 0) {
- sTalkBackBuilder.append(getQuantityString(r, R.plurals.Nminutes_description, minutes));
- sTalkBackBuilder.append(' ');
- sTalkBackBuilder.append(getQuantityString(r, R.plurals.Nseconds_description, seconds));
- } else {
- sTalkBackBuilder.append(getQuantityString(r, R.plurals.Nhours_description, hours));
- sTalkBackBuilder.append(' ');
- sTalkBackBuilder.append(getQuantityString(r, R.plurals.Nminutes_description, minutes));
- sTalkBackBuilder.append(' ');
- sTalkBackBuilder.append(getQuantityString(r, R.plurals.Nseconds_description, seconds));
- }
- return sTalkBackBuilder.toString();
- }
-
- private static String getQuantityString(Resources r, @PluralsRes int resId, int quantity) {
- return r.getQuantityString(resId, quantity, quantity);
- }
-
- public void setVirtualButtonEnabled(boolean enabled) {
- mVirtualButtonEnabled = enabled;
- }
-
- private void virtualButtonPressed(boolean pressedOn) {
- mVirtualButtonPressedOn = pressedOn;
- invalidate();
- }
-
- private boolean withinVirtualButtonBounds(float x, float y) {
- int width = getWidth();
- int height = getHeight();
- float centerX = width / 2;
- float centerY = height / 2;
- float radius = Math.min(width, height) / 2;
-
- // Within the circle button if distance to the center is less than the radius.
- double distance = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2));
- return distance < radius;
- }
-
- public void registerVirtualButtonAction(final Runnable runnable) {
- if (!mAccessibilityManager.isEnabled()) {
- this.setOnTouchListener(new OnTouchListener() {
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- if (mVirtualButtonEnabled) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN:
- if (withinVirtualButtonBounds(event.getX(), event.getY())) {
- virtualButtonPressed(true);
- return true;
- } else {
- virtualButtonPressed(false);
- return false;
- }
- case MotionEvent.ACTION_CANCEL:
- virtualButtonPressed(false);
- return true;
- case MotionEvent.ACTION_OUTSIDE:
- virtualButtonPressed(false);
- return false;
- case MotionEvent.ACTION_UP:
- virtualButtonPressed(false);
- if (withinVirtualButtonBounds(event.getX(), event.getY())) {
- runnable.run();
- }
- return true;
- }
- }
- return false;
- }
- });
- } else {
- this.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- runnable.run();
- }
- });
- }
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- // Blink functionality.
- if (!mShowTimeStr && !mVirtualButtonPressedOn) {
- return;
- }
-
- int width = getWidth();
- if (mRemeasureText && width != 0) {
- setTotalTextWidth();
- width = getWidth();
- mRemeasureText = false;
- }
-
- int xCenter = width / 2;
- int yCenter = getHeight() / 2;
-
- float xTextStart = xCenter - mTotalTextWidth / 2;
- float yTextStart = yCenter + mTextHeight/2 - (mTextHeight * FONT_VERTICAL_OFFSET);
-
- // Text color differs based on pressed state.
- final int textColor = mVirtualButtonPressedOn ? mPressedColor : mDefaultColor;
- mPaintBigThin.setColor(textColor);
- mPaintMed.setColor(textColor);
-
- if (mHours != null) {
- xTextStart = mBigHours.draw(canvas, mHours, xTextStart, yTextStart);
- }
- if (mMinutes != null) {
- xTextStart = mBigMinutes.draw(canvas, mMinutes, xTextStart, yTextStart);
- }
- if (mSeconds != null) {
- xTextStart = mBigSeconds.draw(canvas, mSeconds, xTextStart, yTextStart);
- }
- if (mHundredths != null) {
- mMedHundredths.draw(canvas, mHundredths, xTextStart, yTextStart);
- }
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mRemeasureText = true;
- resetTextSize();
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/ExpiredTimersActivity.java b/src/com/android/deskclock/timer/ExpiredTimersActivity.java
index 3c1c48d..44acd26 100644
--- a/src/com/android/deskclock/timer/ExpiredTimersActivity.java
+++ b/src/com/android/deskclock/timer/ExpiredTimersActivity.java
@@ -19,6 +19,7 @@
import android.os.Bundle;
import android.os.SystemClock;
import android.support.annotation.NonNull;
+import android.text.TextUtils;
import android.transition.AutoTransition;
import android.transition.TransitionManager;
import android.view.Gravity;
@@ -90,7 +91,7 @@
sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
// Honor rotation on tablets; fix the orientation on phones.
- if (!getResources().getBoolean(R.bool.config_rotateAlarmAlert)) {
+ if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
}
@@ -169,6 +170,7 @@
// Hide the label hint for expired timers.
final TextView labelView = (TextView) timerItem.findViewById(R.id.timer_label);
labelView.setHint(null);
+ labelView.setVisibility(TextUtils.isEmpty(timer.getLabel()) ? View.GONE : View.VISIBLE);
// Add logic to the "Add 1 Minute" button.
final View addMinuteButton = timerItem.findViewById(R.id.reset_add);
diff --git a/src/com/android/deskclock/timer/TimerCircleView.java b/src/com/android/deskclock/timer/TimerCircleView.java
index 6756631..f605f91 100644
--- a/src/com/android/deskclock/timer/TimerCircleView.java
+++ b/src/com/android/deskclock/timer/TimerCircleView.java
@@ -26,6 +26,7 @@
import android.view.View;
import com.android.deskclock.R;
+import com.android.deskclock.ThemeUtils;
import com.android.deskclock.Utils;
import com.android.deskclock.data.Timer;
@@ -71,7 +72,7 @@
mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, 0);
mRemainderColor = Color.WHITE;
- mCompletedColor = Utils.obtainStyledColor(context, R.attr.colorAccent, Color.RED);
+ mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent);
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
diff --git a/src/com/android/deskclock/timer/TimerFragment.java b/src/com/android/deskclock/timer/TimerFragment.java
index 831a38e..1054873 100644
--- a/src/com/android/deskclock/timer/TimerFragment.java
+++ b/src/com/android/deskclock/timer/TimerFragment.java
@@ -31,17 +31,21 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
-import android.widget.ImageButton;
+import android.widget.Button;
import android.widget.ImageView;
+import com.android.deskclock.AnimatorUtils;
import com.android.deskclock.DeskClock;
import com.android.deskclock.DeskClockFragment;
import com.android.deskclock.R;
+import com.android.deskclock.Utils;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Timer;
import com.android.deskclock.data.TimerListener;
+import com.android.deskclock.data.TimerStringFormatter;
import com.android.deskclock.events.Events;
import com.android.deskclock.uidata.UiDataModel;
@@ -51,11 +55,8 @@
import static android.view.View.ALPHA;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
-import static android.view.View.SCALE_X;
+import static android.view.View.TRANSLATION_Y;
import static android.view.View.VISIBLE;
-import static com.android.deskclock.FabContainer.UpdateType.DISABLE_BUTTONS;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_SHRINK_AND_EXPAND;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_AND_BUTTONS_IMMEDIATE;
import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
/**
@@ -85,6 +86,9 @@
private Serializable mTimerSetupState;
+ /** {@code true} while this fragment is creating a new timer; {@code false} otherwise. */
+ private boolean mCreatingTimer;
+
/**
* @return an Intent that selects the timers tab with the setup screen for a new timer in place.
*/
@@ -102,7 +106,7 @@
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.timer_fragment, container, false);
- mAdapter = new TimerPagerAdapter(getChildFragmentManager());
+ mAdapter = new TimerPagerAdapter(getFragmentManager());
mViewPager = (ViewPager) view.findViewById(R.id.vertical_view_pager);
mViewPager.setAdapter(mAdapter);
mViewPager.addOnPageChangeListener(mTimerPageChangeListener);
@@ -200,7 +204,7 @@
final int index = DataModel.getDataModel().getTimers().indexOf(timer);
mViewPager.setCurrentItem(index);
- animateToView(mTimersView, null);
+ animateToView(mTimersView, null, false);
}
}
}
@@ -232,8 +236,7 @@
}
}
- @Override
- public void onUpdateFab(@NonNull ImageView fab) {
+ private void updateFab(@NonNull ImageView fab, boolean animate) {
if (mCurrentView == mTimersView) {
final Timer timer = getTimer();
if (timer == null) {
@@ -244,12 +247,27 @@
fab.setVisibility(VISIBLE);
switch (timer.getState()) {
case RUNNING:
- fab.setImageResource(R.drawable.ic_pause_white_24dp);
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_play_pause_animation);
+ } else {
+ fab.setImageResource(R.drawable.ic_play_pause);
+ }
fab.setContentDescription(fab.getResources().getString(R.string.timer_stop));
break;
case RESET:
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_stop_play_animation);
+ } else {
+ fab.setImageResource(R.drawable.ic_pause_play);
+ }
+ fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
+ break;
case PAUSED:
- fab.setImageResource(R.drawable.ic_start_white_24dp);
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_pause_play_animation);
+ } else {
+ fab.setImageResource(R.drawable.ic_pause_play);
+ }
fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
break;
case MISSED:
@@ -258,39 +276,52 @@
fab.setContentDescription(fab.getResources().getString(R.string.timer_stop));
break;
}
-
} else if (mCurrentView == mCreateTimerView) {
if (mCreateTimerView.hasValidInput()) {
fab.setImageResource(R.drawable.ic_start_white_24dp);
fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
fab.setVisibility(VISIBLE);
} else {
+ fab.setContentDescription(null);
fab.setVisibility(INVISIBLE);
}
}
}
@Override
- public void onUpdateFabButtons(@NonNull ImageButton left, @NonNull ImageButton right) {
- if (mCurrentView == mTimersView) {
- left.setEnabled(true);
- left.setImageResource(R.drawable.ic_delete);
- left.setContentDescription(left.getResources().getString(R.string.timer_delete));
- left.setVisibility(mCurrentView != mTimersView ? GONE : VISIBLE);
+ public void onUpdateFab(@NonNull ImageView fab) {
+ updateFab(fab, false);
+ }
- right.setEnabled(true);
- right.setImageResource(R.drawable.ic_add_timer);
+ @Override
+ public void onMorphFab(@NonNull ImageView fab) {
+ // Update the fab's drawable to match the current timer state.
+ updateFab(fab, Utils.isNOrLater());
+ // Animate the drawable.
+ AnimatorUtils.startDrawableAnimation(fab);
+ }
+
+ @Override
+ public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
+ if (mCurrentView == mTimersView) {
+ left.setClickable(true);
+ left.setText(R.string.timer_delete);
+ left.setContentDescription(left.getResources().getString(R.string.timer_delete));
+ left.setVisibility(VISIBLE);
+
+ right.setClickable(true);
+ right.setText(R.string.timer_add_timer);
right.setContentDescription(right.getResources().getString(R.string.timer_add_timer));
- right.setVisibility(mCurrentView != mTimersView ? GONE : VISIBLE);
+ right.setVisibility(VISIBLE);
} else if (mCurrentView == mCreateTimerView) {
- left.setEnabled(true);
- left.setImageResource(R.drawable.ic_close);
+ left.setClickable(true);
+ left.setText(R.string.timer_cancel);
left.setContentDescription(left.getResources().getString(R.string.timer_cancel));
// If no timers yet exist, the user is forced to create the first one.
left.setVisibility(hasTimers() ? VISIBLE : INVISIBLE);
- right.setVisibility(GONE);
+ right.setVisibility(INVISIBLE);
}
}
@@ -304,15 +335,26 @@
return;
}
+ final Context context = fab.getContext();
+ final long currentTime = timer.getRemainingTime();
+
switch (timer.getState()) {
case RUNNING:
DataModel.getDataModel().pauseTimer(timer);
Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock);
+ if (currentTime > 0) {
+ mTimersView.announceForAccessibility(TimerStringFormatter.formatString(
+ context, R.string.timer_accessibility_stopped, currentTime, true));
+ }
break;
case PAUSED:
case RESET:
DataModel.getDataModel().startTimer(timer);
Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock);
+ if (currentTime > 0) {
+ mTimersView.announceForAccessibility(TimerStringFormatter.formatString(
+ context, R.string.timer_accessibility_started, currentTime, true));
+ }
break;
case MISSED:
case EXPIRED:
@@ -321,29 +363,32 @@
}
} else if (mCurrentView == mCreateTimerView) {
- // Create the new timer.
- final long length = mCreateTimerView.getTimeInMillis();
- final Timer timer = DataModel.getDataModel().addTimer(length, "", false);
- Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock);
+ mCreatingTimer = true;
+ try {
+ // Create the new timer.
+ final long timerLength = mCreateTimerView.getTimeInMillis();
+ final Timer timer = DataModel.getDataModel().addTimer(timerLength, "", false);
+ Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock);
- // Start the new timer.
- DataModel.getDataModel().startTimer(timer);
- Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock);
+ // Start the new timer.
+ DataModel.getDataModel().startTimer(timer);
+ Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock);
- // Reset the state of the create view.
- mCreateTimerView.reset();
-
- // Display the freshly created timer view.
- mViewPager.setCurrentItem(0);
+ // Display the freshly created timer view.
+ mViewPager.setCurrentItem(0);
+ } finally {
+ mCreatingTimer = false;
+ }
// Return to the list of timers.
- animateToView(mTimersView, null);
+ animateToView(mTimersView, null, true);
}
}
@Override
- public void onLeftButtonClick(@NonNull ImageButton left) {
+ public void onLeftButtonClick(@NonNull Button left) {
if (mCurrentView == mTimersView) {
+ // Clicking the "delete" button.
final Timer timer = getTimer();
if (timer == null) {
return;
@@ -352,25 +397,25 @@
if (mAdapter.getCount() > 1) {
animateTimerRemove(timer);
} else {
- animateToView(mCreateTimerView, timer);
+ animateToView(mCreateTimerView, timer, false);
}
left.announceForAccessibility(getActivity().getString(R.string.timer_deleted));
} else if (mCurrentView == mCreateTimerView) {
- // Clicking the X icon on the timer creation page returns to the timers list.
+ // Clicking the "cancel" button on the timer creation page returns to the timers list.
mCreateTimerView.reset();
- animateToView(mTimersView, null);
+ animateToView(mTimersView, null, false);
left.announceForAccessibility(getActivity().getString(R.string.timer_canceled));
}
}
@Override
- public void onRightButtonClick(@NonNull ImageButton right) {
+ public void onRightButtonClick(@NonNull Button right) {
if (mCurrentView != mCreateTimerView) {
- animateToView(mCreateTimerView, null);
+ animateToView(mCreateTimerView, null, true);
}
}
@@ -463,7 +508,7 @@
/**
* Display the view that creates a new timer.
*/
- private void showCreateTimerView(UpdateType updateType) {
+ private void showCreateTimerView(int updateTypes) {
// Stop animating the timers.
stopUpdatingTime();
@@ -475,13 +520,13 @@
mCurrentView = mCreateTimerView;
// Update the fab and buttons.
- updateFab(updateType);
+ updateFab(updateTypes);
}
/**
* Display the view that lists all existing timers.
*/
- private void showTimersView(UpdateType updateType) {
+ private void showTimersView(int updateTypes) {
// Clear any defunct timer creation state; the next timer creation starts fresh.
mTimerSetupState = null;
@@ -493,7 +538,7 @@
mCurrentView = mTimersView;
// Update the fab and buttons.
- updateFab(updateType);
+ updateFab(updateTypes);
// Start animating the timers.
startUpdatingTime();
@@ -529,45 +574,105 @@
* @param toView one of {@link #mTimersView} or {@link #mCreateTimerView}
* @param timerToRemove the timer to be removed during the animation; {@code null} if no timer
* should be removed
+ * @param animateDown {@code true} if the views should animate upwards, otherwise downwards
*/
- private void animateToView(View toView, final Timer timerToRemove) {
+ private void animateToView(final View toView, final Timer timerToRemove,
+ final boolean animateDown) {
if (mCurrentView == toView) {
return;
}
final boolean toTimers = toView == mTimersView;
-
+ if (toTimers) {
+ mTimersView.setVisibility(VISIBLE);
+ } else {
+ mCreateTimerView.setVisibility(VISIBLE);
+ }
// Avoid double-taps by enabling/disabling the set of buttons active on the new view.
- updateFab(DISABLE_BUTTONS);
+ updateFab(BUTTONS_DISABLE);
- final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
- final Animator rotateFrom = ObjectAnimator.ofFloat(mCurrentView, SCALE_X, 1, 0);
- rotateFrom.setDuration(duration);
- rotateFrom.setInterpolator(new DecelerateInterpolator());
- rotateFrom.addListener(new AnimatorListenerAdapter() {
+ final long animationDuration = UiDataModel.getUiDataModel().getLongAnimationDuration();
+
+ final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver();
+ viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
- public void onAnimationEnd(Animator animation) {
- mCurrentView.setScaleX(1);
- if (toTimers) {
- showTimersView(FAB_AND_BUTTONS_SHRINK_AND_EXPAND);
- } else {
- showCreateTimerView(FAB_AND_BUTTONS_SHRINK_AND_EXPAND);
+ public boolean onPreDraw() {
+ if (viewTreeObserver.isAlive()) {
+ viewTreeObserver.removeOnPreDrawListener(this);
}
- if (timerToRemove != null) {
- DataModel.getDataModel().removeTimer(timerToRemove);
- Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
- }
+ final View view = mTimersView.findViewById(R.id.timer_time);
+ final float distanceY = view != null ? view.getHeight() + view.getY() : 0;
+ final float translationDistance = animateDown ? distanceY : -distanceY;
+
+ toView.setTranslationY(-translationDistance);
+ mCurrentView.setTranslationY(0f);
+ toView.setAlpha(0f);
+ mCurrentView.setAlpha(1f);
+
+ final Animator translateCurrent = ObjectAnimator.ofFloat(mCurrentView,
+ TRANSLATION_Y, translationDistance);
+ final Animator translateNew = ObjectAnimator.ofFloat(toView, TRANSLATION_Y, 0f);
+ final AnimatorSet translationAnimatorSet = new AnimatorSet();
+ translationAnimatorSet.playTogether(translateCurrent, translateNew);
+ translationAnimatorSet.setDuration(animationDuration);
+ translationAnimatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
+
+ final Animator fadeOutAnimator = ObjectAnimator.ofFloat(mCurrentView, ALPHA, 0f);
+ fadeOutAnimator.setDuration(animationDuration / 2);
+ fadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+
+ // The fade-out animation and fab-shrinking animation should run together.
+ updateFab(FAB_AND_BUTTONS_SHRINK);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (toTimers) {
+ showTimersView(FAB_AND_BUTTONS_EXPAND);
+
+ // Reset the state of the create view.
+ mCreateTimerView.reset();
+ } else {
+ showCreateTimerView(FAB_AND_BUTTONS_EXPAND);
+ }
+
+ if (timerToRemove != null) {
+ DataModel.getDataModel().removeTimer(timerToRemove);
+ Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
+ }
+
+ // Update the fab and button states now that the correct view is visible and
+ // before the animation to expand the fab and buttons starts.
+ updateFab(FAB_AND_BUTTONS_IMMEDIATE);
+ }
+ });
+
+ final Animator fadeInAnimator = ObjectAnimator.ofFloat(toView, ALPHA, 1f);
+ fadeInAnimator.setDuration(animationDuration / 2);
+ fadeInAnimator.setStartDelay(animationDuration / 2);
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet);
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mTimersView.setTranslationY(0f);
+ mCreateTimerView.setTranslationY(0f);
+ mTimersView.setAlpha(1f);
+ mCreateTimerView.setAlpha(1f);
+ }
+ });
+ animatorSet.start();
+
+ return true;
}
});
-
- final Animator rotateTo = ObjectAnimator.ofFloat(toView, SCALE_X, 0, 1);
- rotateTo.setDuration(duration);
- rotateTo.setInterpolator(new AccelerateInterpolator());
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.play(rotateFrom).before(rotateTo);
- animatorSet.start();
}
private boolean hasTimers() {
@@ -641,6 +746,12 @@
@Override
public void timerAdded(Timer timer) {
updatePageIndicators();
+ // If the timer is being created via this fragment avoid adjusting the fab.
+ // Timer setup view is about to be animated away in response to this timer creation.
+ // Changes to the fab immediately preceding that animation are jarring.
+ if (!mCreatingTimer) {
+ updateFab(FAB_AND_BUTTONS_IMMEDIATE);
+ }
}
@Override
@@ -658,22 +769,22 @@
mViewPager.setCurrentItem(index, true);
} else if (mCurrentView == mTimersView && index == mViewPager.getCurrentItem()) {
- // If the visible timer changed, update the fab to match its new state.
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
+ // Morph the fab from its old state to new state if necessary.
+ if (before.getState() != after.getState()
+ && !(before.isPaused() && after.isReset())) {
+ updateFab(FAB_MORPH);
+ }
}
}
@Override
public void timerRemoved(Timer timer) {
updatePageIndicators();
+ updateFab(FAB_AND_BUTTONS_IMMEDIATE);
- if (mCurrentView == mTimersView) {
- if (mAdapter.getCount() == 0) {
- animateToView(mCreateTimerView, null);
- } else {
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
- }
+ if (mCurrentView == mTimersView && mAdapter.getCount() == 0) {
+ animateToView(mCreateTimerView, null, false);
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerItem.java b/src/com/android/deskclock/timer/TimerItem.java
index 9eadadb..0d5fb79 100644
--- a/src/com/android/deskclock/timer/TimerItem.java
+++ b/src/com/android/deskclock/timer/TimerItem.java
@@ -17,29 +17,40 @@
package com.android.deskclock.timer;
import android.content.Context;
+import android.content.res.ColorStateList;
import android.os.SystemClock;
+import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
-import android.widget.ImageView;
+import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.deskclock.R;
+import com.android.deskclock.ThemeUtils;
+import com.android.deskclock.TimerTextController;
+import com.android.deskclock.Utils.ClickAccessibilityDelegate;
import com.android.deskclock.data.Timer;
+import static android.R.attr.state_activated;
+import static android.R.attr.state_pressed;
+
/**
* This view is a visual representation of a {@link Timer}.
*/
public class TimerItem extends LinearLayout {
/** Displays the remaining time or time since expiration. */
- private CountingTimerView mTimerText;
+ private TextView mTimerText;
+
+ /** Formats and displays the text in the timer. */
+ private TimerTextController mTimerTextController;
/** Displays timer progress as a color circle that changes from white to red. */
private TimerCircleView mCircleView;
/** A button that either resets the timer or adds time to it, depending on its state. */
- private ImageView mResetAddButton;
+ private Button mResetAddButton;
/** Displays the label associated with the timer. Tapping it presents an edit dialog. */
private TextView mLabelView;
@@ -59,10 +70,17 @@
protected void onFinishInflate() {
super.onFinishInflate();
mLabelView = (TextView) findViewById(R.id.timer_label);
- mResetAddButton = (ImageView) findViewById(R.id.reset_add);
+ mResetAddButton = (Button) findViewById(R.id.reset_add);
mCircleView = (TimerCircleView) findViewById(R.id.timer_time);
- mTimerText = (CountingTimerView) findViewById(R.id.timer_time_text);
- mTimerText.setShowBoundingCircle(mCircleView != null);
+ mTimerText = (TextView) findViewById(R.id.timer_time_text);
+ mTimerTextController = new TimerTextController(mTimerText);
+
+ final Context c = mTimerText.getContext();
+ final int colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent);
+ final int textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary);
+ mTimerText.setTextColor(new ColorStateList(
+ new int[][] { { -state_activated, -state_pressed }, {} },
+ new int[] { textColorPrimary, colorAccent }));
}
/**
@@ -70,7 +88,7 @@
*/
void update(Timer timer) {
// Update the time.
- mTimerText.setTime(timer.getRemainingTime(), false);
+ mTimerTextController.setTimeString(timer.getRemainingTime());
// Update the label if it changed.
final String label = timer.getLabel();
@@ -89,42 +107,50 @@
mCircleView.update(timer);
}
}
- mTimerText.showTime(!timer.isPaused() || !blinkOff);
+ if (!timer.isPaused() || !blinkOff || mTimerText.isPressed()) {
+ mTimerText.setAlpha(1f);
+ } else {
+ mTimerText.setAlpha(0f);
+ }
// Update some potentially expensive areas of the user interface only on state changes.
if (timer.getState() != mLastState) {
mLastState = timer.getState();
+ final Context context = getContext();
switch (mLastState) {
- case RESET: {
- final String resetDesc = getResources().getString(R.string.timer_reset);
- mResetAddButton.setImageResource(R.drawable.ic_reset);
- mResetAddButton.setContentDescription(resetDesc);
- mTimerText.setTimeStrTextColor(false, true);
+ case RESET:
+ case PAUSED: {
+ mResetAddButton.setText(R.string.timer_reset);
+ mResetAddButton.setContentDescription(null);
+ mTimerText.setClickable(true);
+ mTimerText.setActivated(false);
+ mTimerText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ ViewCompat.setAccessibilityDelegate(mTimerText, new ClickAccessibilityDelegate(
+ context.getString(R.string.timer_start), true));
break;
}
case RUNNING: {
- final String addTimeDesc = getResources().getString(R.string.timer_plus_one);
- mResetAddButton.setImageResource(R.drawable.ic_plusone);
+ final String addTimeDesc = context.getString(R.string.timer_plus_one);
+ mResetAddButton.setText(R.string.timer_add_minute);
mResetAddButton.setContentDescription(addTimeDesc);
- mTimerText.setTimeStrTextColor(false, true);
+ mTimerText.setClickable(true);
+ mTimerText.setActivated(false);
+ mTimerText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+ ViewCompat.setAccessibilityDelegate(mTimerText, new ClickAccessibilityDelegate(
+ context.getString(R.string.timer_pause)));
break;
}
- case PAUSED: {
- final String resetDesc = getResources().getString(R.string.timer_reset);
- mResetAddButton.setImageResource(R.drawable.ic_reset);
- mResetAddButton.setContentDescription(resetDesc);
- mTimerText.setTimeStrTextColor(false, true);
- break;
- }
- case MISSED:
- case EXPIRED: {
- final String addTimeDesc = getResources().getString(R.string.timer_plus_one);
- mResetAddButton.setImageResource(R.drawable.ic_plusone);
+ case EXPIRED:
+ case MISSED: {
+ final String addTimeDesc = context.getString(R.string.timer_plus_one);
+ mResetAddButton.setText(R.string.timer_add_minute);
mResetAddButton.setContentDescription(addTimeDesc);
- mTimerText.setTimeStrTextColor(true, true);
+ mTimerText.setClickable(false);
+ mTimerText.setActivated(true);
+ mTimerText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
break;
}
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/deskclock/timer/TimerItemFragment.java b/src/com/android/deskclock/timer/TimerItemFragment.java
index 8174997..7ce6876 100644
--- a/src/com/android/deskclock/timer/TimerItemFragment.java
+++ b/src/com/android/deskclock/timer/TimerItemFragment.java
@@ -17,7 +17,7 @@
package com.android.deskclock.timer;
import android.app.Fragment;
-import android.app.FragmentTransaction;
+import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -27,6 +27,7 @@
import com.android.deskclock.R;
import com.android.deskclock.data.DataModel;
import com.android.deskclock.data.Timer;
+import com.android.deskclock.data.TimerStringFormatter;
import com.android.deskclock.events.Events;
public class TimerItemFragment extends Fragment {
@@ -63,6 +64,7 @@
final TimerItem view = (TimerItem) inflater.inflate(R.layout.timer_item, container, false);
view.findViewById(R.id.reset_add).setOnClickListener(new ResetAddListener());
view.findViewById(R.id.timer_label).setOnClickListener(new EditLabelListener());
+ view.findViewById(R.id.timer_time_text).setOnClickListener(new TimeTextListener());
view.update(timer);
return view;
@@ -90,7 +92,7 @@
return DataModel.getDataModel().getTimer(getTimerId());
}
- private class ResetAddListener implements View.OnClickListener {
+ private final class ResetAddListener implements View.OnClickListener {
@Override
public void onClick(View v) {
final Timer timer = getTimer();
@@ -99,23 +101,36 @@
} else if (timer.isRunning() || timer.isExpired() || timer.isMissed()) {
DataModel.getDataModel().addTimerMinute(timer);
Events.sendTimerEvent(R.string.action_add_minute, R.string.label_deskclock);
+
+ final Context context = v.getContext();
+ // Must use getTimer() because old timer is no longer accurate.
+ final long currentTime = getTimer().getRemainingTime();
+ if (currentTime > 0) {
+ v.announceForAccessibility(TimerStringFormatter.formatString(
+ context, R.string.timer_accessibility_one_minute_added, currentTime,
+ true));
+ }
}
}
}
- private class EditLabelListener implements View.OnClickListener {
-
- private static final String TAG = "label_dialog";
-
+ private final class EditLabelListener implements View.OnClickListener {
@Override
public void onClick(View v) {
- final FragmentTransaction ft = getFragmentManager().beginTransaction();
- final Fragment existingFragment = getFragmentManager().findFragmentByTag(TAG);
- if (existingFragment != null) {
- ft.remove(existingFragment);
+ final LabelDialogFragment fragment = LabelDialogFragment.newInstance(getTimer());
+ LabelDialogFragment.show(getFragmentManager(), fragment);
+ }
+ }
+
+ private final class TimeTextListener implements View.OnClickListener {
+ @Override
+ public void onClick(View view) {
+ final Timer clickedTimer = getTimer();
+ if (clickedTimer.isPaused() || clickedTimer.isReset()) {
+ DataModel.getDataModel().startTimer(clickedTimer);
+ } else if (clickedTimer.isRunning()) {
+ DataModel.getDataModel().pauseTimer(clickedTimer);
}
- ft.addToBackStack(null);
- LabelDialogFragment.newInstance(getTimer()).show(ft, TAG);
}
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerKlaxon.java b/src/com/android/deskclock/timer/TimerKlaxon.java
index c2f6dc1..1c40b4a 100644
--- a/src/com/android/deskclock/timer/TimerKlaxon.java
+++ b/src/com/android/deskclock/timer/TimerKlaxon.java
@@ -27,9 +27,12 @@
import com.android.deskclock.LogUtils;
import com.android.deskclock.Utils;
import com.android.deskclock.data.DataModel;
-import com.android.deskclock.settings.SettingsActivity;
+/**
+ * Manages playing the timer ringtone and vibrating the device.
+ */
public abstract class TimerKlaxon {
+
private static final long[] VIBRATE_PATTERN = {500, 500};
private static boolean sStarted = false;
@@ -58,7 +61,8 @@
LogUtils.i("Playing silent ringtone for timer");
} else {
final Uri uri = DataModel.getDataModel().getTimerRingtoneUri();
- getAsyncRingtonePlayer(context).play(uri);
+ final long crescendoDuration = DataModel.getDataModel().getTimerCrescendoDuration();
+ getAsyncRingtonePlayer(context).play(uri, crescendoDuration);
}
if (DataModel.getDataModel().getTimerVibrate()) {
@@ -86,8 +90,7 @@
private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
if (sAsyncRingtonePlayer == null) {
- sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext(),
- SettingsActivity.KEY_TIMER_CRESCENDO);
+ sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
}
return sAsyncRingtonePlayer;
diff --git a/src/com/android/deskclock/timer/TimerSetupView.java b/src/com/android/deskclock/timer/TimerSetupView.java
index 6e79637..5dd624e 100644
--- a/src/com/android/deskclock/timer/TimerSetupView.java
+++ b/src/com/android/deskclock/timer/TimerSetupView.java
@@ -17,45 +17,50 @@
package com.android.deskclock.timer;
import android.content.Context;
+import android.content.res.ColorStateList;
import android.content.res.Resources;
-import android.graphics.Color;
-import android.support.v4.content.ContextCompat;
+import android.graphics.PorterDuff;
+import android.support.annotation.IdRes;
+import android.support.v4.view.ViewCompat;
+import android.text.BidiFormatter;
+import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
-import android.widget.Button;
-import android.widget.ImageButton;
import android.widget.LinearLayout;
+import android.widget.TextView;
import com.android.deskclock.FabContainer;
+import com.android.deskclock.FormattedTextUtils;
import com.android.deskclock.R;
-import com.android.deskclock.Utils;
+import com.android.deskclock.ThemeUtils;
import com.android.deskclock.uidata.UiDataModel;
import java.io.Serializable;
import java.util.Arrays;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_ONLY_SHRINK_AND_EXPAND;
-import static com.android.deskclock.FabContainer.UpdateType.FAB_REQUESTS_FOCUS;
+import static com.android.deskclock.FabContainer.FAB_REQUEST_FOCUS;
+import static com.android.deskclock.FabContainer.FAB_SHRINK_AND_EXPAND;
-public class TimerSetupView extends LinearLayout implements Button.OnClickListener,
- Button.OnLongClickListener {
+public class TimerSetupView extends LinearLayout implements View.OnClickListener,
+ View.OnLongClickListener {
- private final Button[] mNumbers = new Button[10];
- private final int[] mInput = {0, 0, 0, 0, 0, 0};
+ private final int[] mInput = { 0, 0, 0, 0, 0, 0 };
+
private int mInputPointer = -1;
- private ImageButton mDelete;
- private TimerView mEnteredTime;
- private View mDivider;
+ private CharSequence mTimeTemplate;
+
+ private TextView mTimeView;
+ private View mDeleteView;
+ private View mDividerView;
+ private TextView[] mDigitViews;
/** Updates to the fab are requested via this container. */
private FabContainer mFabContainer;
- private final int mColorAccent;
- private final int mColorHairline;
-
public TimerSetupView(Context context) {
this(context, null /* attrs */);
}
@@ -63,173 +68,247 @@
public TimerSetupView(Context context, AttributeSet attrs) {
super(context, attrs);
- mColorAccent = Utils.obtainStyledColor(context, R.attr.colorAccent, Color.RED);
- mColorHairline = ContextCompat.getColor(context, R.color.hairline);
+ final BidiFormatter bf = BidiFormatter.getInstance(false /* rtlContext */);
+ final String hoursLabel = bf.unicodeWrap(context.getString(R.string.hours_label));
+ final String minutesLabel = bf.unicodeWrap(context.getString(R.string.minutes_label));
+ final String secondsLabel = bf.unicodeWrap(context.getString(R.string.seconds_label));
- LayoutInflater.from(context).inflate(R.layout.time_setup_container, this);
- }
+ // Create a formatted template for "00h 00m 00s".
+ mTimeTemplate = TextUtils.expandTemplate("^1^4 ^2^5 ^3^6",
+ bf.unicodeWrap("^1"),
+ bf.unicodeWrap("^2"),
+ bf.unicodeWrap("^3"),
+ FormattedTextUtils.formatText(hoursLabel, new RelativeSizeSpan(0.5f)),
+ FormattedTextUtils.formatText(minutesLabel, new RelativeSizeSpan(0.5f)),
+ FormattedTextUtils.formatText(secondsLabel, new RelativeSizeSpan(0.5f)));
- void setFabContainer(FabContainer fabContainer) {
- mFabContainer = fabContainer;
+ LayoutInflater.from(context).inflate(R.layout.timer_setup_container, this);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- final View v1 = findViewById(R.id.first);
- final View v2 = findViewById(R.id.second);
- final View v3 = findViewById(R.id.third);
- final View v4 = findViewById(R.id.fourth);
+ mTimeView = (TextView) findViewById(R.id.timer_setup_time);
+ mDeleteView = findViewById(R.id.timer_setup_delete);
+ mDividerView = findViewById(R.id.timer_setup_divider);
+ mDigitViews = new TextView[] {
+ (TextView) findViewById(R.id.timer_setup_digit_0),
+ (TextView) findViewById(R.id.timer_setup_digit_1),
+ (TextView) findViewById(R.id.timer_setup_digit_2),
+ (TextView) findViewById(R.id.timer_setup_digit_3),
+ (TextView) findViewById(R.id.timer_setup_digit_4),
+ (TextView) findViewById(R.id.timer_setup_digit_5),
+ (TextView) findViewById(R.id.timer_setup_digit_6),
+ (TextView) findViewById(R.id.timer_setup_digit_7),
+ (TextView) findViewById(R.id.timer_setup_digit_8),
+ (TextView) findViewById(R.id.timer_setup_digit_9),
+ };
- mDivider = findViewById(R.id.divider);
- mDelete = (ImageButton) findViewById(R.id.delete);
- mDelete.setOnClickListener(this);
- mDelete.setOnLongClickListener(this);
- mEnteredTime = (TimerView) findViewById(R.id.timer_time_text);
+ // Tint the divider to match the disabled control color by default and used the activated
+ // control color when there is valid input.
+ final Context dividerContext = mDividerView.getContext();
+ final int colorControlActivated = ThemeUtils.resolveColor(dividerContext,
+ R.attr.colorControlActivated);
+ final int colorControlDisabled = ThemeUtils.resolveColor(dividerContext,
+ R.attr.colorControlNormal, new int[] { ~android.R.attr.state_enabled });
+ ViewCompat.setBackgroundTintList(mDividerView, new ColorStateList(
+ new int[][] { { android.R.attr.state_activated }, {} },
+ new int[] { colorControlActivated, colorControlDisabled }));
+ ViewCompat.setBackgroundTintMode(mDividerView, PorterDuff.Mode.SRC);
- mNumbers[1] = (Button) v1.findViewById(R.id.key_left);
- mNumbers[2] = (Button) v1.findViewById(R.id.key_middle);
- mNumbers[3] = (Button) v1.findViewById(R.id.key_right);
-
- mNumbers[4] = (Button) v2.findViewById(R.id.key_left);
- mNumbers[5] = (Button) v2.findViewById(R.id.key_middle);
- mNumbers[6] = (Button) v2.findViewById(R.id.key_right);
-
- mNumbers[7] = (Button) v3.findViewById(R.id.key_left);
- mNumbers[8] = (Button) v3.findViewById(R.id.key_middle);
- mNumbers[9] = (Button) v3.findViewById(R.id.key_right);
-
- mNumbers[0] = (Button) v4.findViewById(R.id.key_middle);
- v4.findViewById(R.id.key_left).setVisibility(INVISIBLE);
- v4.findViewById(R.id.key_right).setVisibility(INVISIBLE);
-
- final UiDataModel uiDataModel = UiDataModel.getUiDataModel();
- for (int i = 0; i < mNumbers.length; i++) {
- mNumbers[i].setOnClickListener(this);
- mNumbers[i].setText(uiDataModel.getFormattedNumber(i, 1));
- mNumbers[i].setTextColor(Color.WHITE);
- mNumbers[i].setTag(R.id.numbers_key, i);
+ // Initialize the digit buttons.
+ final UiDataModel uidm = UiDataModel.getUiDataModel();
+ for (final TextView digitView : mDigitViews) {
+ final int digit = getDigitForId(digitView.getId());
+ digitView.setText(uidm.getFormattedNumber(digit, 1));
+ digitView.setOnClickListener(this);
}
+ mDeleteView.setOnClickListener(this);
+ mDeleteView.setOnLongClickListener(this);
+
updateTime();
- updateDeleteButtonAndDivider();
+ updateDeleteAndDivider();
}
- private boolean clickButton(View button) {
- button.performClick();
- mFabContainer.updateFab(FAB_REQUESTS_FOCUS);
- return true;
+ public void setFabContainer(FabContainer fabContainer) {
+ mFabContainer = fabContainer;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_0:
- return clickButton(mNumbers[0]);
- case KeyEvent.KEYCODE_1:
- return clickButton(mNumbers[1]);
- case KeyEvent.KEYCODE_2:
- return clickButton(mNumbers[2]);
- case KeyEvent.KEYCODE_3:
- return clickButton(mNumbers[3]);
- case KeyEvent.KEYCODE_4:
- return clickButton(mNumbers[4]);
- case KeyEvent.KEYCODE_5:
- return clickButton(mNumbers[5]);
- case KeyEvent.KEYCODE_6:
- return clickButton(mNumbers[6]);
- case KeyEvent.KEYCODE_7:
- return clickButton(mNumbers[7]);
- case KeyEvent.KEYCODE_8:
- return clickButton(mNumbers[8]);
- case KeyEvent.KEYCODE_9:
- return clickButton(mNumbers[9]);
- case KeyEvent.KEYCODE_DEL:
- return clickButton(mDelete);
- default:
- return false;
+ View view = null;
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ view = mDeleteView;
+ } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
+ view = mDigitViews[keyCode - KeyEvent.KEYCODE_0];
+ }
+
+ if (view != null) {
+ final boolean result = view.performClick();
+ if (result && hasValidInput()) {
+ mFabContainer.updateFab(FAB_REQUEST_FOCUS);
+ }
+ return result;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == mDeleteView) {
+ delete();
+ } else {
+ append(getDigitForId(view.getId()));
}
}
@Override
- public void onClick(View v) {
- final boolean validInputBeforeClick = hasValidInput();
- final Integer n = (Integer) v.getTag(R.id.numbers_key);
- // A number was pressed
- if (n != null) {
- // pressing "0" as the first digit does nothing
- if (mInputPointer == -1 && n == 0) {
- return;
- }
-
- // No space for more digits, so ignore input.
- if (mInputPointer == mInput.length - 1) {
- return;
- }
-
- // Append the new digit.
- System.arraycopy(mInput, 0, mInput, 1, mInputPointer + 1);
- mInput[0] = n;
- mInputPointer++;
- updateTime();
-
- // Update talkback to read the number being deleted
- final Resources resources = getResources();
- final String cd = resources.getString(R.string.timer_descriptive_delete, n.toString());
- mDelete.setContentDescription(cd);
- }
-
- // other keys
- if (v == mDelete) {
- if (mInputPointer < 0) {
- // Nothing exists to delete so return.
- return;
- }
-
- System.arraycopy(mInput, 1, mInput, 0, mInputPointer);
- mInput[mInputPointer] = 0;
- mInputPointer--;
- updateTime();
-
- // Update talkback to read the number being deleted or its original description.
- final String number = mInputPointer < 0 ? "" : Integer.toString(mInput[0]);
- final String cd = getResources().getString(R.string.timer_descriptive_delete, number);
- mDelete.setContentDescription(cd);
- }
-
- if (validInputBeforeClick != hasValidInput()) {
- updateFab();
- updateDeleteButtonAndDivider();
- }
- }
-
- @Override
- public boolean onLongClick(View v) {
- if (v == mDelete) {
+ public boolean onLongClick(View view) {
+ if (view == mDeleteView) {
reset();
+ updateFab();
return true;
}
return false;
}
+ private int getDigitForId(@IdRes int id) {
+ switch (id) {
+ case R.id.timer_setup_digit_0:
+ return 0;
+ case R.id.timer_setup_digit_1:
+ return 1;
+ case R.id.timer_setup_digit_2:
+ return 2;
+ case R.id.timer_setup_digit_3:
+ return 3;
+ case R.id.timer_setup_digit_4:
+ return 4;
+ case R.id.timer_setup_digit_5:
+ return 5;
+ case R.id.timer_setup_digit_6:
+ return 6;
+ case R.id.timer_setup_digit_7:
+ return 7;
+ case R.id.timer_setup_digit_8:
+ return 8;
+ case R.id.timer_setup_digit_9:
+ return 9;
+ }
+ throw new IllegalArgumentException("Invalid id: " + id);
+ }
+
+ private void updateTime() {
+ final int seconds = mInput[1] * 10 + mInput[0];
+ final int minutes = mInput[3] * 10 + mInput[2];
+ final int hours = mInput[5] * 10 + mInput[4];
+
+ final UiDataModel uidm = UiDataModel.getUiDataModel();
+ mTimeView.setText(TextUtils.expandTemplate(mTimeTemplate,
+ uidm.getFormattedNumber(hours, 2),
+ uidm.getFormattedNumber(minutes, 2),
+ uidm.getFormattedNumber(seconds, 2)));
+
+ final Resources r = getResources();
+ mTimeView.setContentDescription(r.getString(R.string.timer_setup_description,
+ r.getQuantityString(R.plurals.hours, hours, hours),
+ r.getQuantityString(R.plurals.minutes, minutes, minutes),
+ r.getQuantityString(R.plurals.seconds, seconds, seconds)));
+ }
+
+ private void updateDeleteAndDivider() {
+ final boolean enabled = hasValidInput();
+ mDeleteView.setEnabled(enabled);
+ mDividerView.setActivated(enabled);
+ }
+
+ private void updateFab() {
+ mFabContainer.updateFab(FAB_SHRINK_AND_EXPAND);
+ }
+
+ private void append(int digit) {
+ if (digit < 0 || digit > 9) {
+ throw new IllegalArgumentException("Invalid digit: " + digit);
+ }
+
+ // Pressing "0" as the first digit does nothing.
+ if (mInputPointer == -1 && digit == 0) {
+ return;
+ }
+
+ // No space for more digits, so ignore input.
+ if (mInputPointer == mInput.length - 1) {
+ return;
+ }
+
+ // Append the new digit.
+ System.arraycopy(mInput, 0, mInput, 1, mInputPointer + 1);
+ mInput[0] = digit;
+ mInputPointer++;
+ updateTime();
+
+ // Update TalkBack to read the number being deleted.
+ mDeleteView.setContentDescription(getContext().getString(
+ R.string.timer_descriptive_delete,
+ UiDataModel.getUiDataModel().getFormattedNumber(digit)));
+
+ // Update the fab, delete, and divider when we have valid input.
+ if (mInputPointer == 0) {
+ updateFab();
+ updateDeleteAndDivider();
+ }
+ }
+
+ private void delete() {
+ // Nothing exists to delete so return.
+ if (mInputPointer < 0) {
+ return;
+ }
+
+ System.arraycopy(mInput, 1, mInput, 0, mInputPointer);
+ mInput[mInputPointer] = 0;
+ mInputPointer--;
+ updateTime();
+
+ // Update TalkBack to read the number being deleted or its original description.
+ if (mInputPointer >= 0) {
+ mDeleteView.setContentDescription(getContext().getString(
+ R.string.timer_descriptive_delete,
+ UiDataModel.getUiDataModel().getFormattedNumber(mInput[0])));
+ } else {
+ mDeleteView.setContentDescription(getContext().getString(R.string.timer_delete));
+ }
+
+ // Update the fab, delete, and divider when we no longer have valid input.
+ if (mInputPointer == -1) {
+ updateFab();
+ updateDeleteAndDivider();
+ }
+ }
+
public void reset() {
if (mInputPointer != -1) {
Arrays.fill(mInput, 0);
mInputPointer = -1;
- updateFab();
updateTime();
- updateDeleteButtonAndDivider();
+ updateDeleteAndDivider();
}
}
- public long getTimeInMillis() {
- final int hoursInSeconds = mInput[5] * 36000 + mInput[4] * 3600;
- final int minutesInSeconds = mInput[3] * 600 + mInput[2] * 60;
- final int seconds = mInput[1] * 10 + mInput[0];
- final int totalSeconds = hoursInSeconds + minutesInSeconds + seconds;
+ public boolean hasValidInput() {
+ return mInputPointer != -1;
+ }
- return totalSeconds * DateUtils.SECOND_IN_MILLIS;
+ public long getTimeInMillis() {
+ final int seconds = mInput[1] * 10 + mInput[0];
+ final int minutes = mInput[3] * 10 + mInput[2];
+ final int hours = mInput[5] * 10 + mInput[4];
+ return seconds * DateUtils.SECOND_IN_MILLIS
+ + minutes * DateUtils.MINUTE_IN_MILLIS
+ + hours * DateUtils.HOUR_IN_MILLIS;
}
/**
@@ -252,42 +331,7 @@
}
}
updateTime();
- updateDeleteButtonAndDivider();
+ updateDeleteAndDivider();
}
}
-
- protected boolean hasValidInput() {
- return mInputPointer != -1;
- }
-
- private void updateTime() {
- final int seconds = mInput[1] * 10 + mInput[0];
- final int minutes = mInput[3] * 10 + mInput[2];
- final int hours = mInput[5] * 10 + mInput[4];
- mEnteredTime.setTime(mInput[5], mInput[4], mInput[3], mInput[2], seconds);
- mEnteredTime.setContentDescription(createContentDescription(hours, minutes, seconds));
- }
-
- private void updateDeleteButtonAndDivider() {
- final boolean enabled = hasValidInput();
- mDelete.setEnabled(enabled);
- mDivider.setBackgroundColor(enabled ? mColorAccent : mColorHairline);
- }
-
- private void updateFab() {
- mFabContainer.updateFab(FAB_ONLY_SHRINK_AND_EXPAND);
- }
-
- private CharSequence createContentDescription(int hours, int minutes, int seconds) {
- final Resources r = getResources();
- final String hoursFormatted = r.getQuantityString(
- R.plurals.Nhours_description, hours, hours);
- final String minutesFormatted = r.getQuantityString(
- R.plurals.Nminutes_description, minutes, minutes);
- final String secondsFormatted = r.getQuantityString(
- R.plurals.Nseconds_description, seconds, seconds);
- return (hours == 0 ? "" : hoursFormatted + ", ")
- + (minutes == 0 ? "" : minutesFormatted + ", ")
- + secondsFormatted;
- }
}
diff --git a/src/com/android/deskclock/timer/TimerView.java b/src/com/android/deskclock/timer/TimerView.java
deleted file mode 100644
index e95ebbe..0000000
--- a/src/com/android/deskclock/timer/TimerView.java
+++ /dev/null
@@ -1,126 +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.timer;
-
-import android.content.Context;
-import android.graphics.Paint;
-import android.support.v4.content.ContextCompat;
-import android.util.AttributeSet;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.deskclock.R;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.Locale;
-
-public class TimerView extends LinearLayout {
-
- private TextView mHoursTens, mHoursOnes;
- private TextView mMinutesTens, mMinutesOnes;
- private TextView mSeconds;
- private final int mWhiteColor, mGrayColor;
-
- @SuppressWarnings("unused")
- public TimerView(Context context) {
- this(context, null);
- }
-
- public TimerView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mWhiteColor = ContextCompat.getColor(context, R.color.clock_white);
- mGrayColor = ContextCompat.getColor(context, R.color.clock_gray);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- mHoursTens = (TextView) findViewById(R.id.hours_tens);
- mHoursOnes = (TextView) findViewById(R.id.hours_ones);
-
- mMinutesTens = (TextView) findViewById(R.id.minutes_tens);
- addStartPadding(mMinutesTens);
- mMinutesOnes = (TextView) findViewById(R.id.minutes_ones);
-
- mSeconds = (TextView) findViewById(R.id.seconds);
- addStartPadding(mSeconds);
- }
-
- /**
- * Measure the text and add a start padding to the view
- * @param textView view to measure and onb to which add start padding
- */
- private void addStartPadding(TextView textView) {
- final float gapPadding = 0.45f;
- // allDigits will contain ten digits: "0123456789" in the default locale
- String allDigits = String.format(Locale.getDefault(), "%010d", 123456789);
- Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
- paint.setTextSize(textView.getTextSize());
- paint.setTypeface(textView.getTypeface());
-
- float widths[] = new float[allDigits.length()];
- int ll = paint.getTextWidths(allDigits, widths);
- int largest = 0;
- for (int ii = 1; ii < ll; ii++) {
- if (widths[ii] > widths[largest]) {
- largest = ii;
- }
- }
- // Add left padding to the view - Note: layout inherits LTR
- textView.setPadding((int) (gapPadding * widths[largest]), 0, 0, 0);
- }
-
- public void setTime(int hoursTensDigit, int hoursOnesDigit, int minutesTensDigit,
- int minutesOnesDigit, int seconds) {
- final UiDataModel uiDataModel = UiDataModel.getUiDataModel();
- if (hoursTensDigit == -1) {
- mHoursTens.setText("-");
- mHoursTens.setTextColor(mGrayColor);
- } else {
- mHoursTens.setText(uiDataModel.getFormattedNumber(hoursTensDigit, 1));
- mHoursTens.setTextColor(mWhiteColor);
- }
-
- if (hoursOnesDigit == -1) {
- mHoursOnes.setText("-");
- mHoursOnes.setTextColor(mGrayColor);
- } else {
- mHoursOnes.setText(uiDataModel.getFormattedNumber(hoursOnesDigit, 1));
- mHoursOnes.setTextColor(mWhiteColor);
- }
-
- if (minutesTensDigit == -1) {
- mMinutesTens.setText("-");
- mMinutesTens.setTextColor(mGrayColor);
- } else {
- mMinutesTens.setText(uiDataModel.getFormattedNumber(minutesTensDigit, 1));
- mMinutesTens.setTextColor(mWhiteColor);
- }
-
- if (minutesOnesDigit == -1) {
- mMinutesOnes.setText("-");
- mMinutesOnes.setTextColor(mGrayColor);
- } else {
- mMinutesOnes.setText(uiDataModel.getFormattedNumber(minutesOnesDigit, 1));
- mMinutesOnes.setTextColor(mWhiteColor);
- }
-
- mSeconds.setText(uiDataModel.getFormattedNumber(seconds, 2));
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/ColorModel.java b/src/com/android/deskclock/uidata/ColorModel.java
deleted file mode 100644
index 07f3b49..0000000
--- a/src/com/android/deskclock/uidata/ColorModel.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright (C) 2016 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.uidata;
-
-import android.support.annotation.ColorInt;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-import static java.util.Calendar.HOUR_OF_DAY;
-
-/**
- * All globally available color data is accessed via this model.
- */
-final class ColorModel {
-
- /** The colors of the app window - it changes throughout out the day to mimic the sky. */
- private static final @ColorInt int[] BACKGROUND_SPECTRUM = {
- 0xFF212121 /* 12 AM */,
- 0xFF20222A /* 1 AM */,
- 0xFF202233 /* 2 AM */,
- 0xFF1F2242 /* 3 AM */,
- 0xFF1E224F /* 4 AM */,
- 0xFF1D225C /* 5 AM */,
- 0xFF1B236B /* 6 AM */,
- 0xFF1A237E /* 7 AM */,
- 0xFF1D2783 /* 8 AM */,
- 0xFF232E8B /* 9 AM */,
- 0xFF283593 /* 10 AM */,
- 0xFF2C3998 /* 11 AM */,
- 0xFF303F9F /* 12 PM */,
- 0xFF2C3998 /* 1 PM */,
- 0xFF283593 /* 2 PM */,
- 0xFF232E8B /* 3 PM */,
- 0xFF1D2783 /* 4 PM */,
- 0xFF1A237E /* 5 PM */,
- 0xFF1B236B /* 6 PM */,
- 0xFF1D225C /* 7 PM */,
- 0xFF1E224F /* 8 PM */,
- 0xFF1F2242 /* 9 PM */,
- 0xFF202233 /* 10 PM */,
- 0xFF20222A /* 11 PM */
- };
-
- /** The listeners to notify when colors change. */
- private final List<OnAppColorChangeListener> mListeners = new ArrayList<>();
-
- /** The current app window color. */
- private @ColorInt int mWindowBackgroundColor;
-
- ColorModel(PeriodicCallbackModel periodicCallbackModel) {
- // Update the application window color when the hour changes.
- periodicCallbackModel.addHourCallback(new HourlyAppColorChanger(), 100);
-
- // Initialize the app window color.
- updateAppColor();
- }
-
- /**
- * @param colorListener to be notified when colors change
- */
- void addOnAppColorChangeListener(OnAppColorChangeListener colorListener) {
- mListeners.add(colorListener);
- }
-
- /**
- * @param colorListener to be notified when colors change
- */
- void removeOnAppColorChangeListener(OnAppColorChangeListener colorListener) {
- mListeners.remove(colorListener);
- }
-
- /**
- * @return the color of the app window
- */
- @ColorInt int getAppColor() {
- return mWindowBackgroundColor;
- }
-
- private void updateAppColor() {
- final @ColorInt int color = BACKGROUND_SPECTRUM[Calendar.getInstance().get(HOUR_OF_DAY)];
- if (mWindowBackgroundColor != color) {
- final @ColorInt int oldColor = mWindowBackgroundColor;
- mWindowBackgroundColor = color;
-
- for (OnAppColorChangeListener listener : mListeners) {
- listener.onAppColorChange(oldColor, color);
- }
- }
- }
-
- /**
- * Updates the app window color each time the hour changes.
- */
- private final class HourlyAppColorChanger implements Runnable {
- @Override
- public void run() {
- updateAppColor();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/FormattedStringModel.java b/src/com/android/deskclock/uidata/FormattedStringModel.java
new file mode 100644
index 0000000..e630d74
--- /dev/null
+++ b/src/com/android/deskclock/uidata/FormattedStringModel.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2016 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.uidata;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.Map;
+
+import static java.util.Calendar.JULY;
+
+/**
+ * All formatted strings that are cached for performance are accessed via this model.
+ */
+final class FormattedStringModel {
+
+ /** Clears data structures containing data that is locale-sensitive. */
+ @SuppressWarnings("FieldCanBeLocal")
+ private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
+
+ /**
+ * Caches formatted numbers in the current locale padded with zeroes to requested lengths.
+ * The first level of the cache maps length to the second level of the cache.
+ * The second level of the cache maps an integer to a formatted String in the current locale.
+ */
+ private final SparseArray<SparseArray<String>> mNumberFormatCache = new SparseArray<>(3);
+
+ /** Single-character version of weekday names; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S' */
+ private Map<Integer, String> mShortWeekdayNames;
+
+ /** Full weekday names; e.g.: 'Sunday', 'Monday', 'Tuesday', etc. */
+ private Map<Integer, String> mLongWeekdayNames;
+
+ FormattedStringModel(Context context) {
+ // Clear caches affected by locale when locale changes.
+ final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+ context.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @return the {@code value} formatted as a String in the current locale
+ * @throws IllegalArgumentException if {@code value} is negative
+ */
+ String getFormattedNumber(int value) {
+ final int length = value == 0 ? 1 : ((int) Math.log10(value) + 1);
+ return getFormattedNumber(false, value, length);
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length
+ * @return the {@code value} formatted as a String in the current locale and padded to the
+ * requested {@code length}
+ * @throws IllegalArgumentException if {@code value} is negative
+ */
+ String getFormattedNumber(int value, int length) {
+ return getFormattedNumber(false, value, length);
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param negative force a minus sign (-) onto the display, even if {@code value} is {@code 0}
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length. If
+ * {@code negative} is {@code true} the return value will contain a minus sign and a total
+ * length of {@code length + 1}.
+ * @return the {@code value} formatted as a String in the current locale and padded to the
+ * requested {@code length}
+ * @throws IllegalArgumentException if {@code value} is negative
+ */
+ String getFormattedNumber(boolean negative, int value, int length) {
+ if (value < 0) {
+ throw new IllegalArgumentException("value may not be negative: " + value);
+ }
+
+ // Look up the value cache using the length; -ve and +ve values are cached separately.
+ final int lengthCacheKey = negative ? -length : length;
+ SparseArray<String> valueCache = mNumberFormatCache.get(lengthCacheKey);
+ if (valueCache == null) {
+ valueCache = new SparseArray<>((int) Math.pow(10, length));
+ mNumberFormatCache.put(lengthCacheKey, valueCache);
+ }
+
+ // Look up the cached formatted value using the value.
+ String formatted = valueCache.get(value);
+ if (formatted == null) {
+ final String sign = negative ? "−" : "";
+ formatted = String.format(Locale.getDefault(), sign + "%0" + length + "d", value);
+ valueCache.put(value, formatted);
+ }
+
+ return formatted;
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ * <ul>
+ * <li>{@link Calendar#SUNDAY}</li>
+ * <li>{@link Calendar#MONDAY}</li>
+ * <li>{@link Calendar#TUESDAY}</li>
+ * <li>{@link Calendar#WEDNESDAY}</li>
+ * <li>{@link Calendar#THURSDAY}</li>
+ * <li>{@link Calendar#FRIDAY}</li>
+ * <li>{@link Calendar#SATURDAY}</li>
+ * </ul>
+ * @return single-character weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
+ */
+ String getShortWeekday(int calendarDay) {
+ if (mShortWeekdayNames == null) {
+ mShortWeekdayNames = new ArrayMap<>(7);
+
+ final SimpleDateFormat format = new SimpleDateFormat("ccccc", Locale.getDefault());
+ for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
+ final Calendar calendar = new GregorianCalendar(2014, JULY, 20 + i - 1);
+ final String weekday = format.format(calendar.getTime());
+ mShortWeekdayNames.put(i, weekday);
+ }
+ }
+
+ return mShortWeekdayNames.get(calendarDay);
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ * <ul>
+ * <li>{@link Calendar#SUNDAY}</li>
+ * <li>{@link Calendar#MONDAY}</li>
+ * <li>{@link Calendar#TUESDAY}</li>
+ * <li>{@link Calendar#WEDNESDAY}</li>
+ * <li>{@link Calendar#THURSDAY}</li>
+ * <li>{@link Calendar#FRIDAY}</li>
+ * <li>{@link Calendar#SATURDAY}</li>
+ * </ul>
+ * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
+ */
+ String getLongWeekday(int calendarDay) {
+ if (mLongWeekdayNames == null) {
+ mLongWeekdayNames = new ArrayMap<>(7);
+
+ final Calendar calendar = new GregorianCalendar(2014, JULY, 20);
+ final SimpleDateFormat format = new SimpleDateFormat("EEEE", Locale.getDefault());
+ for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
+ final String weekday = format.format(calendar.getTime());
+ mLongWeekdayNames.put(i, weekday);
+ calendar.add(Calendar.DAY_OF_YEAR, 1);
+ }
+ }
+
+ return mLongWeekdayNames.get(calendarDay);
+ }
+
+ /**
+ * Cached information that is locale-sensitive must be cleared in response to locale changes.
+ */
+ private final class LocaleChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mNumberFormatCache.clear();
+ mShortWeekdayNames = null;
+ mLongWeekdayNames = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabDAO.java b/src/com/android/deskclock/uidata/TabDAO.java
index 429dbea..3be5d19 100644
--- a/src/com/android/deskclock/uidata/TabDAO.java
+++ b/src/com/android/deskclock/uidata/TabDAO.java
@@ -16,11 +16,7 @@
package com.android.deskclock.uidata;
-import android.content.Context;
import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-
-import com.android.deskclock.Utils;
import static com.android.deskclock.uidata.UiDataModel.Tab;
@@ -29,34 +25,23 @@
*/
final class TabDAO {
+ /** Key to a preference that stores the ordinal of the selected tab. */
private static final String KEY_SELECTED_TAB = "selected_tab";
- // Lazily instantiated and cached for the life of the application.
- private static SharedPreferences sPrefs;
-
private TabDAO() {}
/**
* @return an enumerated value indicating the currently selected primary tab
*/
- static Tab getSelectedTab(Context context) {
- final SharedPreferences prefs = getSharedPreferences(context);
- final int selectedTabOrdinal = prefs.getInt(KEY_SELECTED_TAB, Tab.CLOCKS.ordinal());
- return Tab.values()[selectedTabOrdinal];
+ static Tab getSelectedTab(SharedPreferences prefs) {
+ final int ordinal = prefs.getInt(KEY_SELECTED_TAB, Tab.CLOCKS.ordinal());
+ return Tab.values()[ordinal];
}
/**
* @param tab an enumerated value indicating the newly selected primary tab
*/
- static void setSelectedTab(Context context, Tab tab) {
- getSharedPreferences(context).edit().putInt(KEY_SELECTED_TAB, tab.ordinal()).apply();
+ static void setSelectedTab(SharedPreferences prefs, Tab tab) {
+ prefs.edit().putInt(KEY_SELECTED_TAB, tab.ordinal()).apply();
}
-
- private static SharedPreferences getSharedPreferences(Context context) {
- if (sPrefs == null) {
- sPrefs = Utils.getDefaultSharedPreferences(context.getApplicationContext());
- }
-
- return sPrefs;
- }
-}
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabModel.java b/src/com/android/deskclock/uidata/TabModel.java
index 48874f3..5b878ed 100644
--- a/src/com/android/deskclock/uidata/TabModel.java
+++ b/src/com/android/deskclock/uidata/TabModel.java
@@ -16,7 +16,7 @@
package com.android.deskclock.uidata;
-import android.content.Context;
+import android.content.SharedPreferences;
import android.text.TextUtils;
import java.util.ArrayList;
@@ -32,7 +32,7 @@
*/
final class TabModel {
- private final Context mContext;
+ private final SharedPreferences mPrefs;
/** The listeners to notify when the selected tab is changed. */
private final List<TabListener> mTabListeners = new ArrayList<>();
@@ -46,8 +46,8 @@
/** An enumerated value indicating the currently selected tab. */
private Tab mSelectedTab;
- TabModel(Context context) {
- mContext = context;
+ TabModel(SharedPreferences prefs) {
+ mPrefs = prefs;
Arrays.fill(mTabScrolledToTop, true);
}
@@ -77,25 +77,25 @@
}
/**
- * @param index the index of the tab
- * @return the tab at the given {@code index}
+ * @param ordinal the ordinal (left-to-right index) of the tab
+ * @return the tab at the given {@code ordinal}
*/
- Tab getTab(int index) {
- return Tab.values()[index];
+ Tab getTab(int ordinal) {
+ return Tab.values()[ordinal];
}
/**
- * @return the index of the currently selected primary tab
+ * @param position the position of the tab in the user interface
+ * @return the tab at the given {@code ordinal}
*/
- int getSelectedTabIndex() {
- return getSelectedTab().ordinal();
- }
-
- /**
- * @param index the index of the tab to select
- */
- void setSelectedTabIndex(int index) {
- setSelectedTab(Tab.values()[index]);
+ Tab getTabAt(int position) {
+ final int ordinal;
+ if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == LAYOUT_DIRECTION_RTL) {
+ ordinal = getTabCount() - position - 1;
+ } else {
+ ordinal = position;
+ }
+ return getTab(ordinal);
}
/**
@@ -103,7 +103,7 @@
*/
Tab getSelectedTab() {
if (mSelectedTab == null) {
- mSelectedTab = TabDAO.getSelectedTab(mContext);
+ mSelectedTab = TabDAO.getSelectedTab(mPrefs);
}
return mSelectedTab;
}
@@ -115,7 +115,7 @@
final Tab oldSelectedTab = getSelectedTab();
if (oldSelectedTab != tab) {
mSelectedTab = tab;
- TabDAO.setSelectedTab(mContext, tab);
+ TabDAO.setSelectedTab(mPrefs, tab);
// Notify of the tab change.
for (TabListener tl : mTabListeners) {
@@ -132,17 +132,6 @@
}
}
- /**
- * @param ltrTabIndex the tab index assuming left-to-right layout direction
- * @return the tab index in the current layout direction
- */
- int getTabLayoutIndex(int ltrTabIndex) {
- if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == LAYOUT_DIRECTION_RTL) {
- return getTabCount() - ltrTabIndex - 1;
- }
- return ltrTabIndex;
- }
-
//
// Tab scrolling
//
diff --git a/src/com/android/deskclock/uidata/UiDataModel.java b/src/com/android/deskclock/uidata/UiDataModel.java
index a3c09fd..4ff7e20 100644
--- a/src/com/android/deskclock/uidata/UiDataModel.java
+++ b/src/com/android/deskclock/uidata/UiDataModel.java
@@ -16,15 +16,11 @@
package com.android.deskclock.uidata;
-import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
+import android.content.SharedPreferences;
import android.graphics.Typeface;
-import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
-import android.util.SparseArray;
import com.android.deskclock.AlarmClockFragment;
import com.android.deskclock.ClockFragment;
@@ -32,7 +28,7 @@
import com.android.deskclock.stopwatch.StopwatchFragment;
import com.android.deskclock.timer.TimerFragment;
-import java.util.Locale;
+import java.util.Calendar;
import static com.android.deskclock.Utils.enforceMainLooper;
@@ -49,18 +45,18 @@
STOPWATCH(StopwatchFragment.class, R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);
private final String mFragmentClassName;
- private final @DrawableRes int mIconId;
- private final @StringRes int mContentDescriptionId;
+ private final @DrawableRes int mIconResId;
+ private final @StringRes int mLabelResId;
- Tab(Class fragmentClass, @DrawableRes int iconId, @StringRes int contentDescriptionId) {
+ Tab(Class fragmentClass, @DrawableRes int iconResId, @StringRes int labelResId) {
mFragmentClassName = fragmentClass.getName();
- mIconId = iconId;
- mContentDescriptionId = contentDescriptionId;
+ mIconResId = iconResId;
+ mLabelResId = labelResId;
}
public String getFragmentClassName() { return mFragmentClassName; }
- public int getIconId() { return mIconId; }
- public int getContentDescriptionId() { return mContentDescriptionId; }
+ public @DrawableRes int getIconResId() { return mIconResId; }
+ public @StringRes int getLabelResId() { return mLabelResId; }
}
/** The single instance of this data model that exists for the life of the application. */
@@ -70,24 +66,13 @@
return sUiDataModel;
}
- /** Clears data structures containing data that is locale-sensitive. */
- @SuppressWarnings("FieldCanBeLocal")
- private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
- /**
- * Caches formatted numbers in the current locale padded with zeroes to requested lengths.
- * The first level of the cache maps length to the second level of the cache.
- * The second level of the cache maps an integer to a formatted String in the current locale.
- */
- private final SparseArray<SparseArray<String>> mNumberFormatCache = new SparseArray<>(3);
-
private Context mContext;
/** The model from which tab data are fetched. */
private TabModel mTabModel;
- /** The model from which colors are fetched. */
- private ColorModel mColorModel;
+ /** The model from which formatted strings are fetched. */
+ private FormattedStringModel mFormattedStringModel;
/** The model from which timed callbacks originate. */
private PeriodicCallbackModel mPeriodicCallbackModel;
@@ -97,19 +82,14 @@
/**
* The context may be set precisely once during the application life.
*/
- public void setContext(Context context) {
- if (mContext != null) {
- throw new IllegalStateException("context has already been set");
+ public void init(Context context, SharedPreferences prefs) {
+ if (mContext != context) {
+ mContext = context.getApplicationContext();
+
+ mPeriodicCallbackModel = new PeriodicCallbackModel(mContext);
+ mFormattedStringModel = new FormattedStringModel(mContext);
+ mTabModel = new TabModel(prefs);
}
- mContext = context.getApplicationContext();
-
- mPeriodicCallbackModel = new PeriodicCallbackModel(mContext);
- mColorModel = new ColorModel(mPeriodicCallbackModel);
- mTabModel = new TabModel(mContext);
-
- // Clear caches affected by locale when locale changes.
- final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
- mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
}
/**
@@ -122,7 +102,7 @@
}
//
- // Number Formatting
+ // Formatted Strings
//
/**
@@ -135,8 +115,8 @@
* @throws IllegalArgumentException if {@code value} is negative
*/
public String getFormattedNumber(int value) {
- final int length = (int) Math.log10(value);
- return getFormattedNumber(false, value, length == 0 ? 1 : length);
+ enforceMainLooper();
+ return mFormattedStringModel.getFormattedNumber(value);
}
/**
@@ -151,7 +131,8 @@
* @throws IllegalArgumentException if {@code value} is negative
*/
public String getFormattedNumber(int value, int length) {
- return getFormattedNumber(false, value, length);
+ enforceMainLooper();
+ return mFormattedStringModel.getFormattedNumber(value, length);
}
/**
@@ -169,55 +150,44 @@
* @throws IllegalArgumentException if {@code value} is negative
*/
public String getFormattedNumber(boolean negative, int value, int length) {
- if (value < 0) {
- throw new IllegalArgumentException("value may not be negative: " + value);
- }
-
- // Look up the value cache using the length; -ve and +ve values are cached separately.
- final int lengthCacheKey = negative ? -length : length;
- SparseArray<String> valueCache = mNumberFormatCache.get(lengthCacheKey);
- if (valueCache == null) {
- valueCache = new SparseArray<>((int) Math.pow(10, length));
- mNumberFormatCache.put(lengthCacheKey, valueCache);
- }
-
- // Look up the cached formatted value using the value.
- String formatted = valueCache.get(value);
- if (formatted == null) {
- final String sign = negative ? "−" : "";
- formatted = String.format(Locale.getDefault(), sign + "%0" + length + "d", value);
- valueCache.put(value, formatted);
- }
-
- return formatted;
- }
-
- //
- // Colors
- //
-
- /**
- * @param colorListener to be notified when the app's color changes
- */
- public void addOnAppColorChangeListener(OnAppColorChangeListener colorListener) {
enforceMainLooper();
- mColorModel.addOnAppColorChangeListener(colorListener);
+ return mFormattedStringModel.getFormattedNumber(negative, value, length);
}
/**
- * @param colorListener to be notified when the app's color changes
+ * @param calendarDay any of the following values
+ * <ul>
+ * <li>{@link Calendar#SUNDAY}</li>
+ * <li>{@link Calendar#MONDAY}</li>
+ * <li>{@link Calendar#TUESDAY}</li>
+ * <li>{@link Calendar#WEDNESDAY}</li>
+ * <li>{@link Calendar#THURSDAY}</li>
+ * <li>{@link Calendar#FRIDAY}</li>
+ * <li>{@link Calendar#SATURDAY}</li>
+ * </ul>
+ * @return single-character version of weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
*/
- public void removeOnAppColorChangeListener(OnAppColorChangeListener colorListener) {
+ public String getShortWeekday(int calendarDay) {
enforceMainLooper();
- mColorModel.removeOnAppColorChangeListener(colorListener);
+ return mFormattedStringModel.getShortWeekday(calendarDay);
}
/**
- * @return the color of the application window background
+ * @param calendarDay any of the following values
+ * <ul>
+ * <li>{@link Calendar#SUNDAY}</li>
+ * <li>{@link Calendar#MONDAY}</li>
+ * <li>{@link Calendar#TUESDAY}</li>
+ * <li>{@link Calendar#WEDNESDAY}</li>
+ * <li>{@link Calendar#THURSDAY}</li>
+ * <li>{@link Calendar#FRIDAY}</li>
+ * <li>{@link Calendar#SATURDAY}</li>
+ * </ul>
+ * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
*/
- public @ColorInt int getWindowBackgroundColor() {
+ public String getLongWeekday(int calendarDay) {
enforceMainLooper();
- return mColorModel.getAppColor();
+ return mFormattedStringModel.getLongWeekday(calendarDay);
}
//
@@ -232,6 +202,14 @@
return mContext.getResources().getInteger(android.R.integer.config_shortAnimTime);
}
+ /**
+ * @return the duration in milliseconds of long animations
+ */
+ public long getLongAnimationDuration() {
+ enforceMainLooper();
+ return mContext.getResources().getInteger(android.R.integer.config_longAnimTime);
+ }
+
//
// Tabs
//
@@ -261,28 +239,21 @@
}
/**
- * @param index the index of the tab
- * @return the tab at the given {@code index}
+ * @param ordinal the ordinal of the tab
+ * @return the tab at the given {@code ordinal}
*/
- public Tab getTab(int index) {
+ public Tab getTab(int ordinal) {
enforceMainLooper();
- return mTabModel.getTab(index);
+ return mTabModel.getTab(ordinal);
}
/**
- * @return the index of the currently selected primary tab
+ * @param position the position of the tab in the user interface
+ * @return the tab at the given {@code ordinal}
*/
- public int getSelectedTabIndex() {
+ public Tab getTabAt(int position) {
enforceMainLooper();
- return mTabModel.getSelectedTabIndex();
- }
-
- /**
- * @param index the index of the tab to select
- */
- public void setSelectedTabIndex(int index) {
- enforceMainLooper();
- mTabModel.setSelectedTabIndex(index);
+ return mTabModel.getTabAt(position);
}
/**
@@ -336,19 +307,6 @@
return mTabModel.isTabScrolledToTop(getSelectedTab());
}
- /**
- * This method converts the given {@code ltrTabIndex} which assumes Left-To-Right layout of the
- * tabs into an index that respects the system layout, which may be Left-To-Right or
- * Right-To-Left.
- *
- * @param ltrTabIndex the tab index assuming left-to-right layout direction
- * @return the tab index in the current layout direction
- */
- public int getTabLayoutIndex(int ltrTabIndex) {
- enforceMainLooper();
- return mTabModel.getTabLayoutIndex(ltrTabIndex);
- }
-
//
// Shortcut Ids
//
@@ -412,14 +370,4 @@
enforceMainLooper();
mPeriodicCallbackModel.removePeriodicCallback(runnable);
}
-
- /**
- * Cached information that is locale-sensitive must be cleared in response to locale changes.
- */
- private final class LocaleChangedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- mNumberFormatCache.clear();
- }
- }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/deskclock/widget/AutoSizingTextClock.java b/src/com/android/deskclock/widget/AutoSizingTextClock.java
new file mode 100644
index 0000000..1a70da9
--- /dev/null
+++ b/src/com/android/deskclock/widget/AutoSizingTextClock.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TextClock;
+
+/**
+ * Wrapper around TextClock that automatically re-sizes itself to fit within the given bounds.
+ */
+public class AutoSizingTextClock extends TextClock {
+
+ private final TextSizeHelper mTextSizeHelper;
+ private boolean mSuppressLayout = false;
+
+ public AutoSizingTextClock(Context context) {
+ this(context, null);
+ }
+
+ public AutoSizingTextClock(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AutoSizingTextClock(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mTextSizeHelper = new TextSizeHelper(this);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mTextSizeHelper.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+ if (mTextSizeHelper != null) {
+ if (lengthBefore != lengthAfter) {
+ mSuppressLayout = false;
+ }
+ mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter);
+ } else {
+ requestLayout();
+ }
+ }
+
+ @Override
+ public void setText(CharSequence text, BufferType type) {
+ mSuppressLayout = true;
+ super.setText(text, type);
+ mSuppressLayout = false;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (mTextSizeHelper == null || !mTextSizeHelper.shouldIgnoreRequestLayout()) {
+ if (!mSuppressLayout) {
+ super.requestLayout();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/AutoSizingTextView.java b/src/com/android/deskclock/widget/AutoSizingTextView.java
new file mode 100644
index 0000000..cc3303a
--- /dev/null
+++ b/src/com/android/deskclock/widget/AutoSizingTextView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 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.widget;
+
+import android.content.Context;
+import android.support.v7.widget.AppCompatTextView;
+import android.util.AttributeSet;
+
+/**
+ * A TextView which automatically re-sizes its text to fit within its boundaries.
+ */
+public class AutoSizingTextView extends AppCompatTextView {
+
+ private final TextSizeHelper mTextSizeHelper;
+
+ public AutoSizingTextView(Context context) {
+ this(context, null);
+ }
+
+ public AutoSizingTextView(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.textViewStyle);
+ }
+
+ public AutoSizingTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mTextSizeHelper = new TextSizeHelper(this);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mTextSizeHelper.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+ if (mTextSizeHelper != null) {
+ mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter);
+ } else {
+ requestLayout();
+ }
+ }
+
+ @Override
+ public void requestLayout() {
+ if (mTextSizeHelper == null || !mTextSizeHelper.shouldIgnoreRequestLayout()) {
+ super.requestLayout();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/CircleView.java b/src/com/android/deskclock/widget/CircleView.java
index 8606a27..b3a4675 100644
--- a/src/com/android/deskclock/widget/CircleView.java
+++ b/src/com/android/deskclock/widget/CircleView.java
@@ -131,7 +131,7 @@
@Override
public boolean hasOverlappingRendering() {
// only if we have a background, which we shouldn't...
- return getBackground() != null && getBackground().getCurrent() != null;
+ return getBackground() != null;
}
/**
@@ -188,13 +188,6 @@
}
/**
- * @return the x-coordinate of the center of the circle
- */
- public final float getCenterX() {
- return mCenterX;
- }
-
- /**
* Sets the x-coordinate for the center of the circle and invalidates only the affected area.
*
* @param centerX the x-coordinate to use, relative to the view's bounds
@@ -218,13 +211,6 @@
}
/**
- * @return the y-coordinate of the center of the circle
- */
- public final float getCenterY() {
- return mCenterY;
- }
-
- /**
* Sets the y-coordinate for the center of the circle and invalidates only the affected area.
*
* @param centerY the y-coordinate to use, relative to the view's bounds
diff --git a/src/com/android/deskclock/widget/RtlViewPager.java b/src/com/android/deskclock/widget/RtlViewPager.java
deleted file mode 100644
index 5bfee69..0000000
--- a/src/com/android/deskclock/widget/RtlViewPager.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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.widget;
-
-import android.content.Context;
-import android.support.v4.view.ViewPager;
-import android.util.AttributeSet;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.uidata.UiDataModel;
-
-/**
- * A {@link ViewPager} that's aware of RTL changes when used with FragmentPagerAdapter.
- */
-public final class RtlViewPager extends ViewPager {
-
- /**
- * Callback interface for responding to changing state of the selected page.
- * Positions supplied will always be the logical position in the adapter -
- * that is, the 0 index corresponds to the left-most page in LTR and the
- * right-most page in RTL.
- */
- private OnPageChangeListener mListener;
-
- public RtlViewPager(Context context) {
- this(context, null /* attrs */);
- }
-
- public RtlViewPager(Context context, AttributeSet attrs) {
- super(context, attrs);
- addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
- @Override
- public void onPageScrolled(int position, float offset, int offsetPixels) {
- if (mListener != null) {
- position = UiDataModel.getUiDataModel().getTabLayoutIndex(position);
- mListener.onPageScrolled(position, offset, offsetPixels);
- }
- }
-
- @Override
- public void onPageSelected(int position) {
- if (mListener != null) {
- position = UiDataModel.getUiDataModel().getTabLayoutIndex(position);
- mListener.onPageSelected(position);
- }
- }
-
- @Override
- public void onPageScrollStateChanged(int state) {
- if (mListener != null) {
- mListener.onPageScrollStateChanged(state);
- }
- }
- });
- }
-
- @Override
- public int getCurrentItem() {
- return UiDataModel.getUiDataModel().getTabLayoutIndex(super.getCurrentItem());
- }
-
- @Override
- public void setCurrentItem(int item) {
- // Smooth-scroll to the new tab if the app is open; snap to the new tab if it is not.
- final boolean smoothScrolling = DataModel.getDataModel().isApplicationInForeground();
- setCurrentItem(item, smoothScrolling);
- }
-
- @Override
- public void setCurrentItem(int item, boolean smoothScroll) {
- // Convert the item (which assumes LTR) into the correct index relative to layout direction.
- final int index = UiDataModel.getUiDataModel().getTabLayoutIndex(item);
- super.setCurrentItem(index, smoothScroll);
- }
-
- @Override
- @SuppressWarnings("deprecation")
- public void setOnPageChangeListener(OnPageChangeListener unused) {
- throw new UnsupportedOperationException("Use setOnRTLPageChangeListener instead");
- }
-
- /**
- * Sets a {@link OnPageChangeListener}. The listener will be called when a page is selected.
- */
- public void setOnRTLPageChangeListener(OnPageChangeListener listener) {
- mListener = listener;
- }
-}
diff --git a/src/com/android/deskclock/widget/TextSizeHelper.java b/src/com/android/deskclock/widget/TextSizeHelper.java
new file mode 100644
index 0000000..bf37f76
--- /dev/null
+++ b/src/com/android/deskclock/widget/TextSizeHelper.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 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.widget;
+
+import android.text.Layout;
+import android.text.TextPaint;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import static java.lang.Integer.MAX_VALUE;
+
+/**
+ * A TextView which automatically re-sizes its text to fit within its boundaries.
+ */
+public final class TextSizeHelper {
+
+ // The text view whose size this class controls.
+ private final TextView mTextView;
+
+ // Text paint used for measuring.
+ private final TextPaint mMeasurePaint = new TextPaint();
+
+ // The maximum size the text is allowed to be (in pixels).
+ private float mMaxTextSize;
+
+ // The maximum width the text is allowed to be (in pixels).
+ private int mWidthConstraint = MAX_VALUE;
+
+ // The maximum height the text is allowed to be (in pixels).
+ private int mHeightConstraint = MAX_VALUE;
+
+ // When {@code true} calls to {@link #requestLayout()} should be ignored.
+ private boolean mIgnoreRequestLayout;
+
+ public TextSizeHelper(TextView view) {
+ mTextView = view;
+ mMaxTextSize = view.getTextSize();
+ }
+
+ public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthConstraint = MAX_VALUE;
+ if (View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.UNSPECIFIED) {
+ widthConstraint = View.MeasureSpec.getSize(widthMeasureSpec)
+ - mTextView.getCompoundPaddingLeft() - mTextView.getCompoundPaddingRight();
+ }
+
+ int heightConstraint = MAX_VALUE;
+ if (View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.UNSPECIFIED) {
+ heightConstraint = View.MeasureSpec.getSize(heightMeasureSpec)
+ - mTextView.getCompoundPaddingTop() - mTextView.getCompoundPaddingBottom();
+ }
+
+ if (mTextView.isLayoutRequested() || mWidthConstraint != widthConstraint
+ || mHeightConstraint != heightConstraint) {
+ mWidthConstraint = widthConstraint;
+ mHeightConstraint = heightConstraint;
+
+ adjustTextSize();
+ }
+ }
+
+ public void onTextChanged(int lengthBefore, int lengthAfter) {
+ // The length of the text has changed, request layout to recalculate the current text
+ // size. This is necessary to workaround an optimization in TextView#checkForRelayout()
+ // which will avoid re-layout when the view has a fixed layout width.
+ if (lengthBefore != lengthAfter) {
+ mTextView.requestLayout();
+ }
+ }
+
+ public boolean shouldIgnoreRequestLayout() {
+ return mIgnoreRequestLayout;
+ }
+
+ private void adjustTextSize() {
+ final CharSequence text = mTextView.getText();
+ float textSize = mMaxTextSize;
+ if (text.length() > 0 && (mWidthConstraint < MAX_VALUE || mHeightConstraint < MAX_VALUE)) {
+ mMeasurePaint.set(mTextView.getPaint());
+
+ float minTextSize = 1f;
+ float maxTextSize = mMaxTextSize;
+ while (maxTextSize >= minTextSize) {
+ final float midTextSize = Math.round((maxTextSize + minTextSize) / 2f);
+ mMeasurePaint.setTextSize(midTextSize);
+
+ final float width = Layout.getDesiredWidth(text, mMeasurePaint);
+ final float height = mMeasurePaint.getFontMetricsInt(null);
+ if (width > mWidthConstraint || height > mHeightConstraint) {
+ maxTextSize = midTextSize - 1f;
+ } else {
+ textSize = midTextSize;
+ minTextSize = midTextSize + 1f;
+ }
+ }
+ }
+
+ if (mTextView.getTextSize() != textSize) {
+ mIgnoreRequestLayout = true;
+ mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+ mIgnoreRequestLayout = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextTime.java b/src/com/android/deskclock/widget/TextTime.java
index 744f5d6..e07c40f 100644
--- a/src/com/android/deskclock/widget/TextTime.java
+++ b/src/com/android/deskclock/widget/TextTime.java
@@ -22,13 +22,19 @@
import android.net.Uri;
import android.os.Handler;
import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.widget.TextView;
import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
import java.util.Calendar;
+import java.util.TimeZone;
+
+import static java.util.Calendar.HOUR_OF_DAY;
+import static java.util.Calendar.MINUTE;
/**
* Based on {@link android.widget.TextClock}, This widget displays a constant time of day using
@@ -36,8 +42,13 @@
*/
public class TextTime extends TextView {
- private static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
- private static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
+ /** UTC does not have DST rules and will not alter the {@link #mHour} and {@link #mMinute}. */
+ private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
private CharSequence mFormat12;
private CharSequence mFormat24;
@@ -75,8 +86,8 @@
public TextTime(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
- setFormat12Hour(Utils.get12ModeFormat(0.22f /* amPmRatio */));
- setFormat24Hour(Utils.get24ModeFormat());
+ setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */, false));
+ setFormat24Hour(Utils.get24ModeFormat(false));
chooseFormat();
}
@@ -108,7 +119,7 @@
}
private void chooseFormat() {
- final boolean format24Requested = DateFormat.is24HourFormat(getContext());
+ final boolean format24Requested = DataModel.getDataModel().is24HourFormat();
if (format24Requested) {
mFormat = mFormat24 == null ? DEFAULT_FORMAT_24_HOUR : mFormat24;
} else {
@@ -152,9 +163,11 @@
}
private void updateTime() {
- final Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, mHour);
- calendar.set(Calendar.MINUTE, mMinute);
+ // Format the time relative to UTC to ensure hour and minute are not adjusted for DST.
+ final Calendar calendar = DataModel.getDataModel().getCalendar();
+ calendar.setTimeZone(UTC);
+ calendar.set(HOUR_OF_DAY, mHour);
+ calendar.set(MINUTE, mMinute);
final CharSequence text = DateFormat.format(mFormat, calendar);
setText(text);
// Strip away the spans from text so talkback is not confused
diff --git a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java
index 267a8e5..c781e46 100644
--- a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java
+++ b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java
@@ -16,6 +16,8 @@
package com.android.deskclock.widget.selector;
import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -23,6 +25,8 @@
import android.widget.TextView;
import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Weekdays;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.widget.TextTime;
@@ -36,7 +40,8 @@
}
@Override
- public View getView(int position, View convertView, ViewGroup parent) {
+ public @NonNull View getView(int position, @Nullable View convertView,
+ @NonNull ViewGroup parent) {
final Context context = getContext();
View row = convertView;
if (row == null) {
@@ -60,7 +65,8 @@
context.getResources().getString(R.string.alarm_tomorrow) :
context.getResources().getString(R.string.alarm_today);
} else {
- daysOfWeek = alarm.daysOfWeek.toString(context, 0);
+ final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
+ daysOfWeek = alarm.daysOfWeek.toString(context, weekdayOrder);
}
final TextView daysOfWeekView = (TextView) row.findViewById(R.id.daysOfWeek);
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.java b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
index 24dc5ba..7229113 100644
--- a/src/com/android/deskclock/worldclock/CitySelectionActivity.java
+++ b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
@@ -1,17 +1,17 @@
/*
* Copyright (C) 2016 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
+ * 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
+ * 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.
+ * 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.worldclock;
@@ -39,10 +39,10 @@
import com.android.deskclock.DropShadowController;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
-import com.android.deskclock.actionbarmenu.OptionsMenuManager;
import com.android.deskclock.actionbarmenu.MenuItemController;
import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
+import com.android.deskclock.actionbarmenu.OptionsMenuManager;
import com.android.deskclock.actionbarmenu.SearchMenuItemController;
import com.android.deskclock.actionbarmenu.SettingsMenuItemController;
import com.android.deskclock.data.City;
@@ -62,32 +62,42 @@
/**
* This activity allows the user to alter the cities selected for display.
- *
+ * <p/>
* Note, it is possible for two instances of this Activity to exist simultaneously:
- *
+ * <p/>
* <ul>
- * <li>Clock Tab-> Tap Floating Action Button</li>
- * <li>Digital Widget -> Tap any city clock</li>
+ * <li>Clock Tab-> Tap Floating Action Button</li>
+ * <li>Digital Widget -> Tap any city clock</li>
* </ul>
- *
+ * <p/>
* As a result, {@link #onResume()} conservatively refreshes itself from the backing
* {@link DataModel} which may have changed since this activity was last displayed.
*/
public final class CitySelectionActivity extends BaseActivity {
- /** The list of all selected and unselected cities, indexed and possibly filtered. */
+ /**
+ * The list of all selected and unselected cities, indexed and possibly filtered.
+ */
private ListView mCitiesList;
- /** The adapter that presents all of the selected and unselected cities. */
+ /**
+ * The adapter that presents all of the selected and unselected cities.
+ */
private CityAdapter mCitiesAdapter;
- /** Manages all action bar menu display and click handling. */
+ /**
+ * Manages all action bar menu display and click handling.
+ */
private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
- /** Menu item controller for search view. */
+ /**
+ * Menu item controller for search view.
+ */
private SearchMenuItemController mSearchMenuItemController;
- /** The controller that shows the drop shadow when content is not scrolled to the top. */
+ /**
+ * The controller that shows the drop shadow when content is not scrolled to the top.
+ */
private DropShadowController mDropShadowController;
@Override
@@ -98,18 +108,18 @@
mSearchMenuItemController =
new SearchMenuItemController(getSupportActionBar().getThemedContext(),
new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- return false;
- }
+ @Override
+ public boolean onQueryTextSubmit(String query) {
+ return false;
+ }
- @Override
- public boolean onQueryTextChange(String query) {
- mCitiesAdapter.filter(query);
- updateFastScrolling();
- return true;
- }
- }, savedInstanceState);
+ @Override
+ public boolean onQueryTextChange(String query) {
+ mCitiesAdapter.filter(query);
+ updateFastScrolling();
+ return true;
+ }
+ }, savedInstanceState);
mCitiesAdapter = new CityAdapter(this, mSearchMenuItemController);
mOptionsMenuManager.addMenuItemController(new NavUpMenuItemController(this))
.addMenuItemController(mSearchMenuItemController)
@@ -118,7 +128,6 @@
.addMenuItemController(MenuItemControllerFactory.getInstance()
.buildMenuItemControllers(this));
mCitiesList = (ListView) findViewById(R.id.cities_list);
- mCitiesList.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
mCitiesList.setAdapter(mCitiesAdapter);
updateFastScrolling();
@@ -180,7 +189,7 @@
/**
* This adapter presents data in 2 possible modes. If selected cities exist the format is:
- *
+ * <p/>
* <pre>
* Selected Cities
* City 1 (alphabetically first)
@@ -193,9 +202,9 @@
* City B2 (alphabetically second starting with B)
* ...
* </pre>
- *
+ * <p/>
* If selected cities do not exist, that section is removed and all that remains is:
- *
+ * <p/>
* <pre>
* A City A1 (alphabetically first starting with A)
* City A2 (alphabetically second starting with A)
@@ -208,44 +217,68 @@
private static final class CityAdapter extends BaseAdapter implements View.OnClickListener,
CompoundButton.OnCheckedChangeListener, SectionIndexer {
- /** The type of the single optional "Selected Cities" header entry. */
+ /**
+ * The type of the single optional "Selected Cities" header entry.
+ */
private static final int VIEW_TYPE_SELECTED_CITIES_HEADER = 0;
- /** The type of each city entry. */
+ /**
+ * The type of each city entry.
+ */
private static final int VIEW_TYPE_CITY = 1;
private final Context mContext;
private final LayoutInflater mInflater;
- /** The 12-hour time pattern for the current locale. */
+ /**
+ * The 12-hour time pattern for the current locale.
+ */
private final String mPattern12;
- /** The 24-hour time pattern for the current locale. */
+ /**
+ * The 24-hour time pattern for the current locale.
+ */
private final String mPattern24;
- /** {@code true} time should honor {@link #mPattern24}; {@link #mPattern12} otherwise. */
+ /**
+ * {@code true} time should honor {@link #mPattern24}; {@link #mPattern12} otherwise.
+ */
private boolean mIs24HoursMode;
- /** A calendar used to format time in a particular timezone. */
+ /**
+ * A calendar used to format time in a particular timezone.
+ */
private final Calendar mCalendar;
- /** The list of cities which may be filtered by a search term. */
+ /**
+ * The list of cities which may be filtered by a search term.
+ */
private List<City> mFilteredCities = Collections.emptyList();
- /** A mutable set of cities currently selected by the user. */
+ /**
+ * A mutable set of cities currently selected by the user.
+ */
private final Set<City> mUserSelectedCities = new ArraySet<>();
- /** The number of user selections at the top of the adapter to avoid indexing. */
+ /**
+ * The number of user selections at the top of the adapter to avoid indexing.
+ */
private int mOriginalUserSelectionCount;
- /** The precomputed section headers. */
+ /**
+ * The precomputed section headers.
+ */
private String[] mSectionHeaders;
- /** The corresponding location of each precomputed section header. */
+ /**
+ * The corresponding location of each precomputed section header.
+ */
private Integer[] mSectionHeaderPositions;
- /** Menu item controller for search. Search query is maintained here. */
+ /**
+ * Menu item controller for search. Search query is maintained here.
+ */
private final SearchMenuItemController mSearchMenuItemController;
public CityAdapter(Context context, SearchMenuItemController searchMenuItemController) {
@@ -308,6 +341,9 @@
case VIEW_TYPE_CITY:
final City city = getItem(position);
+ if (city == null) {
+ throw new IllegalStateException("The desired city does not exist");
+ }
final TimeZone timeZone = city.getTimeZone();
// Inflate a new view if necessary.
@@ -402,6 +438,9 @@
// Add a section if this position should show the section index.
if (getShowIndex(position)) {
final City city = getItem(position);
+ if (city == null) {
+ throw new IllegalStateException("The desired city does not exist");
+ }
switch (getCitySort()) {
case NAME:
sections.add(city.getIndexString());
@@ -500,8 +539,13 @@
return !TextUtils.isEmpty(mSearchMenuItemController.getQueryText().trim());
}
- private Collection<City> getSelectedCities() { return mUserSelectedCities; }
- private boolean hasHeader() { return !isFiltering() && mOriginalUserSelectionCount > 0; }
+ private Collection<City> getSelectedCities() {
+ return mUserSelectedCities;
+ }
+
+ private boolean hasHeader() {
+ return !isFiltering() && mOriginalUserSelectionCount > 0;
+ }
private DataModel.CitySort getCitySort() {
return DataModel.getDataModel().getCitySort();