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();