Snap for 8564071 from 26cd97d2262987fdd26b3481c44aee740956e364 to mainline-os-statsd-release

Change-Id: I6bb6c6edd2f7b6dcbcb9c2ea076b4c7a6a4b5e4f
diff --git a/Android.bp b/Android.bp
index ec6d1fe..a6e4274 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,10 +1,18 @@
+package {
+    // http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
 android_app {
     name: "DeskClock",
     resource_dirs: ["res"],
     sdk_version: "current",
-    overrides: ["AlarmClock"],
+    target_sdk_version: "24",
+    overrides: [
+        "AlarmClock",
+    ],
     srcs: [
-        "src/**/*.java",
+        "src/**/*.kt",
         "gen/**/*.java",
     ],
     product_specific: true,
@@ -26,4 +34,6 @@
         "androidx.gridlayout_gridlayout",
         "androidx.recyclerview_recyclerview",
     ],
+
+    aaptflags: ["--legacy"],
 }
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index fc966ab..963afe4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -22,7 +22,7 @@
     <original-package android:name="com.android.alarmclock" />
     <original-package android:name="com.android.deskclock" />
 
-    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="25" />
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="24" />
 
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -30,6 +30,10 @@
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
+    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
 
     <!-- WRITE_SETTINGS is required to record the upcoming alarm prior to L -->
     <uses-permission
@@ -60,6 +64,7 @@
             android:name=".DeskClock"
             android:label="@string/app_label"
             android:launchMode="singleTask"
+            android:exported="true"
             android:windowSoftInputMode="adjustPan">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -103,10 +108,14 @@
         <!-- ============================================================== -->
 
         <activity
-            android:name=".HandleApiCalls"
+            android:name="com.android.deskclock.HandleApiCalls"
+            android:permission="com.android.alarm.permission.SET_ALARM"
+            android:directBootAware="true"
             android:excludeFromRecents="true"
             android:launchMode="singleInstance"
+            android:showWhenLocked="true"
             android:taskAffinity=""
+            android:exported="true"
             android:theme="@android:style/Theme.NoDisplay">
             <intent-filter>
                 <action android:name="android.intent.action.DISMISS_ALARM" />
@@ -121,9 +130,10 @@
         </activity>
 
         <activity-alias
-            android:name=".HandleSetAlarmApiCalls"
+            android:name="com.android.deskclock.HandleSetAlarmApiCalls"
             android:permission="com.android.alarm.permission.SET_ALARM"
-            android:targetActivity=".HandleApiCalls">
+            android:exported="true"
+            android:targetActivity="com.android.deskclock.HandleApiCalls">
             <intent-filter>
                 <action android:name="android.intent.action.SET_ALARM" />
                 <action android:name="android.intent.action.SET_TIMER" />
@@ -160,6 +170,7 @@
 
         <receiver
             android:name=".AlarmInitReceiver"
+            android:exported="true"
             android:directBootAware="true">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
@@ -237,6 +248,7 @@
         <service
             android:name=".Screensaver"
             android:label="@string/app_label"
+            android:exported="true"
             android:permission="android.permission.BIND_DREAM_SERVICE">
             <intent-filter>
                 <action android:name="android.service.dreams.DreamService" />
@@ -255,6 +267,7 @@
 
         <receiver
             android:name="com.android.alarmclock.AnalogAppWidgetProvider"
+            android:exported="true"
             android:label="@string/analog_gadget">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -270,6 +283,7 @@
 
         <receiver
             android:name="com.android.alarmclock.DigitalAppWidgetProvider"
+            android:exported="true"
             android:label="@string/digital_gadget">
             <intent-filter>
                 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
deleted file mode 100644
index e69de29..0000000
--- a/MODULE_LICENSE_APACHE2
+++ /dev/null
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index c5b1efa..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,190 +0,0 @@
-
-   Copyright (c) 2005-2008, The Android Open Source Project
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with 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.
-
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 443d182..c966686 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1069,4 +1069,12 @@
     -->
     <string name="alarm_is_snoozed"><xliff:g id="alarm_time" example="14:20">%s</xliff:g> alarm snoozed for 10 minutes</string>
 
+    <!-- Strings for notification channel. -->
+    <string name="firing_alarms_timers_channel">Firing alarms &amp; timers</string>
+    <string name="alarm_missed_channel">Missed alarms</string>
+    <string name="alarm_snooze_channel">Snoozed alarms</string>
+    <string name="alarm_upcoming_channel">Upcoming alarms</string>
+    <string name="stopwatch_channel">Stopwatch</string>
+    <string name="timer_channel">Timer</string>
+
 </resources>
diff --git a/src/com/android/alarmclock/AnalogAppWidgetProvider.java b/src/com/android/alarmclock/AnalogAppWidgetProvider.java
deleted file mode 100644
index 80bc22f..0000000
--- a/src/com/android/alarmclock/AnalogAppWidgetProvider.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.alarmclock;
-
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.widget.RemoteViews;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-/**
- * Simple widget to show an analog clock.
- */
-public class AnalogAppWidgetProvider extends AppWidgetProvider {
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        super.onReceive(context, intent);
-
-        final AppWidgetManager wm = AppWidgetManager.getInstance(context);
-        if (wm == null) {
-            return;
-        }
-
-        // Send events for newly created/deleted widgets.
-        final ComponentName provider = new ComponentName(context, getClass());
-        final int widgetCount = wm.getAppWidgetIds(provider).length;
-
-        final DataModel dm = DataModel.getDataModel();
-        dm.updateWidgetCount(getClass(), widgetCount, R.string.category_analog_widget);
-    }
-
-    /**
-     * Called when widgets must provide remote views.
-     */
-    @Override
-    public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
-        super.onUpdate(context, wm, widgetIds);
-
-        for (int widgetId : widgetIds) {
-            final String packageName = context.getPackageName();
-            final RemoteViews widget = new RemoteViews(packageName, R.layout.analog_appwidget);
-
-            // Tapping on the widget opens the app (if not on the lock screen).
-            if (Utils.isWidgetClickable(wm, widgetId)) {
-                final Intent openApp = new Intent(context, DeskClock.class);
-                final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
-                widget.setOnClickPendingIntent(R.id.analog_appwidget, pi);
-            }
-
-            wm.updateAppWidget(widgetId, widget);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/AnalogAppWidgetProvider.kt b/src/com/android/alarmclock/AnalogAppWidgetProvider.kt
new file mode 100644
index 0000000..743e441
--- /dev/null
+++ b/src/com/android/alarmclock/AnalogAppWidgetProvider.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.alarmclock
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.widget.RemoteViews
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+/**
+ * Simple widget to show an analog clock.
+ */
+class AnalogAppWidgetProvider : AppWidgetProvider() {
+    override fun onReceive(context: Context, intent: Intent?) {
+        super.onReceive(context, intent)
+        val wm: AppWidgetManager = AppWidgetManager.getInstance(context) ?: return
+
+        // Send events for newly created/deleted widgets.
+        val provider = ComponentName(context, javaClass)
+        val widgetCount: Int = wm.getAppWidgetIds(provider).size
+        val dm = DataModel.dataModel
+        dm.updateWidgetCount(javaClass, widgetCount, R.string.category_analog_widget)
+    }
+
+    /**
+     * Called when widgets must provide remote views.
+     */
+    override fun onUpdate(context: Context, wm: AppWidgetManager, widgetIds: IntArray) {
+        super.onUpdate(context, wm, widgetIds)
+        widgetIds.forEach { widgetId ->
+            val packageName: String = context.getPackageName()
+            val widget = RemoteViews(packageName, R.layout.analog_appwidget)
+
+            // Tapping on the widget opens the app (if not on the lock screen).
+            if (Utils.isWidgetClickable(wm, widgetId)) {
+                val openApp = Intent(context, DeskClock::class.java)
+                val pi: PendingIntent = PendingIntent.getActivity(context, 0, openApp, 0)
+                widget.setOnClickPendingIntent(R.id.analog_appwidget, pi)
+            }
+            wm.updateAppWidget(widgetId, widget)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/DigitalAppWidgetCityService.java b/src/com/android/alarmclock/DigitalAppWidgetCityService.java
deleted file mode 100644
index 6392697..0000000
--- a/src/com/android/alarmclock/DigitalAppWidgetCityService.java
+++ /dev/null
@@ -1,28 +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.alarmclock;
-
-import android.content.Intent;
-import android.widget.RemoteViewsService;
-
-public class DigitalAppWidgetCityService extends RemoteViewsService {
-
-    @Override
-    public RemoteViewsFactory onGetViewFactory(Intent i) {
-        return new DigitalAppWidgetCityViewsFactory(getApplicationContext(), i);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java b/src/com/android/alarmclock/DigitalAppWidgetCityService.kt
similarity index 60%
copy from src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
copy to src/com/android/alarmclock/DigitalAppWidgetCityService.kt
index c3e460d..36521b2 100644
--- a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
+++ b/src/com/android/alarmclock/DigitalAppWidgetCityService.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open 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,18 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package com.android.alarmclock
 
-package com.android.deskclock.actionbarmenu;
+import android.content.Intent
+import android.widget.RemoteViewsService
 
-import android.app.Activity;
-
-/**
- * Provider for a {@link MenuItemController} instances.
- */
-public interface MenuItemProvider {
-
-    /**
-     * provides a {@link MenuItemController} that handles menu item.
-     */
-    MenuItemController provide(Activity activity);
-}
+class DigitalAppWidgetCityService : RemoteViewsService() {
+    override fun onGetViewFactory(i: Intent): RemoteViewsFactory {
+        return DigitalAppWidgetCityViewsFactory(getApplicationContext(), i)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java
deleted file mode 100644
index 849e8cf..0000000
--- a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java
+++ /dev/null
@@ -1,231 +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.alarmclock;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.text.format.DateFormat;
-import android.util.TypedValue;
-import android.view.View;
-import android.widget.RemoteViews;
-import android.widget.RemoteViewsService.RemoteViewsFactory;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.City;
-import com.android.deskclock.data.DataModel;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
-
-import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
-import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
-import static java.util.Calendar.DAY_OF_WEEK;
-
-/**
- * This factory produces entries in the world cities list view displayed at the bottom of the
- * digital widget. Each row is comprised of two world cities located side-by-side.
- */
-public class DigitalAppWidgetCityViewsFactory implements RemoteViewsFactory {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigWidgetViewsFactory");
-
-    private final Intent mFillInIntent = new Intent();
-
-    private final Context mContext;
-    private final float m12HourFontSize;
-    private final float m24HourFontSize;
-    private final int mWidgetId;
-    private float mFontScale = 1;
-
-    private City mHomeCity;
-    private boolean mShowHomeClock;
-    private List<City> mCities = Collections.emptyList();
-
-    public DigitalAppWidgetCityViewsFactory(Context context, Intent intent) {
-        mContext = context;
-        mWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
-
-        final Resources res = context.getResources();
-        m12HourFontSize = res.getDimension(R.dimen.digital_widget_city_12_medium_font_size);
-        m24HourFontSize = res.getDimension(R.dimen.digital_widget_city_24_medium_font_size);
-    }
-
-    @Override
-    public void onCreate() {
-        LOGGER.i("DigitalAppWidgetCityViewsFactory onCreate " + mWidgetId);
-    }
-
-    @Override
-    public void onDestroy() {
-        LOGGER.i("DigitalAppWidgetCityViewsFactory onDestroy " + mWidgetId);
-    }
-
-    /**
-     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
-     * mShowHomeClock.</p>
-     *
-     * {@inheritDoc}
-     */
-    @Override
-    public synchronized int getCount() {
-        final int homeClockCount = mShowHomeClock ? 1 : 0;
-        final int worldClockCount = mCities.size();
-        final double totalClockCount = homeClockCount + worldClockCount;
-
-        // number of clocks / 2 clocks per row
-        return (int) Math.ceil(totalClockCount / 2);
-    }
-
-    /**
-     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
-     * mShowHomeClock.</p>
-     *
-     * {@inheritDoc}
-     */
-    @Override
-    public synchronized RemoteViews getViewAt(int position) {
-        final int homeClockOffset = mShowHomeClock ? -1 : 0;
-        final int leftIndex = position * 2 + homeClockOffset;
-        final int rightIndex = leftIndex + 1;
-
-        final City left = leftIndex == -1 ? mHomeCity :
-                (leftIndex < mCities.size() ? mCities.get(leftIndex) : null);
-        final City right = rightIndex < mCities.size() ? mCities.get(rightIndex) : null;
-
-        final RemoteViews rv =
-                new RemoteViews(mContext.getPackageName(), R.layout.world_clock_remote_list_item);
-
-        // Show the left clock if one exists.
-        if (left != null) {
-            update(rv, left, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
-        } else {
-            hide(rv, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
-        }
-
-        // Show the right clock if one exists.
-        if (right != null) {
-            update(rv, right, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
-        } else {
-            hide(rv, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
-        }
-
-        // Hide last spacer in last row; show for all others.
-        final boolean lastRow = position == getCount() - 1;
-        rv.setViewVisibility(R.id.city_spacer, lastRow ? View.GONE : View.VISIBLE);
-
-        rv.setOnClickFillInIntent(R.id.widget_item, mFillInIntent);
-        return rv;
-    }
-
-    @Override
-    public long getItemId(int position) {
-        return position;
-    }
-
-    @Override
-    public RemoteViews getLoadingView() {
-        return null;
-    }
-
-    @Override
-    public int getViewTypeCount() {
-        return 1;
-    }
-
-    @Override
-    public boolean hasStableIds() {
-        return false;
-    }
-
-    /**
-     * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
-     * mShowHomeClock.</p>
-     *
-     * {@inheritDoc}
-     */
-    @Override
-    public synchronized void onDataSetChanged() {
-        // Fetch the data on the main Looper.
-        final RefreshRunnable refreshRunnable = new RefreshRunnable();
-        DataModel.getDataModel().run(refreshRunnable);
-
-        // Store the data in local variables.
-        mHomeCity = refreshRunnable.mHomeCity;
-        mCities = refreshRunnable.mCities;
-        mShowHomeClock = refreshRunnable.mShowHomeClock;
-        mFontScale = WidgetUtils.getScaleRatio(mContext, null, mWidgetId, mCities.size());
-    }
-
-    private void update(RemoteViews rv, City city, int clockId, int labelId, int dayId) {
-        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;
-        rv.setTextViewTextSize(clockId, TypedValue.COMPLEX_UNIT_PX, fontSize * mFontScale);
-        rv.setString(clockId, "setTimeZone", city.getTimeZone().getID());
-        rv.setTextViewText(labelId, city.getName());
-
-        // Compute if the city week day matches the weekday of the current timezone.
-        final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
-        final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
-        final boolean displayDayOfWeek = localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
-
-        // Bind the week day display.
-        if (displayDayOfWeek) {
-            final Locale locale = Locale.getDefault();
-            final String weekday = cityCal.getDisplayName(DAY_OF_WEEK, Calendar.SHORT, locale);
-            final String slashDay = mContext.getString(R.string.world_day_of_week_label, weekday);
-            rv.setTextViewText(dayId, slashDay);
-        }
-
-        rv.setViewVisibility(dayId, displayDayOfWeek ? View.VISIBLE : View.GONE);
-        rv.setViewVisibility(clockId, View.VISIBLE);
-        rv.setViewVisibility(labelId, View.VISIBLE);
-    }
-
-    private void hide(RemoteViews clock, int clockId, int labelId, int dayId) {
-        clock.setViewVisibility(dayId, View.INVISIBLE);
-        clock.setViewVisibility(clockId, View.INVISIBLE);
-        clock.setViewVisibility(labelId, View.INVISIBLE);
-    }
-
-    /**
-     * This Runnable fetches data for this factory on the main thread to ensure all DataModel reads
-     * occur on the main thread.
-     */
-    private static final class RefreshRunnable implements Runnable {
-
-        private City mHomeCity;
-        private List<City> mCities;
-        private boolean mShowHomeClock;
-
-        @Override
-        public void run() {
-            mHomeCity = DataModel.getDataModel().getHomeCity();
-            mCities = new ArrayList<>(DataModel.getDataModel().getSelectedCities());
-            mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
-        }
-    }
-}
diff --git a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.kt b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.kt
new file mode 100644
index 0000000..a4e55ad
--- /dev/null
+++ b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.alarmclock
+
+import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
+import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.text.format.DateFormat
+import android.util.TypedValue
+import android.view.View
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService.RemoteViewsFactory
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.City
+import com.android.deskclock.data.DataModel
+
+import java.util.ArrayList
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * This factory produces entries in the world cities list view displayed at the bottom of the
+ * digital widget. Each row is comprised of two world cities located side-by-side.
+ */
+class DigitalAppWidgetCityViewsFactory(context: Context, intent: Intent) : RemoteViewsFactory {
+    private val mFillInIntent: Intent = Intent()
+    private val mContext: Context = context
+    private val m12HourFontSize: Float
+    private val m24HourFontSize: Float
+    private val mWidgetId: Int = intent.getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID)
+    private var mFontScale = 1f
+    private var mHomeCity: City? = null
+    private var mShowHomeClock = false
+    private var mCities: List<City>? = emptyList()
+
+    init {
+        val res: Resources = context.getResources()
+        m12HourFontSize = res.getDimension(R.dimen.digital_widget_city_12_medium_font_size)
+        m24HourFontSize = res.getDimension(R.dimen.digital_widget_city_24_medium_font_size)
+    }
+
+    override fun onCreate() {
+        LOGGER.i("DigitalAppWidgetCityViewsFactory onCreate $mWidgetId")
+    }
+
+    override fun onDestroy() {
+        LOGGER.i("DigitalAppWidgetCityViewsFactory onDestroy $mWidgetId")
+    }
+
+    /**
+     * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
+     * mShowHomeClock.
+     *
+     * {@inheritDoc}
+     */
+    @Synchronized
+    override fun getCount(): Int {
+        val homeClockCount = if (mShowHomeClock) 1 else 0
+        val worldClockCount = mCities!!.size
+        val totalClockCount = homeClockCount + worldClockCount.toDouble()
+
+        // Number of clocks / 2 clocks per row
+        return Math.ceil(totalClockCount / 2).toInt()
+    }
+
+    /**
+     * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
+     * mShowHomeClock.
+     *
+     * {@inheritDoc}
+     */
+    @Synchronized
+    override fun getViewAt(position: Int): RemoteViews {
+        val homeClockOffset = if (mShowHomeClock) -1 else 0
+        val leftIndex = position * 2 + homeClockOffset
+        val rightIndex = leftIndex + 1
+        val left = when {
+            leftIndex == -1 -> mHomeCity
+            leftIndex < mCities!!.size -> mCities!![leftIndex]
+            else -> null
+        }
+        val right = if (rightIndex < mCities!!.size) mCities!![rightIndex] else null
+        val rv = RemoteViews(mContext.getPackageName(), R.layout.world_clock_remote_list_item)
+
+        // Show the left clock if one exists.
+        if (left != null) {
+            update(rv, left, R.id.left_clock, R.id.city_name_left, R.id.city_day_left)
+        } else {
+            hide(rv, R.id.left_clock, R.id.city_name_left, R.id.city_day_left)
+        }
+
+        // Show the right clock if one exists.
+        if (right != null) {
+            update(rv, right, R.id.right_clock, R.id.city_name_right, R.id.city_day_right)
+        } else {
+            hide(rv, R.id.right_clock, R.id.city_name_right, R.id.city_day_right)
+        }
+
+        // Hide last spacer in last row; show for all others.
+        val lastRow = position == count - 1
+        rv.setViewVisibility(R.id.city_spacer, if (lastRow) View.GONE else View.VISIBLE)
+        rv.setOnClickFillInIntent(R.id.widget_item, mFillInIntent)
+        return rv
+    }
+
+    override fun getItemId(position: Int): Long {
+        return position.toLong()
+    }
+
+    override fun getLoadingView(): RemoteViews? {
+        return null
+    }
+
+    override fun getViewTypeCount(): Int {
+        return 1
+    }
+
+    override fun hasStableIds(): Boolean {
+        return false
+    }
+
+    /**
+     * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
+     * mShowHomeClock.
+     *
+     * {@inheritDoc}
+     */
+    @Synchronized
+    override fun onDataSetChanged() {
+        // Fetch the data on the main Looper.
+        val refreshRunnable = RefreshRunnable()
+        DataModel.dataModel.run(refreshRunnable)
+
+        // Store the data in local variables.
+        mHomeCity = refreshRunnable.mHomeCity
+        mCities = refreshRunnable.mCities
+        mShowHomeClock = refreshRunnable.mShowHomeClock
+        mFontScale = WidgetUtils.getScaleRatio(mContext, null, mWidgetId, mCities!!.size)
+    }
+
+    private fun update(rv: RemoteViews, city: City, clockId: Int, labelId: Int, dayId: Int) {
+        rv.setCharSequence(clockId, "setFormat12Hour", Utils.get12ModeFormat(0.4f, false))
+        rv.setCharSequence(clockId, "setFormat24Hour", Utils.get24ModeFormat(false))
+
+        val is24HourFormat: Boolean = DateFormat.is24HourFormat(mContext)
+        val fontSize = if (is24HourFormat) m24HourFontSize else m12HourFontSize
+        rv.setTextViewTextSize(clockId, TypedValue.COMPLEX_UNIT_PX, fontSize * mFontScale)
+        rv.setString(clockId, "setTimeZone", city.timeZone.id)
+        rv.setTextViewText(labelId, city.name)
+
+        // Compute if the city week day matches the weekday of the current timezone.
+        val localCal = Calendar.getInstance(TimeZone.getDefault())
+        val cityCal = Calendar.getInstance(city.timeZone)
+        val displayDayOfWeek = localCal[Calendar.DAY_OF_WEEK] != cityCal[Calendar.DAY_OF_WEEK]
+
+        // Bind the week day display.
+        if (displayDayOfWeek) {
+            val locale = Locale.getDefault()
+            val weekday = cityCal.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, locale)
+            val slashDay: String = mContext.getString(R.string.world_day_of_week_label, weekday)
+            rv.setTextViewText(dayId, slashDay)
+        }
+
+        rv.setViewVisibility(dayId, if (displayDayOfWeek) View.VISIBLE else View.GONE)
+        rv.setViewVisibility(clockId, View.VISIBLE)
+        rv.setViewVisibility(labelId, View.VISIBLE)
+    }
+
+    private fun hide(clock: RemoteViews, clockId: Int, labelId: Int, dayId: Int) {
+        clock.setViewVisibility(dayId, View.INVISIBLE)
+        clock.setViewVisibility(clockId, View.INVISIBLE)
+        clock.setViewVisibility(labelId, View.INVISIBLE)
+    }
+
+    /**
+     * This Runnable fetches data for this factory on the main thread to ensure all DataModel reads
+     * occur on the main thread.
+     */
+    private class RefreshRunnable : Runnable {
+        var mHomeCity: City? = null
+        var mCities: List<City>? = null
+        var mShowHomeClock = false
+
+        override fun run() {
+            mHomeCity = DataModel.dataModel.homeCity
+            mCities = ArrayList(DataModel.dataModel.selectedCities)
+            mShowHomeClock = DataModel.dataModel.showHomeClock
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("DigWidgetViewsFactory")
+    }
+}
diff --git a/src/com/android/alarmclock/DigitalAppWidgetProvider.java b/src/com/android/alarmclock/DigitalAppWidgetProvider.java
deleted file mode 100644
index 3be07ec..0000000
--- a/src/com/android/alarmclock/DigitalAppWidgetProvider.java
+++ /dev/null
@@ -1,543 +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.alarmclock;
-
-import android.annotation.SuppressLint;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.util.ArraySet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.RemoteViews;
-import android.widget.TextClock;
-import android.widget.TextView;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.City;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.worldclock.CitySelectionActivity;
-
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.TimeZone;
-
-import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
-import static android.app.PendingIntent.FLAG_NO_CREATE;
-import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
-import static android.content.Intent.ACTION_DATE_CHANGED;
-import static android.content.Intent.ACTION_LOCALE_CHANGED;
-import static android.content.Intent.ACTION_SCREEN_ON;
-import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
-import static android.content.Intent.ACTION_TIME_CHANGED;
-import static android.util.TypedValue.COMPLEX_UNIT_PX;
-import static android.view.View.GONE;
-import static android.view.View.MeasureSpec.UNSPECIFIED;
-import static android.view.View.VISIBLE;
-import static com.android.deskclock.alarms.AlarmStateManager.ACTION_ALARM_CHANGED;
-import static com.android.deskclock.data.DataModel.ACTION_WORLD_CITIES_CHANGED;
-import static java.lang.Math.max;
-import static java.lang.Math.round;
-
-/**
- * <p>This provider produces a widget resembling one of the formats below.</p>
- *
- * If an alarm is scheduled to ring in the future:
- * <pre>
- *         12:59 AM
- * WED, FEB 3 ⏰ THU 9:30 AM
- * </pre>
- *
- * If no alarm is scheduled to ring in the future:
- * <pre>
- *         12:59 AM
- *        WED, FEB 3
- * </pre>
- *
- * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
- * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
- * choose optimal values.
- */
-public class DigitalAppWidgetProvider extends AppWidgetProvider {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigitalWidgetProvider");
-
-    /**
-     * Intent action used for refreshing a world city display when any of them changes days or when
-     * the default TimeZone changes days. This affects the widget display because the day-of-week is
-     * only visible when the world city day-of-week differs from the default TimeZone's day-of-week.
-     */
-    private static final String ACTION_ON_DAY_CHANGE = "com.android.deskclock.ON_DAY_CHANGE";
-
-    /** Intent used to deliver the {@link #ACTION_ON_DAY_CHANGE} callback. */
-    private static final Intent DAY_CHANGE_INTENT = new Intent(ACTION_ON_DAY_CHANGE);
-
-    @Override
-    public void onEnabled(Context context) {
-        super.onEnabled(context);
-
-        // Schedule the day-change callback if necessary.
-        updateDayChangeCallback(context);
-    }
-
-    @Override
-    public void onDisabled(Context context) {
-        super.onDisabled(context);
-
-        // Remove any scheduled day-change callback.
-        removeDayChangeCallback(context);
-    }
-
-    @Override
-    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
-        LOGGER.i("onReceive: " + intent);
-        super.onReceive(context, intent);
-
-        final AppWidgetManager wm = AppWidgetManager.getInstance(context);
-        if (wm == null) {
-            return;
-        }
-
-        final ComponentName provider = new ComponentName(context, getClass());
-        final int[] widgetIds = wm.getAppWidgetIds(provider);
-
-        final String action = intent.getAction();
-        switch (action) {
-            case ACTION_NEXT_ALARM_CLOCK_CHANGED:
-            case ACTION_DATE_CHANGED:
-            case ACTION_LOCALE_CHANGED:
-            case ACTION_SCREEN_ON:
-            case ACTION_TIME_CHANGED:
-            case ACTION_TIMEZONE_CHANGED:
-            case ACTION_ALARM_CHANGED:
-            case ACTION_ON_DAY_CHANGE:
-            case ACTION_WORLD_CITIES_CHANGED:
-                for (int widgetId : widgetIds) {
-                    relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
-                }
-        }
-
-        final DataModel dm = DataModel.getDataModel();
-        dm.updateWidgetCount(getClass(), widgetIds.length, R.string.category_digital_widget);
-
-        if (widgetIds.length > 0) {
-            updateDayChangeCallback(context);
-        }
-    }
-
-    /**
-     * Called when widgets must provide remote views.
-     */
-    @Override
-    public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
-        super.onUpdate(context, wm, widgetIds);
-
-        for (int widgetId : widgetIds) {
-            relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
-        }
-    }
-
-    /**
-     * Called when the app widget changes sizes.
-     */
-    @Override
-    public void onAppWidgetOptionsChanged(Context context, AppWidgetManager wm, int widgetId,
-            Bundle options) {
-        super.onAppWidgetOptionsChanged(context, wm, widgetId, options);
-
-        // scale the fonts of the clock to fit inside the new size
-        relayoutWidget(context, AppWidgetManager.getInstance(context), widgetId, options);
-    }
-
-    /**
-     * Compute optimal font and icon sizes offscreen for both portrait and landscape orientations
-     * using the last known widget size and apply them to the widget.
-     */
-    private static void relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
-            Bundle options) {
-        final RemoteViews portrait = relayoutWidget(context, wm, widgetId, options, true);
-        final RemoteViews landscape = relayoutWidget(context, wm, widgetId, options, false);
-        final RemoteViews widget = new RemoteViews(landscape, portrait);
-        wm.updateAppWidget(widgetId, widget);
-        wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list);
-    }
-
-    /**
-     * Compute optimal font and icon sizes offscreen for the given orientation.
-     */
-    private static RemoteViews relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
-            Bundle options, boolean portrait) {
-        // Create a remote view for the digital clock.
-        final String packageName = context.getPackageName();
-        final RemoteViews rv = new RemoteViews(packageName, R.layout.digital_widget);
-
-        // Tapping on the widget opens the app (if not on the lock screen).
-        if (Utils.isWidgetClickable(wm, widgetId)) {
-            final Intent openApp = new Intent(context, DeskClock.class);
-            final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
-            rv.setOnClickPendingIntent(R.id.digital_widget, pi);
-        }
-
-        // Configure child views of the remote view.
-        final CharSequence dateFormat = getDateFormat(context);
-        rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat);
-        rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat);
-
-        final String nextAlarmTime = Utils.getNextAlarm(context);
-        if (TextUtils.isEmpty(nextAlarmTime)) {
-            rv.setViewVisibility(R.id.nextAlarm, GONE);
-            rv.setViewVisibility(R.id.nextAlarmIcon, GONE);
-        } else  {
-            rv.setTextViewText(R.id.nextAlarm, nextAlarmTime);
-            rv.setViewVisibility(R.id.nextAlarm, VISIBLE);
-            rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE);
-        }
-
-        if (options == null) {
-            options = wm.getAppWidgetOptions(widgetId);
-        }
-
-        // Fetch the widget size selected by the user.
-        final Resources resources = context.getResources();
-        final float density = resources.getDisplayMetrics().density;
-        final int minWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
-        final int minHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
-        final int maxWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
-        final int maxHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
-        final int targetWidthPx = portrait ? minWidthPx : maxWidthPx;
-        final int targetHeightPx = portrait ? maxHeightPx : minHeightPx;
-        final int largestClockFontSizePx =
-                resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size);
-
-        // Create a size template that describes the widget bounds.
-        final Sizes template = new Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx);
-
-        // Compute optimal font sizes and icon sizes to fit within the widget bounds.
-        final Sizes sizes = optimizeSizes(context, template, nextAlarmTime);
-        if (LOGGER.isVerboseLoggable()) {
-            LOGGER.v(sizes.toString());
-        }
-
-        // Apply the computed sizes to the remote views.
-        rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap);
-        rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx);
-        rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx);
-        rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx);
-
-        final int smallestWorldCityListSizePx =
-                resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size);
-        if (sizes.getListHeight() <= smallestWorldCityListSizePx) {
-            // Insufficient space; hide the world city list.
-            rv.setViewVisibility(R.id.world_city_list, GONE);
-        } else {
-            // Set an adapter on the world city list. That adapter connects to a Service via intent.
-            final Intent intent = new Intent(context, DigitalAppWidgetCityService.class);
-            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
-            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
-            rv.setRemoteAdapter(R.id.world_city_list, intent);
-            rv.setViewVisibility(R.id.world_city_list, VISIBLE);
-
-            // Tapping on the widget opens the city selection activity (if not on the lock screen).
-            if (Utils.isWidgetClickable(wm, widgetId)) {
-                final Intent selectCity = new Intent(context, CitySelectionActivity.class);
-                final PendingIntent pi = PendingIntent.getActivity(context, 0, selectCity, 0);
-                rv.setPendingIntentTemplate(R.id.world_city_list, pi);
-            }
-        }
-
-        return rv;
-    }
-
-    /**
-     * Inflate an offscreen copy of the widget views. Binary search through the range of sizes until
-     * the optimal sizes that fit within the widget bounds are located.
-     */
-    private static Sizes optimizeSizes(Context context, Sizes template, String nextAlarmTime) {
-        // Inflate a test layout to compute sizes at different font sizes.
-        final LayoutInflater inflater = LayoutInflater.from(context);
-        @SuppressLint("InflateParams")
-        final View sizer = inflater.inflate(R.layout.digital_widget_sizer, null /* root */);
-
-        // Configure the date to display the current date string.
-        final CharSequence dateFormat = getDateFormat(context);
-        final TextClock date = (TextClock) sizer.findViewById(R.id.date);
-        date.setFormat12Hour(dateFormat);
-        date.setFormat24Hour(dateFormat);
-
-        // Configure the next alarm views to display the next alarm time or be gone.
-        final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
-        final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
-        if (TextUtils.isEmpty(nextAlarmTime)) {
-            nextAlarm.setVisibility(GONE);
-            nextAlarmIcon.setVisibility(GONE);
-        } else  {
-            nextAlarm.setText(nextAlarmTime);
-            nextAlarm.setVisibility(VISIBLE);
-            nextAlarmIcon.setVisibility(VISIBLE);
-            nextAlarmIcon.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
-        }
-
-        // Measure the widget at the largest possible size.
-        Sizes high = measure(template, template.getLargestClockFontSizePx(), sizer);
-        if (!high.hasViolations()) {
-            return high;
-        }
-
-        // Measure the widget at the smallest possible size.
-        Sizes low = measure(template, template.getSmallestClockFontSizePx(), sizer);
-        if (low.hasViolations()) {
-            return low;
-        }
-
-        // Binary search between the smallest and largest sizes until an optimum size is found.
-        while (low.getClockFontSizePx() != high.getClockFontSizePx()) {
-            final int midFontSize = (low.getClockFontSizePx() + high.getClockFontSizePx()) / 2;
-            if (midFontSize == low.getClockFontSizePx()) {
-                return low;
-            }
-
-            final Sizes midSize = measure(template, midFontSize, sizer);
-            if (midSize.hasViolations()) {
-                high = midSize;
-            } else {
-                low = midSize;
-            }
-        }
-
-        return low;
-    }
-
-    /**
-     * Remove the existing day-change callback if it is not needed (no selected cities exist).
-     * Add the day-change callback if it is needed (selected cities exist).
-     */
-    private void updateDayChangeCallback(Context context) {
-        final DataModel dm = DataModel.getDataModel();
-        final List<City> selectedCities = dm.getSelectedCities();
-        final boolean showHomeClock = dm.getShowHomeClock();
-        if (selectedCities.isEmpty() && !showHomeClock) {
-            // Remove the existing day-change callback.
-            removeDayChangeCallback(context);
-            return;
-        }
-
-        // Look up the time at which the next day change occurs across all timezones.
-        final Set<TimeZone> zones = new ArraySet<>(selectedCities.size() + 2);
-        zones.add(TimeZone.getDefault());
-        if (showHomeClock) {
-            zones.add(dm.getHomeCity().getTimeZone());
-        }
-        for (City city : selectedCities) {
-            zones.add(city.getTimeZone());
-        }
-        final Date nextDay = Utils.getNextDay(new Date(), zones);
-
-        // Schedule the next day-change callback; at least one city is displayed.
-        final PendingIntent pi =
-                PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_UPDATE_CURRENT);
-        getAlarmManager(context).setExact(AlarmManager.RTC, nextDay.getTime(), pi);
-    }
-
-    /**
-     * Remove the existing day-change callback.
-     */
-    private void removeDayChangeCallback(Context context) {
-        final PendingIntent pi =
-                PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_NO_CREATE);
-        if (pi != null) {
-            getAlarmManager(context).cancel(pi);
-            pi.cancel();
-        }
-    }
-
-    private static AlarmManager getAlarmManager(Context context) {
-        return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
-    }
-
-    /**
-     * Compute all font and icon sizes based on the given {@code clockFontSize} and apply them to
-     * the offscreen {@code sizer} view. Measure the {@code sizer} view and return the resulting
-     * size measurements.
-     */
-    private static Sizes measure(Sizes template, int clockFontSize, View sizer) {
-        // Create a copy of the given template sizes.
-        final Sizes measuredSizes = template.newSize();
-
-        // Configure the clock to display the widest time string.
-        final TextClock date = (TextClock) sizer.findViewById(R.id.date);
-        final TextClock clock = (TextClock) sizer.findViewById(R.id.clock);
-        final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
-        final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
-
-        // Adjust the font sizes.
-        measuredSizes.setClockFontSizePx(clockFontSize);
-        clock.setText(getLongestTimeString(clock));
-        clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx);
-        date.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
-        nextAlarm.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
-        nextAlarmIcon.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mIconFontSizePx);
-        nextAlarmIcon.setPadding(measuredSizes.mIconPaddingPx, 0, measuredSizes.mIconPaddingPx, 0);
-
-        // Measure and layout the sizer.
-        final int widthSize = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx);
-        final int heightSize = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx);
-        final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED);
-        final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED);
-        sizer.measure(widthMeasureSpec, heightMeasureSpec);
-        sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight());
-
-        // Copy the measurements into the result object.
-        measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth();
-        measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight();
-        measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth();
-        measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight();
-
-        // If an alarm icon is required, generate one from the TextView with the special font.
-        if (nextAlarmIcon.getVisibility() == VISIBLE) {
-            measuredSizes.mIconBitmap = Utils.createBitmap(nextAlarmIcon);
-        }
-
-        return measuredSizes;
-    }
-
-    /**
-     * @return "11:59" or "23:59" in the current locale
-     */
-    private static CharSequence getLongestTimeString(TextClock clock) {
-        final CharSequence format = clock.is24HourModeEnabled()
-                ? clock.getFormat24Hour()
-                : clock.getFormat12Hour();
-        final Calendar longestPMTime = Calendar.getInstance();
-        longestPMTime.set(0, 0, 0, 23, 59);
-        return DateFormat.format(format, longestPMTime);
-    }
-
-    /**
-     * @return the locale-specific date pattern
-     */
-    private static String getDateFormat(Context context) {
-        final Locale locale = Locale.getDefault();
-        final String skeleton = context.getString(R.string.abbrev_wday_month_day_no_year);
-        return DateFormat.getBestDateTimePattern(locale, skeleton);
-    }
-
-    /**
-     * This class stores the target size of the widget as well as the measured size using a given
-     * clock font size. All other fonts and icons are scaled proportional to the clock font.
-     */
-    private static final class Sizes {
-
-        private final int mTargetWidthPx;
-        private final int mTargetHeightPx;
-        private final int mLargestClockFontSizePx;
-        private final int mSmallestClockFontSizePx;
-        private Bitmap mIconBitmap;
-
-        private int mMeasuredWidthPx;
-        private int mMeasuredHeightPx;
-        private int mMeasuredTextClockWidthPx;
-        private int mMeasuredTextClockHeightPx;
-
-        /** The size of the font to use on the date / next alarm time fields. */
-        private int mFontSizePx;
-
-        /** The size of the font to use on the clock field. */
-        private int mClockFontSizePx;
-
-        private int mIconFontSizePx;
-        private int mIconPaddingPx;
-
-        private Sizes(int targetWidthPx, int targetHeightPx, int largestClockFontSizePx) {
-            mTargetWidthPx = targetWidthPx;
-            mTargetHeightPx = targetHeightPx;
-            mLargestClockFontSizePx = largestClockFontSizePx;
-            mSmallestClockFontSizePx = 1;
-        }
-
-        private int getLargestClockFontSizePx() { return mLargestClockFontSizePx; }
-        private int getSmallestClockFontSizePx() { return mSmallestClockFontSizePx; }
-        private int getClockFontSizePx() { return mClockFontSizePx; }
-        private void setClockFontSizePx(int clockFontSizePx) {
-            mClockFontSizePx = clockFontSizePx;
-            mFontSizePx = max(1, round(clockFontSizePx / 7.5f));
-            mIconFontSizePx = (int) (mFontSizePx * 1.4f);
-            mIconPaddingPx = mFontSizePx / 3;
-        }
-
-        /**
-         * @return the amount of widget height available to the world cities list
-         */
-        private int getListHeight() {
-            return mTargetHeightPx - mMeasuredHeightPx;
-        }
-
-        private boolean hasViolations() {
-            return mMeasuredWidthPx > mTargetWidthPx || mMeasuredHeightPx > mTargetHeightPx;
-        }
-
-        private Sizes newSize() {
-            return new Sizes(mTargetWidthPx, mTargetHeightPx, mLargestClockFontSizePx);
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder builder = new StringBuilder(1000);
-            builder.append("\n");
-            append(builder, "Target dimensions: %dpx x %dpx\n", mTargetWidthPx, mTargetHeightPx);
-            append(builder, "Last valid widget container measurement: %dpx x %dpx\n",
-                    mMeasuredWidthPx, mMeasuredHeightPx);
-            append(builder, "Last text clock measurement: %dpx x %dpx\n",
-                    mMeasuredTextClockWidthPx, mMeasuredTextClockHeightPx);
-            if (mMeasuredWidthPx > mTargetWidthPx) {
-                append(builder, "Measured width %dpx exceeded widget width %dpx\n",
-                        mMeasuredWidthPx, mTargetWidthPx);
-            }
-            if (mMeasuredHeightPx > mTargetHeightPx) {
-                append(builder, "Measured height %dpx exceeded widget height %dpx\n",
-                        mMeasuredHeightPx, mTargetHeightPx);
-            }
-            append(builder, "Clock font: %dpx\n", mClockFontSizePx);
-            return builder.toString();
-        }
-
-        private static void append(StringBuilder builder, String format, Object... args) {
-            builder.append(String.format(Locale.ENGLISH, format, args));
-        }
-    }
-}
diff --git a/src/com/android/alarmclock/DigitalAppWidgetProvider.kt b/src/com/android/alarmclock/DigitalAppWidgetProvider.kt
new file mode 100644
index 0000000..0f517c9
--- /dev/null
+++ b/src/com/android/alarmclock/DigitalAppWidgetProvider.kt
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.alarmclock
+
+import android.annotation.SuppressLint
+import android.app.AlarmManager
+import android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_NO_CREATE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_DATE_CHANGED
+import android.content.Intent.ACTION_LOCALE_CHANGED
+import android.content.Intent.ACTION_SCREEN_ON
+import android.content.Intent.ACTION_TIMEZONE_CHANGED
+import android.content.Intent.ACTION_TIME_CHANGED
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.util.ArraySet
+import android.util.TypedValue.COMPLEX_UNIT_PX
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.View.VISIBLE
+import android.widget.RemoteViews
+import android.widget.TextClock
+import android.widget.TextView
+
+import com.android.deskclock.DeskClock
+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.data.DataModel
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.worldclock.CitySelectionActivity
+
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * This provider produces a widget resembling one of the formats below.
+ *
+ * If an alarm is scheduled to ring in the future:
+ * <pre>
+ *      12:59 AM
+ *      WED, FEB 3 ⏰ THU 9:30 AM
+ * </pre>
+ *
+ * If no alarm is scheduled to ring in the future:
+ * <pre>
+ *      12:59 AM
+ *      WED, FEB 3
+ * </pre>
+ *
+ * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
+ * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
+ * choose optimal values.
+ */
+class DigitalAppWidgetProvider : AppWidgetProvider() {
+
+    override fun onEnabled(context: Context) {
+        super.onEnabled(context)
+
+        // Schedule the day-change callback if necessary.
+        updateDayChangeCallback(context)
+    }
+
+    override fun onDisabled(context: Context) {
+        super.onDisabled(context)
+
+        // Remove any scheduled day-change callback.
+        removeDayChangeCallback(context)
+    }
+
+    override fun onReceive(context: Context, intent: Intent) {
+        LOGGER.i("onReceive: $intent")
+        super.onReceive(context, intent)
+
+        val wm: AppWidgetManager = AppWidgetManager.getInstance(context) ?: return
+
+        val provider = ComponentName(context, javaClass)
+        val widgetIds: IntArray = wm.getAppWidgetIds(provider)
+
+        val action: String? = intent.action
+        when (action) {
+            ACTION_NEXT_ALARM_CLOCK_CHANGED,
+            ACTION_DATE_CHANGED,
+            ACTION_LOCALE_CHANGED,
+            ACTION_SCREEN_ON,
+            ACTION_TIME_CHANGED,
+            ACTION_TIMEZONE_CHANGED,
+            AlarmStateManager.ACTION_ALARM_CHANGED,
+            ACTION_ON_DAY_CHANGE,
+            DataModel.ACTION_WORLD_CITIES_CHANGED -> widgetIds.forEach { widgetId ->
+                relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId))
+            }
+        }
+
+        val dm = DataModel.dataModel
+        dm.updateWidgetCount(javaClass, widgetIds.size, R.string.category_digital_widget)
+
+        if (widgetIds.size > 0) {
+            updateDayChangeCallback(context)
+        }
+    }
+
+    /**
+     * Called when widgets must provide remote views.
+     */
+    override fun onUpdate(context: Context, wm: AppWidgetManager, widgetIds: IntArray) {
+        super.onUpdate(context, wm, widgetIds)
+
+        widgetIds.forEach { widgetId ->
+            relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId))
+        }
+    }
+
+    /**
+     * Called when the app widget changes sizes.
+     */
+    override fun onAppWidgetOptionsChanged(
+        context: Context,
+        wm: AppWidgetManager?,
+        widgetId: Int,
+        options: Bundle
+    ) {
+        super.onAppWidgetOptionsChanged(context, wm, widgetId, options)
+
+        // Scale the fonts of the clock to fit inside the new size
+        relayoutWidget(context, AppWidgetManager.getInstance(context), widgetId, options)
+    }
+
+    /**
+     * Remove the existing day-change callback if it is not needed (no selected cities exist).
+     * Add the day-change callback if it is needed (selected cities exist).
+     */
+    private fun updateDayChangeCallback(context: Context) {
+        val dm = DataModel.dataModel
+        val selectedCities = dm.selectedCities
+        val showHomeClock = dm.showHomeClock
+        if (selectedCities.isEmpty() && !showHomeClock) {
+            // Remove the existing day-change callback.
+            removeDayChangeCallback(context)
+            return
+        }
+
+        // Look up the time at which the next day change occurs across all timezones.
+        val zones: MutableSet<TimeZone> = ArraySet(selectedCities.size + 2)
+        zones.add(TimeZone.getDefault())
+        if (showHomeClock) {
+            zones.add(dm.homeCity.timeZone)
+        }
+        selectedCities.forEach { city ->
+            zones.add(city.timeZone)
+        }
+        val nextDay = Utils.getNextDay(Date(), zones)
+
+        // Schedule the next day-change callback; at least one city is displayed.
+        val pi: PendingIntent =
+                PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_UPDATE_CURRENT)
+        getAlarmManager(context).setExact(AlarmManager.RTC, nextDay.time, pi)
+    }
+
+    /**
+     * Remove the existing day-change callback.
+     */
+    private fun removeDayChangeCallback(context: Context) {
+        val pi: PendingIntent? =
+                PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_NO_CREATE)
+        if (pi != null) {
+            getAlarmManager(context).cancel(pi)
+            pi.cancel()
+        }
+    }
+
+    /**
+     * This class stores the target size of the widget as well as the measured size using a given
+     * clock font size. All other fonts and icons are scaled proportional to the clock font.
+     */
+    private class Sizes(
+        val mTargetWidthPx: Int,
+        val mTargetHeightPx: Int,
+        val largestClockFontSizePx: Int
+    ) {
+        val smallestClockFontSizePx = 1
+        var mIconBitmap: Bitmap? = null
+
+        var mMeasuredWidthPx = 0
+        var mMeasuredHeightPx = 0
+        var mMeasuredTextClockWidthPx = 0
+        var mMeasuredTextClockHeightPx = 0
+
+        /** The size of the font to use on the date / next alarm time fields.  */
+        var mFontSizePx = 0
+
+        /** The size of the font to use on the clock field.  */
+        var mClockFontSizePx = 0
+
+        var mIconFontSizePx = 0
+        var mIconPaddingPx = 0
+
+        var clockFontSizePx: Int
+            get() = mClockFontSizePx
+            set(clockFontSizePx) {
+                mClockFontSizePx = clockFontSizePx
+                mFontSizePx = Math.max(1, Math.round(clockFontSizePx / 7.5f))
+                mIconFontSizePx = (mFontSizePx * 1.4f).toInt()
+                mIconPaddingPx = mFontSizePx / 3
+            }
+
+        /**
+         * @return the amount of widget height available to the world cities list
+         */
+        val listHeight: Int
+            get() = mTargetHeightPx - mMeasuredHeightPx
+
+        fun hasViolations(): Boolean {
+            return mMeasuredWidthPx > mTargetWidthPx || mMeasuredHeightPx > mTargetHeightPx
+        }
+
+        fun newSize(): Sizes {
+            return Sizes(mTargetWidthPx, mTargetHeightPx, largestClockFontSizePx)
+        }
+
+        override fun toString(): String {
+            val builder = StringBuilder(1000)
+            builder.append("\n")
+            append(builder, "Target dimensions: %dpx x %dpx\n", mTargetWidthPx, mTargetHeightPx)
+            append(builder, "Last valid widget container measurement: %dpx x %dpx\n",
+                    mMeasuredWidthPx, mMeasuredHeightPx)
+            append(builder, "Last text clock measurement: %dpx x %dpx\n",
+                    mMeasuredTextClockWidthPx, mMeasuredTextClockHeightPx)
+            if (mMeasuredWidthPx > mTargetWidthPx) {
+                append(builder, "Measured width %dpx exceeded widget width %dpx\n",
+                        mMeasuredWidthPx, mTargetWidthPx)
+            }
+            if (mMeasuredHeightPx > mTargetHeightPx) {
+                append(builder, "Measured height %dpx exceeded widget height %dpx\n",
+                        mMeasuredHeightPx, mTargetHeightPx)
+            }
+            append(builder, "Clock font: %dpx\n", mClockFontSizePx)
+            return builder.toString()
+        }
+
+        companion object {
+            private fun append(builder: StringBuilder, format: String, vararg args: Any) {
+                builder.append(String.format(Locale.ENGLISH, format, *args))
+            }
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("DigitalWidgetProvider")
+
+        /**
+         * Intent action used for refreshing a world city display when any of them changes days or when
+         * the default TimeZone changes days. This affects the widget display because the day-of-week is
+         * only visible when the world city day-of-week differs from the default TimeZone's day-of-week.
+         */
+        private const val ACTION_ON_DAY_CHANGE = "com.android.deskclock.ON_DAY_CHANGE"
+
+        /** Intent used to deliver the [.ACTION_ON_DAY_CHANGE] callback.  */
+        private val DAY_CHANGE_INTENT: Intent = Intent(ACTION_ON_DAY_CHANGE)
+
+        /**
+         * Compute optimal font and icon sizes offscreen for both portrait and landscape orientations
+         * using the last known widget size and apply them to the widget.
+         */
+        private fun relayoutWidget(
+            context: Context,
+            wm: AppWidgetManager,
+            widgetId: Int,
+            options: Bundle
+        ) {
+            val portrait: RemoteViews = relayoutWidget(context, wm, widgetId, options, true)
+            val landscape: RemoteViews = relayoutWidget(context, wm, widgetId, options, false)
+            val widget = RemoteViews(landscape, portrait)
+            wm.updateAppWidget(widgetId, widget)
+            wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list)
+        }
+
+        /**
+         * Compute optimal font and icon sizes offscreen for the given orientation.
+         */
+        private fun relayoutWidget(
+            context: Context,
+            wm: AppWidgetManager,
+            widgetId: Int,
+            options: Bundle?,
+            portrait: Boolean
+        ): RemoteViews {
+            // Create a remote view for the digital clock.
+            val packageName: String = context.getPackageName()
+            val rv = RemoteViews(packageName, R.layout.digital_widget)
+
+            // Tapping on the widget opens the app (if not on the lock screen).
+            if (Utils.isWidgetClickable(wm, widgetId)) {
+                val openApp = Intent(context, DeskClock::class.java)
+                val pi: PendingIntent = PendingIntent.getActivity(context, 0, openApp, 0)
+                rv.setOnClickPendingIntent(R.id.digital_widget, pi)
+            }
+
+            // Configure child views of the remote view.
+            val dateFormat: CharSequence = getDateFormat(context)
+            rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat)
+            rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat)
+
+            val nextAlarmTime: String? = Utils.getNextAlarm(context)
+            if (TextUtils.isEmpty(nextAlarmTime)) {
+                rv.setViewVisibility(R.id.nextAlarm, GONE)
+                rv.setViewVisibility(R.id.nextAlarmIcon, GONE)
+            } else {
+                rv.setTextViewText(R.id.nextAlarm, nextAlarmTime)
+                rv.setViewVisibility(R.id.nextAlarm, VISIBLE)
+                rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE)
+            }
+
+            val options = options ?: wm.getAppWidgetOptions(widgetId)
+
+            // Fetch the widget size selected by the user.
+            val resources: Resources = context.getResources()
+            val density: Float = resources.getDisplayMetrics().density
+            val minWidthPx = (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH)).toInt()
+            val minHeightPx = (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT)).toInt()
+            val maxWidthPx = (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH)).toInt()
+            val maxHeightPx = (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT)).toInt()
+            val targetWidthPx = if (portrait) minWidthPx else maxWidthPx
+            val targetHeightPx = if (portrait) maxHeightPx else minHeightPx
+            val largestClockFontSizePx: Int =
+                    resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size)
+
+            // Create a size template that describes the widget bounds.
+            val template = Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx)
+
+            // Compute optimal font sizes and icon sizes to fit within the widget bounds.
+            val sizes = optimizeSizes(context, template, nextAlarmTime)
+            if (LOGGER.isVerboseLoggable) {
+                LOGGER.v(sizes.toString())
+            }
+
+            // Apply the computed sizes to the remote views.
+            rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap)
+            rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx.toFloat())
+            rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx.toFloat())
+            rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx.toFloat())
+
+            val smallestWorldCityListSizePx: Int =
+                    resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size)
+            if (sizes.listHeight <= smallestWorldCityListSizePx) {
+                // Insufficient space; hide the world city list.
+                rv.setViewVisibility(R.id.world_city_list, GONE)
+            } else {
+                // Set an adapter on the world city list. That adapter connects to a Service via intent.
+                val intent = Intent(context, DigitalAppWidgetCityService::class.java)
+                intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
+                intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)))
+                rv.setRemoteAdapter(R.id.world_city_list, intent)
+                rv.setViewVisibility(R.id.world_city_list, VISIBLE)
+
+                // Tapping on the widget opens the city selection activity (if not on the lock screen).
+                if (Utils.isWidgetClickable(wm, widgetId)) {
+                    val selectCity = Intent(context, CitySelectionActivity::class.java)
+                    val pi: PendingIntent = PendingIntent.getActivity(context, 0, selectCity, 0)
+                    rv.setPendingIntentTemplate(R.id.world_city_list, pi)
+                }
+            }
+
+            return rv
+        }
+
+        /**
+         * Inflate an offscreen copy of the widget views. Binary search through the range of sizes
+         * until the optimal sizes that fit within the widget bounds are located.
+         */
+        private fun optimizeSizes(
+            context: Context,
+            template: Sizes,
+            nextAlarmTime: String?
+        ): Sizes {
+            // Inflate a test layout to compute sizes at different font sizes.
+            val inflater: LayoutInflater = LayoutInflater.from(context)
+            @SuppressLint("InflateParams") val sizer: View =
+                    inflater.inflate(R.layout.digital_widget_sizer, null /* root */)
+
+            // Configure the date to display the current date string.
+            val dateFormat: CharSequence = getDateFormat(context)
+            val date: TextClock = sizer.findViewById(R.id.date) as TextClock
+            date.setFormat12Hour(dateFormat)
+            date.setFormat24Hour(dateFormat)
+
+            // Configure the next alarm views to display the next alarm time or be gone.
+            val nextAlarmIcon: TextView = sizer.findViewById(R.id.nextAlarmIcon) as TextView
+            val nextAlarm: TextView = sizer.findViewById(R.id.nextAlarm) as TextView
+            if (TextUtils.isEmpty(nextAlarmTime)) {
+                nextAlarm.setVisibility(GONE)
+                nextAlarmIcon.setVisibility(GONE)
+            } else {
+                nextAlarm.setText(nextAlarmTime)
+                nextAlarm.setVisibility(VISIBLE)
+                nextAlarmIcon.setVisibility(VISIBLE)
+                nextAlarmIcon.setTypeface(UiDataModel.uiDataModel.alarmIconTypeface)
+            }
+
+            // Measure the widget at the largest possible size.
+            var high = measure(template, template.largestClockFontSizePx, sizer)
+            if (!high.hasViolations()) {
+                return high
+            }
+
+            // Measure the widget at the smallest possible size.
+            var low = measure(template, template.smallestClockFontSizePx, sizer)
+            if (low.hasViolations()) {
+                return low
+            }
+
+            // Binary search between the smallest and largest sizes until an optimum size is found.
+            while (low.clockFontSizePx != high.clockFontSizePx) {
+                val midFontSize: Int = (low.clockFontSizePx + high.clockFontSizePx) / 2
+                if (midFontSize == low.clockFontSizePx) {
+                    return low
+                }
+                val midSize = measure(template, midFontSize, sizer)
+                if (midSize.hasViolations()) {
+                    high = midSize
+                } else {
+                    low = midSize
+                }
+            }
+
+            return low
+        }
+
+        private fun getAlarmManager(context: Context): AlarmManager {
+            return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+        }
+
+        /**
+         * Compute all font and icon sizes based on the given `clockFontSize` and apply them to
+         * the offscreen `sizer` view. Measure the `sizer` view and return the resulting
+         * size measurements.
+         */
+        private fun measure(template: Sizes, clockFontSize: Int, sizer: View): Sizes {
+            // Create a copy of the given template sizes.
+            val measuredSizes = template.newSize()
+
+            // Configure the clock to display the widest time string.
+            val date: TextClock = sizer.findViewById(R.id.date) as TextClock
+            val clock: TextClock = sizer.findViewById(R.id.clock) as TextClock
+            val nextAlarm: TextView = sizer.findViewById(R.id.nextAlarm) as TextView
+            val nextAlarmIcon: TextView = sizer.findViewById(R.id.nextAlarmIcon) as TextView
+
+            // Adjust the font sizes.
+            measuredSizes.clockFontSizePx = clockFontSize
+            clock.setText(getLongestTimeString(clock))
+            clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx.toFloat())
+            date.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx.toFloat())
+            nextAlarm.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx.toFloat())
+            nextAlarmIcon.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mIconFontSizePx.toFloat())
+            nextAlarmIcon
+                    .setPadding(measuredSizes.mIconPaddingPx, 0, measuredSizes.mIconPaddingPx, 0)
+
+            // Measure and layout the sizer.
+            val widthSize: Int = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx)
+            val heightSize: Int = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx)
+            val widthMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED)
+            val heightMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED)
+            sizer.measure(widthMeasureSpec, heightMeasureSpec)
+            sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight())
+
+            // Copy the measurements into the result object.
+            measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth()
+            measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight()
+            measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth()
+            measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight()
+
+            // If an alarm icon is required, generate one from the TextView with the special font.
+            if (nextAlarmIcon.getVisibility() == VISIBLE) {
+                measuredSizes.mIconBitmap = Utils.createBitmap(nextAlarmIcon)
+            }
+
+            return measuredSizes
+        }
+
+        /**
+         * @return "11:59" or "23:59" in the current locale
+         */
+        private fun getLongestTimeString(clock: TextClock): CharSequence {
+            val format: CharSequence = if (clock.is24HourModeEnabled()) {
+                clock.getFormat24Hour()
+            } else {
+                clock.getFormat12Hour()
+            }
+            val longestPMTime = Calendar.getInstance()
+            longestPMTime[0, 0, 0, 23] = 59
+            return DateFormat.format(format, longestPMTime)
+        }
+
+        /**
+         * @return the locale-specific date pattern
+         */
+        private fun getDateFormat(context: Context): String {
+            val locale = Locale.getDefault()
+            val skeleton: String = context.getString(R.string.abbrev_wday_month_day_no_year)
+            return DateFormat.getBestDateTimePattern(locale, skeleton)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/WidgetUtils.java b/src/com/android/alarmclock/WidgetUtils.java
deleted file mode 100644
index be7bf33..0000000
--- a/src/com/android/alarmclock/WidgetUtils.java
+++ /dev/null
@@ -1,95 +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.alarmclock;
-
-import android.appwidget.AppWidgetManager;
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.Bundle;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-public final class WidgetUtils {
-
-    private WidgetUtils() {}
-
-    // Calculate the scale factor of the fonts in the widget
-    public static float getScaleRatio(Context context, Bundle options, int id, int cityCount) {
-        if (options == null) {
-            AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
-            if (widgetManager == null) {
-                // no manager , do no scaling
-                return 1f;
-            }
-            options = widgetManager.getAppWidgetOptions(id);
-        }
-        if (options != null) {
-            int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
-            if (minWidth == 0) {
-                // No data , do no scaling
-                return 1f;
-            }
-            final Resources res = context.getResources();
-            float density = res.getDisplayMetrics().density;
-            float ratio = (density * minWidth) / res.getDimension(R.dimen.min_digital_widget_width);
-            ratio = Math.min(ratio, getHeightScaleRatio(context, options, id));
-            ratio *= .83f;
-
-            if (cityCount > 0) {
-                return (ratio > 1f) ? 1f : ratio;
-            }
-
-            ratio = Math.min(ratio, 1.6f);
-            if (Utils.isPortrait(context)) {
-                ratio = Math.max(ratio, .71f);
-            }
-            else {
-                ratio = Math.max(ratio, .45f);
-            }
-            return ratio;
-        }
-        return 1f;
-    }
-
-    // Calculate the scale factor of the fonts in the list of  the widget using the widget height
-    private static float getHeightScaleRatio(Context context, Bundle options, int id) {
-        if (options == null) {
-            AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
-            if (widgetManager == null) {
-                // no manager , do no scaling
-                return 1f;
-            }
-            options = widgetManager.getAppWidgetOptions(id);
-        }
-        if (options != null) {
-            int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
-            if (minHeight == 0) {
-                // No data , do no scaling
-                return 1f;
-            }
-            final Resources res = context.getResources();
-            float density = res.getDisplayMetrics().density;
-            float ratio = density * minHeight / res.getDimension(R.dimen.min_digital_widget_height);
-            if (Utils.isPortrait(context)) {
-                return ratio * 1.75f;
-            }
-            return ratio;
-        }
-        return 1;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/WidgetUtils.kt b/src/com/android/alarmclock/WidgetUtils.kt
new file mode 100644
index 0000000..c8ff7aa
--- /dev/null
+++ b/src/com/android/alarmclock/WidgetUtils.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.alarmclock
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.res.Resources
+import android.os.Bundle
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+object WidgetUtils {
+    // Calculate the scale factor of the fonts in the widget
+    fun getScaleRatio(context: Context, options: Bundle?, id: Int, cityCount: Int): Float {
+        var options: Bundle? = options
+        if (options == null) {
+            val widgetManager: AppWidgetManager =
+                    AppWidgetManager.getInstance(context) // no manager , do no scaling
+                    ?: return 1f
+            options = widgetManager.getAppWidgetOptions(id)
+        }
+        options?.let {
+            val minWidth: Int = it.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
+            if (minWidth == 0) {
+                // No data , do no scaling
+                return 1f
+            }
+            val res: Resources = context.getResources()
+            val density: Float = res.getDisplayMetrics().density
+            var ratio: Float =
+                    density * minWidth / res.getDimension(R.dimen.min_digital_widget_width)
+            ratio = Math.min(ratio, getHeightScaleRatio(context, it))
+            ratio *= .83f
+
+            if (cityCount > 0) {
+                return if (ratio > 1f) 1f else ratio
+            }
+
+            ratio = Math.min(ratio, 1.6f)
+            ratio = if (Utils.isPortrait(context)) {
+                Math.max(ratio, .71f)
+            } else {
+                Math.max(ratio, .45f)
+            }
+            return ratio
+        }
+        return 1f
+    }
+
+    // Calculate the scale factor of the fonts in the list of the widget using the widget height
+    private fun getHeightScaleRatio(context: Context, options: Bundle): Float {
+        val minHeight: Int = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
+        if (minHeight == 0) {
+            // No data , do no scaling
+            return 1f
+        }
+        val res: Resources = context.getResources()
+        val density: Float = res.getDisplayMetrics().density
+        val ratio: Float = density * minHeight / res.getDimension(R.dimen.min_digital_widget_height)
+        return if (Utils.isPortrait(context)) { ratio * 1.75f } else ratio
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmAlertWakeLock.java b/src/com/android/deskclock/AlarmAlertWakeLock.java
deleted file mode 100644
index 6af687e..0000000
--- a/src/com/android/deskclock/AlarmAlertWakeLock.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.os.PowerManager;
-
-/**
- * Utility class to hold wake lock in app.
- */
-public class AlarmAlertWakeLock {
-
-    private static final String TAG = "AlarmAlertWakeLock";
-
-    private static PowerManager.WakeLock sCpuWakeLock;
-
-    public static PowerManager.WakeLock createPartialWakeLock(Context context) {
-        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-        return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
-    }
-
-    public static void acquireCpuWakeLock(Context context) {
-        if (sCpuWakeLock != null) {
-            return;
-        }
-
-        sCpuWakeLock = createPartialWakeLock(context);
-        sCpuWakeLock.acquire();
-    }
-
-    public static void acquireScreenCpuWakeLock(Context context) {
-        if (sCpuWakeLock != null) {
-            return;
-        }
-        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-        sCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK
-                | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, TAG);
-        sCpuWakeLock.acquire();
-    }
-
-    public static void releaseCpuLock() {
-        if (sCpuWakeLock != null) {
-            sCpuWakeLock.release();
-            sCpuWakeLock = null;
-        }
-    }
-}
diff --git a/src/com/android/deskclock/AlarmAlertWakeLock.kt b/src/com/android/deskclock/AlarmAlertWakeLock.kt
new file mode 100644
index 0000000..ca97ed3
--- /dev/null
+++ b/src/com/android/deskclock/AlarmAlertWakeLock.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.PowerManager
+import android.os.PowerManager.WakeLock
+
+/**
+ * Utility class to hold wake lock in app.
+ */
+object AlarmAlertWakeLock {
+    private const val TAG = "AlarmAlertWakeLock"
+
+    private var sCpuWakeLock: WakeLock? = null
+
+    @JvmStatic
+    fun createPartialWakeLock(context: Context): WakeLock {
+        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+        return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
+    }
+
+    @JvmStatic
+    fun acquireCpuWakeLock(context: Context) {
+        if (sCpuWakeLock != null) {
+            return
+        }
+
+        sCpuWakeLock = createPartialWakeLock(context)
+        sCpuWakeLock!!.acquire()
+    }
+
+    @JvmStatic
+    fun acquireScreenCpuWakeLock(context: Context) {
+        if (sCpuWakeLock != null) {
+            return
+        }
+        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+        sCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK
+                or PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.ON_AFTER_RELEASE, TAG)
+        sCpuWakeLock!!.acquire()
+    }
+
+    @JvmStatic
+    fun releaseCpuLock() {
+        if (sCpuWakeLock != null) {
+            sCpuWakeLock!!.release()
+            sCpuWakeLock = null
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmClockFragment.java b/src/com/android/deskclock/AlarmClockFragment.java
deleted file mode 100644
index 5c02d03..0000000
--- a/src/com/android/deskclock/AlarmClockFragment.java
+++ /dev/null
@@ -1,447 +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.app.LoaderManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.Loader;
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-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.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.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.widget.EmptyViewController;
-import com.android.deskclock.widget.toast.SnackbarManager;
-import com.android.deskclock.widget.toast.ToastManager;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.ALARMS;
-
-/**
- * A fragment that displays a list of alarm time and allows interaction with them.
- */
-public final class AlarmClockFragment extends DeskClockFragment implements
-        LoaderManager.LoaderCallbacks<Cursor>,
-        ScrollHandler,
-        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.
-    public static final String ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new";
-
-    // This extra is used when receiving an intent to scroll to specific alarm. If alarm
-    // can not be found, and toast message will pop up that the alarm has be deleted.
-    public static final String SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm";
-
-    private static final String KEY_EXPANDED_ID = "expandedId";
-
-    // Updates "Today/Tomorrow" in the UI when midnight passes.
-    private final Runnable mMidnightUpdater = new MidnightRunnable();
-
-    // Views
-    private ViewGroup mMainLayout;
-    private RecyclerView mRecyclerView;
-
-    // Data
-    private Loader mCursorLoader;
-    private long mScrollToAlarmId = Alarm.INVALID_ID;
-    private long mExpandedAlarmId = Alarm.INVALID_ID;
-    private long mCurrentUpdateToken;
-
-    // Controllers
-    private ItemAdapter<AlarmItemHolder> mItemAdapter;
-    private AlarmUpdateHandler mAlarmUpdateHandler;
-    private EmptyViewController mEmptyViewController;
-    private AlarmTimeClickHandler mAlarmTimeClickHandler;
-    private LinearLayoutManager mLayoutManager;
-
-    /**
-     * The public no-arg constructor required by all fragments.
-     */
-    public AlarmClockFragment() {
-        super(ALARMS);
-    }
-
-    @Override
-    public void onCreate(Bundle savedState) {
-        super.onCreate(savedState);
-        mCursorLoader = getLoaderManager().initLoader(0, null, this);
-        if (savedState != null) {
-            mExpandedAlarmId = savedState.getLong(KEY_EXPANDED_ID, Alarm.INVALID_ID);
-        }
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
-        // Inflate the layout for this fragment
-        final View v = inflater.inflate(R.layout.alarm_clock, container, false);
-        final Context context = getActivity();
-
-        mRecyclerView = (RecyclerView) v.findViewById(R.id.alarms_recycler_view);
-        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);
-        final TextView emptyView = (TextView) v.findViewById(R.id.alarms_empty_view);
-        final Drawable noAlarms = Utils.getVectorDrawable(context, R.drawable.ic_noalarms);
-        emptyView.setCompoundDrawablesWithIntrinsicBounds(null, noAlarms, null, null);
-        mEmptyViewController = new EmptyViewController(mMainLayout, mRecyclerView, emptyView);
-        mAlarmTimeClickHandler = new AlarmTimeClickHandler(this, savedState, mAlarmUpdateHandler,
-                this);
-
-        mItemAdapter = new ItemAdapter<>();
-        mItemAdapter.setHasStableIds();
-        mItemAdapter.withViewTypes(new CollapsedAlarmViewHolder.Factory(inflater),
-                null, CollapsedAlarmViewHolder.VIEW_TYPE);
-        mItemAdapter.withViewTypes(new ExpandedAlarmViewHolder.Factory(context),
-                null, ExpandedAlarmViewHolder.VIEW_TYPE);
-        mItemAdapter.setOnItemChangedListener(new ItemAdapter.OnItemChangedListener() {
-            @Override
-            public void onItemChanged(ItemAdapter.ItemHolder<?> holder) {
-                if (((AlarmItemHolder) holder).isExpanded()) {
-                    if (mExpandedAlarmId != holder.itemId) {
-                        // Collapse the prior expanded alarm.
-                        final AlarmItemHolder aih = mItemAdapter.findItemById(mExpandedAlarmId);
-                        if (aih != null) {
-                            aih.collapse();
-                        }
-                        // Record the freshly expanded alarm.
-                        mExpandedAlarmId = holder.itemId;
-                        final RecyclerView.ViewHolder viewHolder =
-                                mRecyclerView.findViewHolderForItemId(mExpandedAlarmId);
-                        if (viewHolder != null) {
-                            smoothScrollTo(viewHolder.getAdapterPosition());
-                        }
-                    }
-                } else if (mExpandedAlarmId == holder.itemId) {
-                    // The expanded alarm is now collapsed so update the tracking id.
-                    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;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        if (!isTabSelected()) {
-            TimePickerDialogFragment.removeTimeEditDialog(getFragmentManager());
-        }
-    }
-
-    @Override
-    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)) {
-                // An external app asked us to create a blank alarm.
-                startCreatingAlarm();
-            }
-
-            // Remove the CREATE_NEW extra now that we've processed it.
-            intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA);
-        } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
-            UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
-            long alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID);
-            if (alarmId != Alarm.INVALID_ID) {
-                setSmoothScrollStableId(alarmId);
-                if (mCursorLoader != null && mCursorLoader.isStarted()) {
-                    // We need to force a reload here to make sure we have the latest view
-                    // of the data to scroll to.
-                    mCursorLoader.forceLoad();
-                }
-            }
-
-            // Remove the SCROLL_TO_ALARM extra now that we've processed it.
-            intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA);
-        }
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-        UiDataModel.getUiDataModel().removePeriodicCallback(mMidnightUpdater);
-
-        // When the user places the app in the background by pressing "home",
-        // dismiss the toast bar. However, since there is no way to determine if
-        // home was pressed, just dismiss any existing toast bar when restarting
-        // the app.
-        mAlarmUpdateHandler.hideUndoBar();
-    }
-
-    @Override
-    public void smoothScrollTo(int position) {
-        mLayoutManager.scrollToPositionWithOffset(position, 0);
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        mAlarmTimeClickHandler.saveInstance(outState);
-        outState.putLong(KEY_EXPANDED_ID, mExpandedAlarmId);
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        ToastManager.cancelToast();
-    }
-
-    public void setLabel(Alarm alarm, String label) {
-        alarm.label = label;
-        mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true);
-    }
-
-    @Override
-    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-        return Alarm.getAlarmsCursorLoader(getActivity());
-    }
-
-    @Override
-    public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) {
-        final List<AlarmItemHolder> itemHolders = new ArrayList<>(data.getCount());
-        for (data.moveToFirst(); !data.isAfterLast(); data.moveToNext()) {
-            final Alarm alarm = new Alarm(data);
-            final AlarmInstance alarmInstance = alarm.canPreemptivelyDismiss()
-                    ? new AlarmInstance(data, true /* joinedTable */) : null;
-            final AlarmItemHolder itemHolder =
-                    new AlarmItemHolder(alarm, alarmInstance, mAlarmTimeClickHandler);
-            itemHolders.add(itemHolder);
-        }
-        setAdapterItems(itemHolders, SystemClock.elapsedRealtime());
-    }
-
-    /**
-     * Updates the adapters items, deferring the update until the current animation is finished or
-     * if no animation is running then the listener will be automatically be invoked immediately.
-     *
-     * @param items       the new list of {@link AlarmItemHolder} to use
-     * @param updateToken a monotonically increasing value used to preserve ordering of deferred
-     *                    updates
-     */
-    private void setAdapterItems(final List<AlarmItemHolder> items, final long updateToken) {
-        if (updateToken < mCurrentUpdateToken) {
-            LogUtils.v("Ignoring adapter update: %d < %d", updateToken, mCurrentUpdateToken);
-            return;
-        }
-
-        if (mRecyclerView.getItemAnimator().isRunning()) {
-            // RecyclerView is currently animating -> defer update.
-            mRecyclerView.getItemAnimator().isRunning(
-                    new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
-                @Override
-                public void onAnimationsFinished() {
-                    setAdapterItems(items, updateToken);
-                }
-            });
-        } else if (mRecyclerView.isComputingLayout()) {
-            // RecyclerView is currently computing a layout -> defer update.
-            mRecyclerView.post(new Runnable() {
-                @Override
-                public void run() {
-                    setAdapterItems(items, updateToken);
-                }
-            });
-        } else {
-            mCurrentUpdateToken = updateToken;
-            mItemAdapter.setItems(items);
-
-            // Show or hide the empty view as appropriate.
-            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) {
-                final AlarmItemHolder aih = mItemAdapter.findItemById(mExpandedAlarmId);
-                if (aih != null) {
-                    mAlarmTimeClickHandler.setSelectedAlarm(aih.item);
-                    aih.expand();
-                } else {
-                    mAlarmTimeClickHandler.setSelectedAlarm(null);
-                    mExpandedAlarmId = Alarm.INVALID_ID;
-                }
-            }
-
-            // Scroll to the selected alarm.
-            if (mScrollToAlarmId != Alarm.INVALID_ID) {
-                scrollToAlarm(mScrollToAlarmId);
-                setSmoothScrollStableId(Alarm.INVALID_ID);
-            }
-        }
-    }
-
-    /**
-     * @param alarmId identifies the alarm to be displayed
-     */
-    private void scrollToAlarm(long alarmId) {
-        final int alarmCount = mItemAdapter.getItemCount();
-        int alarmPosition = -1;
-        for (int i = 0; i < alarmCount; i++) {
-            long id = mItemAdapter.getItemId(i);
-            if (id == alarmId) {
-                alarmPosition = i;
-                break;
-            }
-        }
-
-        if (alarmPosition >= 0) {
-            mItemAdapter.findItemById(alarmId).expand();
-            smoothScrollTo(alarmPosition);
-        } else {
-            // Trying to display a deleted alarm should only happen from a missed notification for
-            // an alarm that has been marked deleted after use.
-            SnackbarManager.show(Snackbar.make(mMainLayout, R.string
-                    .missed_alarm_has_been_deleted, Snackbar.LENGTH_LONG));
-        }
-    }
-
-    @Override
-    public void onLoaderReset(Loader<Cursor> cursorLoader) {
-    }
-
-    @Override
-    public void setSmoothScrollStableId(long stableId) {
-        mScrollToAlarmId = stableId;
-    }
-
-    @Override
-    public void onFabClick(@NonNull ImageView fab) {
-        mAlarmUpdateHandler.hideUndoBar();
-        startCreatingAlarm();
-    }
-
-    @Override
-    public void onUpdateFab(@NonNull ImageView fab) {
-        fab.setVisibility(View.VISIBLE);
-        fab.setImageResource(R.drawable.ic_add_white_24dp);
-        fab.setContentDescription(fab.getResources().getString(R.string.button_alarms));
-    }
-
-    @Override
-    public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
-        left.setVisibility(View.INVISIBLE);
-        right.setVisibility(View.INVISIBLE);
-    }
-
-    private void startCreatingAlarm() {
-        // Clear the currently selected alarm.
-        mAlarmTimeClickHandler.setSelectedAlarm(null);
-        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);
-    }
-
-    /**
-     * 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.
-     */
-    private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
-            implements View.OnLayoutChangeListener {
-        @Override
-        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-            setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView));
-        }
-
-        @Override
-        public void onLayoutChange(View v, int left, int top, int right, int bottom,
-                int oldLeft, int oldTop, int oldRight, int oldBottom) {
-            setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView));
-        }
-    }
-
-    /**
-     * This runnable executes at midnight and refreshes the display of all alarms. Collapsed alarms
-     * that do no repeat will have their "Tomorrow" strings updated to say "Today".
-     */
-    private final class MidnightRunnable implements Runnable {
-        @Override
-        public void run() {
-            mItemAdapter.notifyDataSetChanged();
-        }
-    }
-}
diff --git a/src/com/android/deskclock/AlarmClockFragment.kt b/src/com/android/deskclock/AlarmClockFragment.kt
new file mode 100644
index 0000000..892794e
--- /dev/null
+++ b/src/com/android/deskclock/AlarmClockFragment.kt
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.database.Cursor
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.loader.app.LoaderManager.LoaderCallbacks
+import androidx.loader.content.Loader
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.ItemAdapter.OnItemChangedListener
+import com.android.deskclock.alarms.AlarmTimeClickHandler
+import com.android.deskclock.alarms.AlarmUpdateHandler
+import com.android.deskclock.alarms.ScrollHandler
+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.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.widget.EmptyViewController
+import com.android.deskclock.widget.toast.SnackbarManager
+import com.android.deskclock.widget.toast.ToastManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import kotlin.math.max
+
+/**
+ * A fragment that displays a list of alarm time and allows interaction with them.
+ */
+class AlarmClockFragment : DeskClockFragment(UiDataModel.Tab.ALARMS),
+        LoaderCallbacks<Cursor>, ScrollHandler, TimePickerDialogFragment.OnTimeSetListener {
+    // Updates "Today/Tomorrow" in the UI when midnight passes.
+    private val mMidnightUpdater: Runnable = MidnightRunnable()
+
+    // Views
+    private lateinit var mMainLayout: ViewGroup
+    private lateinit var mRecyclerView: RecyclerView
+
+    // Data
+    private var mCursorLoader: Loader<*>? = null
+    private var mScrollToAlarmId = Alarm.INVALID_ID
+    private var mExpandedAlarmId = Alarm.INVALID_ID
+    private var mCurrentUpdateToken: Long = 0
+
+    // Controllers
+    private lateinit var mItemAdapter: ItemAdapter<AlarmItemHolder>
+    private lateinit var mAlarmUpdateHandler: AlarmUpdateHandler
+    private lateinit var mEmptyViewController: EmptyViewController
+    private lateinit var mAlarmTimeClickHandler: AlarmTimeClickHandler
+    private lateinit var mLayoutManager: LinearLayoutManager
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        mCursorLoader = loaderManager.initLoader(0, Bundle.EMPTY, this)
+        savedState?.let {
+            mExpandedAlarmId = it.getLong(KEY_EXPANDED_ID, Alarm.INVALID_ID)
+        }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedState: Bundle?
+    ): View? {
+        // Inflate the layout for this fragment
+        val v = inflater.inflate(R.layout.alarm_clock, container, false)
+        val context: Context = requireActivity()
+
+        mRecyclerView = v.findViewById<View>(R.id.alarms_recycler_view) as RecyclerView
+        mLayoutManager = object : LinearLayoutManager(context) {
+            override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
+                val extraSpace: Int = super.getExtraLayoutSpace(state)
+                return if (state.willRunPredictiveAnimations()) {
+                    max(getHeight(), extraSpace)
+                } else extraSpace
+            }
+        }
+        mRecyclerView.setLayoutManager(mLayoutManager)
+        mMainLayout = v.findViewById<View>(R.id.main) as ViewGroup
+        mAlarmUpdateHandler = AlarmUpdateHandler(context, this, mMainLayout)
+        val emptyView = v.findViewById<View>(R.id.alarms_empty_view) as TextView
+        val noAlarms: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_noalarms)
+        emptyView.setCompoundDrawablesWithIntrinsicBounds(null, noAlarms, null, null)
+        mEmptyViewController = EmptyViewController(mMainLayout, mRecyclerView, emptyView)
+        mAlarmTimeClickHandler = AlarmTimeClickHandler(this, savedState, mAlarmUpdateHandler, this)
+
+        mItemAdapter = ItemAdapter()
+        mItemAdapter.setHasStableIds()
+        mItemAdapter.withViewTypes(CollapsedAlarmViewHolder.Factory(inflater),
+                null, CollapsedAlarmViewHolder.VIEW_TYPE)
+        mItemAdapter.withViewTypes(ExpandedAlarmViewHolder.Factory(context),
+                null, ExpandedAlarmViewHolder.VIEW_TYPE)
+        mItemAdapter.setOnItemChangedListener(object : OnItemChangedListener {
+            override fun onItemChanged(holder: ItemHolder<*>) {
+                if ((holder as AlarmItemHolder).isExpanded) {
+                    if (mExpandedAlarmId != holder.itemId) {
+                        // Collapse the prior expanded alarm.
+                        val aih = mItemAdapter.findItemById(mExpandedAlarmId)
+                        aih?.collapse()
+                        // Record the freshly expanded alarm.
+                        mExpandedAlarmId = holder.itemId
+                        val viewHolder: RecyclerView.ViewHolder? =
+                                mRecyclerView.findViewHolderForItemId(mExpandedAlarmId)
+                        viewHolder?.let {
+                            smoothScrollTo(viewHolder.getAdapterPosition())
+                        }
+                    }
+                } else if (mExpandedAlarmId == holder.itemId) {
+                    // The expanded alarm is now collapsed so update the tracking id.
+                    mExpandedAlarmId = Alarm.INVALID_ID
+                }
+            }
+
+            override fun onItemChanged(holder: ItemHolder<*>, payload: Any) {
+                /* No additional work to do */
+            }
+        })
+        val scrollPositionWatcher = ScrollPositionWatcher()
+        mRecyclerView.addOnLayoutChangeListener(scrollPositionWatcher)
+        mRecyclerView.addOnScrollListener(scrollPositionWatcher)
+        mRecyclerView.setAdapter(mItemAdapter)
+        val itemAnimator = ItemAnimator()
+        itemAnimator.setChangeDuration(300L)
+        itemAnimator.setMoveDuration(300L)
+        mRecyclerView.setItemAnimator(itemAnimator)
+        return v
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        if (!isTabSelected) {
+            TimePickerDialogFragment.removeTimeEditDialog(parentFragmentManager)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        // Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
+        // alarms when midnight passes.
+        UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
+
+        // Check if another app asked us to create a blank new alarm.
+        val intent = requireActivity().intent ?: return
+
+        if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+            if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
+                // An external app asked us to create a blank alarm.
+                startCreatingAlarm()
+            }
+
+            // Remove the CREATE_NEW extra now that we've processed it.
+            intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA)
+        } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+            val alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID)
+            if (alarmId != Alarm.INVALID_ID) {
+                setSmoothScrollStableId(alarmId)
+                if (mCursorLoader != null && mCursorLoader!!.isStarted) {
+                    // We need to force a reload here to make sure we have the latest view
+                    // of the data to scroll to.
+                    mCursorLoader!!.forceLoad()
+                }
+            }
+
+            // Remove the SCROLL_TO_ALARM extra now that we've processed it.
+            intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA)
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
+
+        // When the user places the app in the background by pressing "home",
+        // dismiss the toast bar. However, since there is no way to determine if
+        // home was pressed, just dismiss any existing toast bar when restarting
+        // the app.
+        mAlarmUpdateHandler.hideUndoBar()
+    }
+
+    override fun smoothScrollTo(position: Int) {
+        mLayoutManager.scrollToPositionWithOffset(position, 0)
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        mAlarmTimeClickHandler.saveInstance(outState)
+        outState.putLong(KEY_EXPANDED_ID, mExpandedAlarmId)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        ToastManager.cancelToast()
+    }
+
+    fun setLabel(alarm: Alarm, label: String?) {
+        alarm.label = label
+        mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+    }
+
+    override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
+        return Alarm.getAlarmsCursorLoader(requireActivity())
+    }
+
+    override fun onLoadFinished(cursorLoader: Loader<Cursor>, data: Cursor) {
+        val itemHolders: MutableList<AlarmItemHolder> = ArrayList(data.count)
+        data.moveToFirst()
+        while (!data.isAfterLast) {
+            val alarm = Alarm(data)
+            val alarmInstance = if (alarm.canPreemptivelyDismiss()) {
+                AlarmInstance(data, joinedTable = true)
+            } else {
+                null
+            }
+            val itemHolder = AlarmItemHolder(alarm, alarmInstance, mAlarmTimeClickHandler)
+            itemHolders.add(itemHolder)
+            data.moveToNext()
+        }
+        setAdapterItems(itemHolders, SystemClock.elapsedRealtime())
+    }
+
+    /**
+     * Updates the adapters items, deferring the update until the current animation is finished or
+     * if no animation is running then the listener will be automatically be invoked immediately.
+     *
+     * @param items the new list of [AlarmItemHolder] to use
+     * @param updateToken a monotonically increasing value used to preserve ordering of deferred
+     * updates
+     */
+    private fun setAdapterItems(items: List<AlarmItemHolder>, updateToken: Long) {
+        if (updateToken < mCurrentUpdateToken) {
+            LogUtils.v("Ignoring adapter update: %d < %d", updateToken, mCurrentUpdateToken)
+            return
+        }
+
+        if (mRecyclerView.getItemAnimator()!!.isRunning()) {
+            // RecyclerView is currently animating -> defer update.
+            mRecyclerView.getItemAnimator()!!.isRunning(
+                    object : RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {
+                        override fun onAnimationsFinished() {
+                            setAdapterItems(items, updateToken)
+                        }
+                    })
+        } else if (mRecyclerView.isComputingLayout()) {
+            // RecyclerView is currently computing a layout -> defer update.
+            mRecyclerView.post(Runnable { setAdapterItems(items, updateToken) })
+        } else {
+            mCurrentUpdateToken = updateToken
+            mItemAdapter.setItems(items)
+
+            // Show or hide the empty view as appropriate.
+            val 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) {
+                val aih = mItemAdapter.findItemById(mExpandedAlarmId)
+                if (aih != null) {
+                    mAlarmTimeClickHandler.setSelectedAlarm(aih.item)
+                    aih.expand()
+                } else {
+                    mAlarmTimeClickHandler.setSelectedAlarm(null)
+                    mExpandedAlarmId = Alarm.INVALID_ID
+                }
+            }
+
+            // Scroll to the selected alarm.
+            if (mScrollToAlarmId != Alarm.INVALID_ID) {
+                scrollToAlarm(mScrollToAlarmId)
+                setSmoothScrollStableId(Alarm.INVALID_ID)
+            }
+        }
+    }
+
+    /**
+     * @param alarmId identifies the alarm to be displayed
+     */
+    private fun scrollToAlarm(alarmId: Long) {
+        val alarmCount = mItemAdapter.itemCount
+        var alarmPosition = -1
+        for (i in 0 until alarmCount) {
+            val id = mItemAdapter.getItemId(i)
+            if (id == alarmId) {
+                alarmPosition = i
+                break
+            }
+        }
+
+        if (alarmPosition >= 0) {
+            mItemAdapter.findItemById(alarmId)?.expand()
+            smoothScrollTo(alarmPosition)
+        } else {
+            // Trying to display a deleted alarm should only happen from a missed notification for
+            // an alarm that has been marked deleted after use.
+            SnackbarManager.show(Snackbar.make(mMainLayout, R.string.missed_alarm_has_been_deleted,
+                    Snackbar.LENGTH_LONG))
+        }
+    }
+
+    override fun onLoaderReset(cursorLoader: Loader<Cursor>) {
+    }
+
+    override fun setSmoothScrollStableId(stableId: Long) {
+        mScrollToAlarmId = stableId
+    }
+
+    override fun onFabClick(fab: ImageView) {
+        mAlarmUpdateHandler.hideUndoBar()
+        startCreatingAlarm()
+    }
+
+    override fun onUpdateFab(fab: ImageView) {
+        fab.visibility = View.VISIBLE
+        fab.setImageResource(R.drawable.ic_add_white_24dp)
+        fab.contentDescription = fab.resources.getString(R.string.button_alarms)
+    }
+
+    override fun onUpdateFabButtons(left: Button, right: Button) {
+        left.visibility = View.INVISIBLE
+        right.visibility = View.INVISIBLE
+    }
+
+    private fun startCreatingAlarm() {
+        // Clear the currently selected alarm.
+        mAlarmTimeClickHandler.setSelectedAlarm(null)
+        TimePickerDialogFragment.show(this)
+    }
+
+    override fun onTimeSet(fragment: TimePickerDialogFragment?, hourOfDay: Int, minute: Int) {
+        mAlarmTimeClickHandler.onTimeSet(hourOfDay, minute)
+    }
+
+    fun removeItem(itemHolder: AlarmItemHolder) {
+        mItemAdapter.removeItem(itemHolder)
+    }
+
+    /**
+     * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls
+     * the recyclerview or when the size/position of elements within the recyclerview changes.
+     */
+    private inner class ScrollPositionWatcher
+        : RecyclerView.OnScrollListener(), OnLayoutChangeListener {
+        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
+        }
+
+        override fun onLayoutChange(
+            v: View,
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            oldLeft: Int,
+            oldTop: Int,
+            oldRight: Int,
+            oldBottom: Int
+        ) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
+        }
+    }
+
+    /**
+     * This runnable executes at midnight and refreshes the display of all alarms. Collapsed alarms
+     * that do no repeat will have their "Tomorrow" strings updated to say "Today".
+     */
+    private inner class MidnightRunnable : Runnable {
+        override fun run() {
+            mItemAdapter.notifyDataSetChanged()
+        }
+    }
+
+    companion object {
+        // 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.
+        const val ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new"
+
+        // This extra is used when receiving an intent to scroll to specific alarm. If alarm
+        // can not be found, and toast message will pop up that the alarm has be deleted.
+        const val SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm"
+
+        private const val KEY_EXPANDED_ID = "expandedId"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmInitReceiver.java b/src/com/android/deskclock/AlarmInitReceiver.java
deleted file mode 100644
index 8bd7cde..0000000
--- a/src/com/android/deskclock/AlarmInitReceiver.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.PowerManager.WakeLock;
-
-import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.controller.Controller;
-import com.android.deskclock.data.DataModel;
-
-public class AlarmInitReceiver extends BroadcastReceiver {
-
-    /**
-     * When running on N devices, we're interested in the boot completed event that is sent while
-     * the user is still locked, so that we can schedule alarms.
-     */
-    @SuppressLint("InlinedApi")
-    private static final String ACTION_BOOT_COMPLETED = Utils.isNOrLater()
-            ? Intent.ACTION_LOCKED_BOOT_COMPLETED : Intent.ACTION_BOOT_COMPLETED;
-
-    /**
-     * This receiver handles a variety of actions:
-     *
-     * <ul>
-     *     <li>Clean up backup data that was recently restored to this device on
-     *     ACTION_COMPLETE_RESTORE.</li>
-     *     <li>Reset timers and stopwatch on ACTION_BOOT_COMPLETED</li>
-     *     <li>Fix alarm states on ACTION_BOOT_COMPLETED, TIME_SET, TIMEZONE_CHANGED,
-     *     and LOCALE_CHANGED</li>
-     *     <li>Rebuild notifications on MY_PACKAGE_REPLACED</li>
-     * </ul>
-     */
-    @Override
-    public void onReceive(final Context context, Intent intent) {
-        final String action = intent.getAction();
-        LogUtils.i("AlarmInitReceiver " + action);
-
-        final PendingResult result = goAsync();
-        final WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
-        wl.acquire();
-
-        // We need to increment the global id out of the async task to prevent race conditions
-        DataModel.getDataModel().updateGlobalIntentId();
-
-        // Updates stopwatch and timer data after a device reboot so they are as accurate as
-        // possible.
-        if (ACTION_BOOT_COMPLETED.equals(action)) {
-            DataModel.getDataModel().updateAfterReboot();
-            // Stopwatch and timer data need to be updated on time change so the reboot
-            // functionality works as expected.
-        } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
-            DataModel.getDataModel().updateAfterTimeSet();
-        }
-
-        // Update shortcuts so they exist for the user.
-        if (Intent.ACTION_BOOT_COMPLETED.equals(action)
-                || Intent.ACTION_LOCALE_CHANGED.equals(action)) {
-            Controller.getController().updateShortcuts();
-        }
-
-        // Notifications are canceled by the system on application upgrade. This broadcast signals
-        // that the new app is free to rebuild the notifications using the existing data.
-        // Additionally on new app installs, make sure to enable shortcuts immediately as opposed
-        // to waiting for system reboot.
-        if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) {
-            DataModel.getDataModel().updateAllNotifications();
-            Controller.getController().updateShortcuts();
-        }
-
-        AsyncHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    // Process restored data if any exists
-                    if (!DeskClockBackupAgent.processRestoredData(context)) {
-                        // Update all the alarm instances on time change event
-                        AlarmStateManager.fixAlarmInstances(context);
-                    }
-                } finally {
-                    result.finish();
-                    wl.release();
-                    LogUtils.v("AlarmInitReceiver finished");
-                }
-            }
-        });
-    }
-}
diff --git a/src/com/android/deskclock/AlarmInitReceiver.kt b/src/com/android/deskclock/AlarmInitReceiver.kt
new file mode 100644
index 0000000..548e939
--- /dev/null
+++ b/src/com/android/deskclock/AlarmInitReceiver.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+import com.android.deskclock.AlarmAlertWakeLock.createPartialWakeLock
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.controller.Controller
+import com.android.deskclock.data.DataModel
+
+class AlarmInitReceiver : BroadcastReceiver() {
+    /**
+     * This receiver handles a variety of actions:
+     *
+     * <ul>
+     *     <li>Clean up backup data that was recently restored to this device on
+     *     ACTION_COMPLETE_RESTORE.</li>
+     *     <li>Reset timers and stopwatch on ACTION_BOOT_COMPLETED</li>
+     *     <li>Fix alarm states on ACTION_BOOT_COMPLETED, TIME_SET, TIMEZONE_CHANGED,
+     *     and LOCALE_CHANGED</li>
+     *     <li>Rebuild notifications on MY_PACKAGE_REPLACED</li>
+     * </ul>
+     */
+    override fun onReceive(context: Context, intent: Intent) {
+        val action = intent.action
+        LogUtils.i("AlarmInitReceiver $action")
+
+        val result = goAsync()
+        val wl = createPartialWakeLock(context)
+        wl.acquire()
+
+        // We need to increment the global id out of the async task to prevent race conditions
+        DataModel.dataModel.updateGlobalIntentId()
+
+        // Updates stopwatch and timer data after a device reboot so they are as accurate as
+        // possible.
+        if (ACTION_BOOT_COMPLETED == action) {
+            DataModel.dataModel.updateAfterReboot()
+            // Stopwatch and timer data need to be updated on time change so the reboot
+            // functionality works as expected.
+        } else if (Intent.ACTION_TIME_CHANGED == action) {
+            DataModel.dataModel.updateAfterTimeSet()
+        }
+
+        // Update shortcuts so they exist for the user.
+        if (Intent.ACTION_BOOT_COMPLETED == action || Intent.ACTION_LOCALE_CHANGED == action) {
+            Controller.getController().updateShortcuts()
+            NotificationUtils.updateNotificationChannels(context)
+        }
+
+        // Notifications are canceled by the system on application upgrade. This broadcast signals
+        // that the new app is free to rebuild the notifications using the existing data.
+        // Additionally on new app installs, make sure to enable shortcuts immediately as opposed
+        // to waiting for system reboot.
+        if (Intent.ACTION_MY_PACKAGE_REPLACED == action) {
+            DataModel.dataModel.updateAllNotifications()
+            Controller.getController().updateShortcuts()
+        }
+
+        AsyncHandler.post {
+            try {
+                // Process restored data if any exists
+                if (!DeskClockBackupAgent.processRestoredData(context)) {
+                    // Update all the alarm instances on time change event
+                    AlarmStateManager.fixAlarmInstances(context)
+                }
+            } finally {
+                result.finish()
+                wl.release()
+                LogUtils.v("AlarmInitReceiver finished")
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * When running on N devices, we're interested in the boot completed event that is sent
+         * while the user is still locked, so that we can schedule alarms.
+         */
+        @SuppressLint("InlinedApi")
+        private val ACTION_BOOT_COMPLETED = if (Utils.isNOrLater) {
+            Intent.ACTION_LOCKED_BOOT_COMPLETED
+        } else {
+            Intent.ACTION_BOOT_COMPLETED
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmRecyclerView.java b/src/com/android/deskclock/AlarmRecyclerView.java
deleted file mode 100644
index 13bffcf..0000000
--- a/src/com/android/deskclock/AlarmRecyclerView.java
+++ /dev/null
@@ -1,67 +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.content.Context;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.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/AlarmRecyclerView.kt b/src/com/android/deskclock/AlarmRecyclerView.kt
new file mode 100644
index 0000000..02f6da8
--- /dev/null
+++ b/src/com/android/deskclock/AlarmRecyclerView.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util.AttributeSet
+import android.view.MotionEvent
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Thin wrapper around RecyclerView to prevent simultaneous layout passes, particularly during
+ * animations.
+ */
+class AlarmRecyclerView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : RecyclerView(context, attrs, defStyle) {
+    private var mIgnoreRequestLayout = false
+
+    init {
+        addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
+            override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
+                // Disable scrolling/user action to prevent choppy animations.
+                return rv.getItemAnimator()!!.isRunning()
+            }
+        })
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        mIgnoreRequestLayout = true
+        super.onLayout(changed, left, top, right, bottom)
+        mIgnoreRequestLayout = false
+    }
+
+    override fun requestLayout() {
+        if (!mIgnoreRequestLayout &&
+                (getItemAnimator() == null || !getItemAnimator()!!.isRunning())) {
+            super.requestLayout()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmSelectionActivity.java b/src/com/android/deskclock/AlarmSelectionActivity.java
deleted file mode 100644
index 4bd983f..0000000
--- a/src/com/android/deskclock/AlarmSelectionActivity.java
+++ /dev/null
@@ -1,126 +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.app.Activity;
-import android.app.ListActivity;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ListView;
-
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.widget.selector.AlarmSelection;
-import com.android.deskclock.widget.selector.AlarmSelectionAdapter;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-public class AlarmSelectionActivity extends ListActivity {
-
-    /** Used by default when an invalid action provided. */
-    private static final int ACTION_INVALID = -1;
-
-    /** Action used to signify alarm should be dismissed on selection. */
-    public static final int ACTION_DISMISS = 0;
-
-    public static final String EXTRA_ACTION = "com.android.deskclock.EXTRA_ACTION";
-    public static final String EXTRA_ALARMS = "com.android.deskclock.EXTRA_ALARMS";
-
-    private final List<AlarmSelection> mSelections = new ArrayList<>();
-
-    private int mAction;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        // this activity is shown if:
-        // a) no search mode was specified in which case we show all
-        // enabled alarms
-        // b) if search mode was next and there was multiple alarms firing next
-        // (at the same time) then we only show those alarms firing at the same time
-        // c) if search mode was time and there are multiple alarms with that time
-        // then we only show those alarms with that time
-
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.selection_layout);
-
-        final Button cancelButton = (Button) findViewById(R.id.cancel_button);
-        cancelButton.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                finish();
-            }
-        });
-
-        final Intent intent = getIntent();
-        final Parcelable[] alarmsFromIntent = intent.getParcelableArrayExtra(EXTRA_ALARMS);
-        mAction = intent.getIntExtra(EXTRA_ACTION, ACTION_INVALID);
-
-        // reading alarms from intent
-        // PickSelection is started only if there are more than 1 relevant alarm
-        // so no need to check if alarmsFromIntent is empty
-        for (Parcelable parcelable : alarmsFromIntent) {
-            final Alarm alarm = (Alarm) parcelable;
-
-            // filling mSelections that go into the UI picker list
-            final String label = String.format(Locale.US, "%d %02d", alarm.hour, alarm.minutes);
-            mSelections.add(new AlarmSelection(label, alarm));
-        }
-
-        setListAdapter(new AlarmSelectionAdapter(this, R.layout.alarm_row, mSelections));
-    }
-
-    @Override
-    public void onListItemClick(ListView l, View v, int position, long id) {
-        super.onListItemClick(l, v, position, id);
-        // id corresponds to mSelections id because the view adapter used mSelections
-        final AlarmSelection selection = mSelections.get((int) id);
-        final Alarm alarm = selection.getAlarm();
-        if (alarm != null) {
-            new ProcessAlarmActionAsync(alarm, this, mAction).execute();
-        }
-        finish();
-    }
-
-    private static class ProcessAlarmActionAsync extends AsyncTask<Void, Void, Void> {
-
-        private final Alarm mAlarm;
-        private final Activity mActivity;
-        private final int mAction;
-
-        public ProcessAlarmActionAsync(Alarm alarm, Activity activity, int action) {
-            mAlarm = alarm;
-            mActivity = activity;
-            mAction = action;
-        }
-
-        @Override
-        protected Void doInBackground(Void... parameters) {
-            switch (mAction) {
-                case ACTION_DISMISS:
-                    HandleApiCalls.dismissAlarm(mAlarm, mActivity);
-                    break;
-                case ACTION_INVALID:
-                    LogUtils.i("Invalid action");
-            }
-            return null;
-        }
-    }
-}
diff --git a/src/com/android/deskclock/AlarmSelectionActivity.kt b/src/com/android/deskclock/AlarmSelectionActivity.kt
new file mode 100644
index 0000000..e20e613
--- /dev/null
+++ b/src/com/android/deskclock/AlarmSelectionActivity.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Activity
+import android.app.ListActivity
+import android.os.AsyncTask
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+import android.widget.ListView
+
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.widget.selector.AlarmSelection
+import com.android.deskclock.widget.selector.AlarmSelectionAdapter
+
+import java.util.Locale
+
+class AlarmSelectionActivity : ListActivity() {
+    private val mSelections: MutableList<AlarmSelection> = ArrayList()
+    private var mAction = 0
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        // this activity is shown if:
+        // a) no search mode was specified in which case we show all
+        // enabled alarms
+        // b) if search mode was next and there was multiple alarms firing next
+        // (at the same time) then we only show those alarms firing at the same time
+        // c) if search mode was time and there are multiple alarms with that time
+        // then we only show those alarms with that time
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.selection_layout)
+
+        val cancelButton = findViewById<View>(R.id.cancel_button) as Button
+        cancelButton.setOnClickListener { finish() }
+
+        val intent = intent
+        val alarmsFromIntent = intent.getParcelableArrayExtra(EXTRA_ALARMS)
+        mAction = intent.getIntExtra(EXTRA_ACTION, ACTION_INVALID)
+
+        // reading alarms from intent
+        // PickSelection is started only if there are more than 1 relevant alarm
+        // so no need to check if alarmsFromIntent is empty
+        for (parcelable in alarmsFromIntent!!) {
+            val alarm = parcelable as Alarm
+
+            // filling mSelections that go into the UI picker list
+            val label = String.format(Locale.US, "%d %02d", alarm.hour, alarm.minutes)
+            mSelections.add(AlarmSelection(label, alarm))
+        }
+
+        listAdapter = AlarmSelectionAdapter(this, R.layout.alarm_row, mSelections)
+    }
+
+    public override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
+        super.onListItemClick(l, v, position, id)
+        // id corresponds to mSelections id because the view adapter used mSelections
+        val selection = mSelections[id.toInt()]
+        val alarm: Alarm? = selection.alarm
+        alarm?.let {
+            ProcessAlarmActionAsync(it, this, mAction).execute()
+        }
+        finish()
+    }
+
+    // TODO(b/165664115) Replace deprecated AsyncTask calls
+    private class ProcessAlarmActionAsync(
+        private val mAlarm: Alarm,
+        private val mActivity: Activity,
+        private val mAction: Int
+    ) : AsyncTask<Void?, Void?, Void?>() {
+        override fun doInBackground(vararg parameters: Void?): Void? {
+            when (mAction) {
+                ACTION_DISMISS -> HandleApiCalls.dismissAlarm(mAlarm, mActivity)
+                ACTION_INVALID -> LogUtils.i("Invalid action")
+            }
+            return null
+        }
+    }
+
+    companion object {
+        /** Used by default when an invalid action provided.  */
+        private const val ACTION_INVALID = -1
+
+        /** Action used to signify alarm should be dismissed on selection.  */
+        const val ACTION_DISMISS = 0
+
+        const val EXTRA_ACTION = "com.android.deskclock.EXTRA_ACTION"
+        const val EXTRA_ALARMS = "com.android.deskclock.EXTRA_ALARMS"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmUtils.java b/src/com/android/deskclock/AlarmUtils.java
deleted file mode 100644
index db60ace..0000000
--- a/src/com/android/deskclock/AlarmUtils.java
+++ /dev/null
@@ -1,110 +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;
-
-import android.content.Context;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.material.snackbar.Snackbar;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.view.View;
-import android.widget.Toast;
-
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.widget.toast.SnackbarManager;
-import com.android.deskclock.widget.toast.ToastManager;
-
-import java.util.Calendar;
-import java.util.Locale;
-
-/**
- * Static utility methods for Alarms.
- */
-public class AlarmUtils {
-
-    public static String getFormattedTime(Context context, Calendar time) {
-        final String skeleton = DateFormat.is24HourFormat(context) ? "EHm" : "Ehma";
-        final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
-        return (String) DateFormat.format(pattern, time);
-    }
-
-    public static String getFormattedTime(Context context, long timeInMillis) {
-        final Calendar c = Calendar.getInstance();
-        c.setTimeInMillis(timeInMillis);
-        return getFormattedTime(context, c);
-    }
-
-    public static String getAlarmText(Context context, AlarmInstance instance,
-            boolean includeLabel) {
-        String alarmTimeStr = getFormattedTime(context, instance.getAlarmTime());
-        return (instance.mLabel.isEmpty() || !includeLabel)
-                ? alarmTimeStr
-                : alarmTimeStr + " - " + instance.mLabel;
-    }
-
-    /**
-     * format "Alarm set for 2 days, 7 hours, and 53 minutes from now."
-     */
-    @VisibleForTesting
-    static String formatElapsedTimeUntilAlarm(Context context, long delta) {
-        // If the alarm will ring within 60 seconds, just report "less than a minute."
-        final String[] formats = context.getResources().getStringArray(R.array.alarm_set);
-        if (delta < DateUtils.MINUTE_IN_MILLIS) {
-            return formats[0];
-        }
-
-        // Otherwise, format the remaining time until the alarm rings.
-
-        // Round delta upwards to the nearest whole minute. (e.g. 7m 58s -> 8m)
-        final long remainder = delta % DateUtils.MINUTE_IN_MILLIS;
-        delta += remainder == 0 ? 0 : (DateUtils.MINUTE_IN_MILLIS - remainder);
-
-        int hours = (int) delta / (1000 * 60 * 60);
-        final int minutes = (int) delta / (1000 * 60) % 60;
-        final int days = hours / 24;
-        hours = hours % 24;
-
-        String daySeq = Utils.getNumberFormattedQuantityString(context, R.plurals.days, days);
-        String minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, minutes);
-        String hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, hours);
-
-        final boolean showDays = days > 0;
-        final boolean showHours = hours > 0;
-        final boolean showMinutes = minutes > 0;
-
-        // Compute the index of the most appropriate time format based on the time delta.
-        final int index = (showDays ? 1 : 0) | (showHours ? 2 : 0) | (showMinutes ? 4 : 0);
-
-        return String.format(formats[index], daySeq, hourSeq, minSeq);
-    }
-
-    public static void popAlarmSetToast(Context context, long alarmTime) {
-        final long alarmTimeDelta = alarmTime - System.currentTimeMillis();
-        final String text = formatElapsedTimeUntilAlarm(context, alarmTimeDelta);
-        Toast toast = Toast.makeText(context, text, Toast.LENGTH_LONG);
-        ToastManager.setToast(toast);
-        toast.show();
-    }
-
-    public static void popAlarmSetSnackbar(View snackbarAnchor, long alarmTime) {
-        final long alarmTimeDelta = alarmTime - System.currentTimeMillis();
-        final String text = formatElapsedTimeUntilAlarm(
-                snackbarAnchor.getContext(), alarmTimeDelta);
-        SnackbarManager.show(Snackbar.make(snackbarAnchor, text, Snackbar.LENGTH_SHORT));
-        snackbarAnchor.announceForAccessibility(text);
-    }
-}
diff --git a/src/com/android/deskclock/AlarmUtils.kt b/src/com/android/deskclock/AlarmUtils.kt
new file mode 100644
index 0000000..d17fbd1
--- /dev/null
+++ b/src/com/android/deskclock/AlarmUtils.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.text.format.DateFormat
+import android.text.format.DateUtils
+import android.view.View
+import android.widget.Toast
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.widget.toast.SnackbarManager
+import com.android.deskclock.widget.toast.ToastManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import java.util.Calendar
+import java.util.Locale
+
+/**
+ * Static utility methods for Alarms.
+ */
+object AlarmUtils {
+    @JvmStatic
+    fun getFormattedTime(context: Context, time: Calendar): String {
+        val skeleton = if (DateFormat.is24HourFormat(context)) "EHm" else "Ehma"
+        val pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton)
+        return DateFormat.format(pattern, time) as String
+    }
+
+    @JvmStatic
+    fun getFormattedTime(context: Context, timeInMillis: Long): String {
+        val c = Calendar.getInstance()
+        c.timeInMillis = timeInMillis
+        return getFormattedTime(context, c)
+    }
+
+    @JvmStatic
+    fun getAlarmText(context: Context, instance: AlarmInstance, includeLabel: Boolean): String {
+        val alarmTimeStr: String = getFormattedTime(context, instance.alarmTime)
+        return if (instance.mLabel!!.isEmpty() || !includeLabel) {
+            alarmTimeStr
+        } else {
+            alarmTimeStr + " - " + instance.mLabel
+        }
+    }
+
+    /**
+     * format "Alarm set for 2 days, 7 hours, and 53 minutes from now."
+     */
+    @VisibleForTesting
+    fun formatElapsedTimeUntilAlarm(context: Context, delta: Long): String {
+        // If the alarm will ring within 60 seconds, just report "less than a minute."
+        var variableDelta = delta
+        val formats = context.resources.getStringArray(R.array.alarm_set)
+        if (variableDelta < DateUtils.MINUTE_IN_MILLIS) {
+            return formats[0]
+        }
+
+        // Otherwise, format the remaining time until the alarm rings.
+
+        // Round delta upwards to the nearest whole minute. (e.g. 7m 58s -> 8m)
+        val remainder = variableDelta % DateUtils.MINUTE_IN_MILLIS
+        variableDelta += if (remainder == 0L) 0 else DateUtils.MINUTE_IN_MILLIS - remainder
+        var hours = variableDelta.toInt() / (1000 * 60 * 60)
+        val minutes = variableDelta.toInt() / (1000 * 60) % 60
+        val days = hours / 24
+        hours %= 24
+
+        val daySeq = Utils.getNumberFormattedQuantityString(context, R.plurals.days, days)
+        val minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, minutes)
+        val hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, hours)
+
+        val showDays = days > 0
+        val showHours = hours > 0
+        val showMinutes = minutes > 0
+
+        // Compute the index of the most appropriate time format based on the time delta.
+        val index = ((if (showDays) 1 else 0)
+                or (if (showHours) 2 else 0)
+                or (if (showMinutes) 4 else 0))
+
+        return String.format(formats[index], daySeq, hourSeq, minSeq)
+    }
+
+    @JvmStatic
+    fun popAlarmSetToast(context: Context, alarmTime: Long) {
+        val alarmTimeDelta = alarmTime - System.currentTimeMillis()
+        val text = formatElapsedTimeUntilAlarm(context, alarmTimeDelta)
+        val toast = Toast.makeText(context, text, Toast.LENGTH_LONG)
+        ToastManager.setToast(toast)
+        toast.show()
+    }
+
+    @JvmStatic
+    fun popAlarmSetSnackbar(snackbarAnchor: View, alarmTime: Long) {
+        val alarmTimeDelta = alarmTime - System.currentTimeMillis()
+        val text = formatElapsedTimeUntilAlarm(
+                snackbarAnchor.context, alarmTimeDelta)
+        SnackbarManager.show(Snackbar.make(snackbarAnchor, text, Snackbar.LENGTH_SHORT))
+        snackbarAnchor.announceForAccessibility(text)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AnalogClock.java b/src/com/android/deskclock/AnalogClock.java
deleted file mode 100644
index e5f9c24..0000000
--- a/src/com/android/deskclock/AnalogClock.java
+++ /dev/null
@@ -1,168 +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.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import androidx.appcompat.widget.AppCompatImageView;
-import android.text.format.DateFormat;
-import android.util.AttributeSet;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.TimeZone;
-
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-
-/**
- * This widget display an analog clock with two hands for hours and minutes.
- */
-public class AnalogClock extends FrameLayout {
-
-    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
-                final String tz = intent.getStringExtra(Intent.EXTRA_TIMEZONE);
-                mTime = Calendar.getInstance(TimeZone.getTimeZone(tz));
-            }
-            onTimeChanged();
-        }
-    };
-
-    private final Runnable mClockTick = new Runnable() {
-        @Override
-        public void run() {
-            onTimeChanged();
-
-            if (mEnableSeconds) {
-                final long now = System.currentTimeMillis();
-                final long delay = SECOND_IN_MILLIS - now % SECOND_IN_MILLIS;
-                postDelayed(this, delay);
-            }
-        }
-    };
-
-    private final ImageView mHourHand;
-    private final ImageView mMinuteHand;
-    private final ImageView mSecondHand;
-
-    private Calendar mTime;
-    private String mDescFormat;
-    private TimeZone mTimeZone;
-    private boolean mEnableSeconds = true;
-
-    public AnalogClock(Context context) {
-        this(context, null /* attrs */);
-    }
-
-    public AnalogClock(Context context, AttributeSet attrs) {
-        this(context, attrs, 0 /* defStyleAttr */);
-    }
-
-    public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-
-        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);
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-
-        final IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_TIME_TICK);
-        filter.addAction(Intent.ACTION_TIME_CHANGED);
-        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
-        getContext().registerReceiver(mIntentReceiver, filter);
-
-        // Refresh the calendar instance since the time zone may have changed while the receiver
-        // wasn't registered.
-        mTime = Calendar.getInstance(mTimeZone != null ? mTimeZone : TimeZone.getDefault());
-        onTimeChanged();
-
-        // Tick every second.
-        if (mEnableSeconds) {
-            mClockTick.run();
-        }
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-
-        getContext().unregisterReceiver(mIntentReceiver);
-        removeCallbacks(mClockTick);
-    }
-
-    private void onTimeChanged() {
-        mTime.setTimeInMillis(System.currentTimeMillis());
-        final float hourAngle = mTime.get(Calendar.HOUR) * 30f;
-        mHourHand.setRotation(hourAngle);
-        final float minuteAngle = mTime.get(Calendar.MINUTE) * 6f;
-        mMinuteHand.setRotation(minuteAngle);
-        if (mEnableSeconds) {
-            final float secondAngle = mTime.get(Calendar.SECOND) * 6f;
-            mSecondHand.setRotation(secondAngle);
-        }
-        setContentDescription(DateFormat.format(mDescFormat, mTime));
-        invalidate();
-    }
-
-    public void setTimeZone(String id) {
-        mTimeZone = TimeZone.getTimeZone(id);
-        mTime.setTimeZone(mTimeZone);
-        onTimeChanged();
-    }
-
-    public void enableSeconds(boolean enable) {
-        mEnableSeconds = enable;
-        if (mEnableSeconds) {
-            mSecondHand.setVisibility(VISIBLE);
-            mClockTick.run();
-        } else {
-            mSecondHand.setVisibility(GONE);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/AnalogClock.kt b/src/com/android/deskclock/AnalogClock.kt
new file mode 100644
index 0000000..2627ff5
--- /dev/null
+++ b/src/com/android/deskclock/AnalogClock.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.appcompat.widget.AppCompatImageView
+
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.TimeZone
+
+/**
+ * This widget display an analog clock with two hands for hours and minutes.
+ */
+class AnalogClock @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+    private val mIntentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED == intent.action) {
+                val tz = intent.getStringExtra("time-zone")
+                mTime = Calendar.getInstance(TimeZone.getTimeZone(tz))
+            }
+            onTimeChanged()
+        }
+    }
+
+    private val mClockTick: Runnable = object : Runnable {
+        override fun run() {
+            onTimeChanged()
+
+            if (mEnableSeconds) {
+                val now = System.currentTimeMillis()
+                val delay = DateUtils.SECOND_IN_MILLIS - now % DateUtils.SECOND_IN_MILLIS
+                postDelayed(this, delay)
+            }
+        }
+    }
+
+    private val mHourHand: ImageView
+    private val mMinuteHand: ImageView
+    private val mSecondHand: ImageView
+
+    private var mTime = Calendar.getInstance()
+    private val mDescFormat =
+            (DateFormat.getTimeFormat(context) as SimpleDateFormat).toLocalizedPattern()
+    private var mTimeZone: TimeZone? = null
+    private var mEnableSeconds = true
+
+    init {
+        // 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.
+        val dial: ImageView = AppCompatImageView(context)
+        dial.setImageResource(R.drawable.clock_analog_dial)
+        dial.drawable.mutate()
+        addView(dial)
+
+        mHourHand = AppCompatImageView(context)
+        mHourHand.setImageResource(R.drawable.clock_analog_hour)
+        mHourHand.drawable.mutate()
+        addView(mHourHand)
+
+        mMinuteHand = AppCompatImageView(context)
+        mMinuteHand.setImageResource(R.drawable.clock_analog_minute)
+        mMinuteHand.drawable.mutate()
+        addView(mMinuteHand)
+
+        mSecondHand = AppCompatImageView(context)
+        mSecondHand.setImageResource(R.drawable.clock_analog_second)
+        mSecondHand.drawable.mutate()
+        addView(mSecondHand)
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+
+        val filter = IntentFilter()
+        filter.addAction(Intent.ACTION_TIME_TICK)
+        filter.addAction(Intent.ACTION_TIME_CHANGED)
+        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
+        context.registerReceiver(mIntentReceiver, filter)
+
+        // Refresh the calendar instance since the time zone may have changed while the receiver
+        // wasn't registered.
+        mTime = Calendar.getInstance(mTimeZone ?: TimeZone.getDefault())
+        onTimeChanged()
+
+        // Tick every second.
+        if (mEnableSeconds) {
+            mClockTick.run()
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+
+        context.unregisterReceiver(mIntentReceiver)
+        removeCallbacks(mClockTick)
+    }
+
+    private fun onTimeChanged() {
+        mTime.timeInMillis = System.currentTimeMillis()
+        val hourAngle = mTime[Calendar.HOUR] * 30f
+        mHourHand.rotation = hourAngle
+        val minuteAngle = mTime[Calendar.MINUTE] * 6f
+        mMinuteHand.rotation = minuteAngle
+        if (mEnableSeconds) {
+            val secondAngle = mTime[Calendar.SECOND] * 6f
+            mSecondHand.rotation = secondAngle
+        }
+        contentDescription = DateFormat.format(mDescFormat, mTime)
+        invalidate()
+    }
+
+    fun setTimeZone(id: String) {
+        mTimeZone = TimeZone.getTimeZone(id)
+        mTime.timeZone = mTimeZone!!
+        onTimeChanged()
+    }
+
+    fun enableSeconds(enable: Boolean) {
+        mEnableSeconds = enable
+        if (mEnableSeconds) {
+            mSecondHand.visibility = View.VISIBLE
+            mClockTick.run()
+        } else {
+            mSecondHand.visibility = View.GONE
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AnimatorUtils.java b/src/com/android/deskclock/AnimatorUtils.java
deleted file mode 100644
index 9b3cc6f..0000000
--- a/src/com/android/deskclock/AnimatorUtils.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * 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.
- */
-
-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 androidx.core.graphics.drawable.DrawableCompat;
-import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
-import android.util.Property;
-import android.view.View;
-import android.view.animation.Interpolator;
-import android.widget.ImageView;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-public class AnimatorUtils {
-
-    public static final Interpolator DECELERATE_ACCELERATE_INTERPOLATOR = new Interpolator() {
-        @Override
-        public float getInterpolation(float x) {
-            return 0.5f + 4.0f * (x - 0.5f) * (x - 0.5f) * (x - 0.5f);
-        }
-    };
-
-    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) {
-            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) {
-            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
-        public Integer get(ImageView view) {
-            return view.getDrawable().getAlpha();
-        }
-
-        @Override
-        public void set(ImageView view, Integer value) {
-            view.getDrawable().setAlpha(value);
-        }
-    };
-
-    public static final Property<ImageView, Integer> DRAWABLE_TINT =
-            new Property<ImageView, Integer>(Integer.class, "drawable.tint") {
-        @Override
-        public Integer get(ImageView view) {
-            return null;
-        }
-
-        @Override
-        public void set(ImageView view, Integer value) {
-            // Ensure the drawable is wrapped using DrawableCompat.
-            final Drawable drawable = view.getDrawable();
-            final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
-            if (wrappedDrawable != drawable) {
-                view.setImageDrawable(wrappedDrawable);
-            }
-            // Set the new tint value via DrawableCompat.
-            DrawableCompat.setTint(wrappedDrawable, value);
-        }
-    };
-
-    @SuppressWarnings("unchecked")
-    public static final TypeEvaluator<Integer> ARGB_EVALUATOR = new ArgbEvaluator();
-
-    private static Method sAnimateValue;
-    private static boolean sTryAnimateValue = true;
-
-    public static void setAnimatedFraction(ValueAnimator animator, float fraction) {
-        if (Utils.isLMR1OrLater()) {
-            animator.setCurrentFraction(fraction);
-            return;
-        }
-
-        if (sTryAnimateValue) {
-            // try to set the animated fraction directly so that it isn't affected by the
-            // internal animator scale or time (b/17938711)
-            try {
-                if (sAnimateValue == null) {
-                    sAnimateValue = ValueAnimator.class
-                            .getDeclaredMethod("animateValue", float.class);
-                    sAnimateValue.setAccessible(true);
-                }
-
-                sAnimateValue.invoke(animator, fraction);
-                return;
-            } catch (NoSuchMethodException | InvocationTargetException
-                    | IllegalAccessException e) {
-                // something went wrong, don't try that again
-                LogUtils.e("Unable to use animateValue directly", e);
-                sTryAnimateValue = false;
-            }
-        }
-
-        // if that doesn't work then just fall back to setting the current play time
-        animator.setCurrentPlayTime(Math.round(fraction * animator.getDuration()));
-    }
-
-    public static void reverse(ValueAnimator... animators) {
-        for (ValueAnimator animator : animators) {
-            final float fraction = animator.getAnimatedFraction();
-            if (fraction > 0.0f) {
-                animator.reverse();
-                setAnimatedFraction(animator, 1.0f - fraction);
-            }
-        }
-    }
-
-    public static void cancel(ValueAnimator... animators) {
-        for (ValueAnimator animator : animators) {
-            animator.cancel();
-        }
-    }
-
-    public static ValueAnimator getScaleAnimator(View view, float... values) {
-        return ObjectAnimator.ofPropertyValuesHolder(view,
-                PropertyValuesHolder.ofFloat(View.SCALE_X, values),
-                PropertyValuesHolder.ofFloat(View.SCALE_Y, values));
-    }
-
-    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/AnimatorUtils.kt b/src/com/android/deskclock/AnimatorUtils.kt
new file mode 100644
index 0000000..3172738
--- /dev/null
+++ b/src/com/android/deskclock/AnimatorUtils.kt
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.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.util.Property
+import android.view.View
+import android.view.animation.Interpolator
+import android.widget.ImageView
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator
+
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Method
+
+import kotlin.math.roundToLong
+
+object AnimatorUtils {
+    @JvmField
+    val DECELERATE_ACCELERATE_INTERPOLATOR =
+            Interpolator { x -> 0.5f + 4.0f * (x - 0.5f) * (x - 0.5f) * (x - 0.5f) }
+
+    @JvmField
+    val INTERPOLATOR_FAST_OUT_SLOW_IN: Interpolator = FastOutSlowInInterpolator()
+
+    @JvmField
+    val BACKGROUND_ALPHA: Property<View, Int> =
+            object : Property<View, Int>(Int::class.java, "background.alpha") {
+        override fun get(view: View): Int {
+            var background = view.background
+            if (background is LayerDrawable &&
+                    background.numberOfLayers > 0) {
+                background = background.getDrawable(0)
+            }
+            return background.alpha
+        }
+
+        override fun set(view: View, value: Int) {
+            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)
+     */
+    @JvmStatic
+    fun setBackgroundAlpha(view: View, value: Int?) {
+        var background = view.background
+        if (background is LayerDrawable &&
+                background.numberOfLayers > 0) {
+            background = background.getDrawable(0)
+        }
+        background.alpha = value!!
+    }
+
+    @JvmField
+    val DRAWABLE_ALPHA: Property<ImageView, Int> =
+            object : Property<ImageView, Int>(Int::class.java, "drawable.alpha") {
+        override fun get(view: ImageView): Int {
+            return view.drawable.alpha
+        }
+
+        override fun set(view: ImageView, value: Int) {
+            view.drawable.alpha = value
+        }
+    }
+
+    @JvmField
+    val DRAWABLE_TINT: Property<ImageView, Int> =
+            object : Property<ImageView, Int>(Int::class.java, "drawable.tint") {
+        override fun get(view: ImageView): Int? {
+            return null
+        }
+
+        override fun set(view: ImageView, value: Int) {
+            // Ensure the drawable is wrapped using DrawableCompat.
+            val drawable = view.drawable
+            val wrappedDrawable: Drawable = DrawableCompat.wrap(drawable)
+            if (wrappedDrawable !== drawable) {
+                view.setImageDrawable(wrappedDrawable)
+            }
+            // Set the new tint value via DrawableCompat.
+            DrawableCompat.setTint(wrappedDrawable, value)
+        }
+    }
+
+    @JvmField
+    val ARGB_EVALUATOR: TypeEvaluator<Int> = ArgbEvaluator() as TypeEvaluator<Int>
+
+    private var sAnimateValue: Method? = null
+
+    private var sTryAnimateValue = true
+
+    @JvmStatic
+    fun setAnimatedFraction(animator: ValueAnimator, fraction: Float) {
+        if (Utils.isLMR1OrLater) {
+            animator.setCurrentFraction(fraction)
+            return
+        }
+
+        if (sTryAnimateValue) {
+            // try to set the animated fraction directly so that it isn't affected by the
+            // internal animator scale or time (b/17938711)
+            try {
+                if (sAnimateValue == null) {
+                    sAnimateValue = ValueAnimator::class.java
+                            .getDeclaredMethod("animateValue", Float::class.javaPrimitiveType)
+                    sAnimateValue!!.isAccessible = true
+                }
+
+                sAnimateValue!!.invoke(animator, fraction)
+                return
+            } catch (e: NoSuchMethodException) {
+                // something went wrong, don't try that again
+                LogUtils.e("Unable to use animateValue directly", e)
+                sTryAnimateValue = false
+            } catch (e: InvocationTargetException) {
+                LogUtils.e("Unable to use animateValue directly", e)
+                sTryAnimateValue = false
+            } catch (e: IllegalAccessException) {
+                LogUtils.e("Unable to use animateValue directly", e)
+                sTryAnimateValue = false
+            }
+        }
+
+        // if that doesn't work then just fall back to setting the current play time
+        animator.currentPlayTime = (fraction * animator.duration).roundToLong()
+    }
+
+    @JvmStatic
+    fun reverse(vararg animators: ValueAnimator) {
+        for (animator in animators) {
+            val fraction = animator.animatedFraction
+            if (fraction > 0.0f) {
+                animator.reverse()
+                setAnimatedFraction(animator, 1.0f - fraction)
+            }
+        }
+    }
+
+    fun cancel(vararg animators: ValueAnimator) {
+        for (animator in animators) {
+            animator.cancel()
+        }
+    }
+
+    @JvmStatic
+    fun getScaleAnimator(view: View?, vararg values: Float): ValueAnimator {
+        return ObjectAnimator.ofPropertyValuesHolder(view,
+                PropertyValuesHolder.ofFloat(View.SCALE_X, *values),
+                PropertyValuesHolder.ofFloat(View.SCALE_Y, *values))
+    }
+
+    @JvmStatic
+    fun getAlphaAnimator(view: View, vararg values: Float): ValueAnimator {
+        return ObjectAnimator.ofFloat(view, View.ALPHA, *values)
+    }
+
+    val VIEW_LEFT: Property<View, Int> = object : Property<View, Int>(Int::class.java, "left") {
+        override fun get(view: View): Int {
+            return view.left
+        }
+
+        override fun set(view: View, left: Int) {
+            view.left = left
+        }
+    }
+
+    val VIEW_TOP: Property<View, Int> = object : Property<View, Int>(Int::class.java, "top") {
+        override fun get(view: View): Int {
+            return view.top
+        }
+
+        override fun set(view: View, top: Int) {
+            view.top = top
+        }
+    }
+
+    val VIEW_BOTTOM: Property<View, Int> = object : Property<View, Int>(Int::class.java, "bottom") {
+        override fun get(view: View): Int {
+            return view.bottom
+        }
+
+        override fun set(view: View, bottom: Int) {
+            view.bottom = bottom
+        }
+    }
+
+    val VIEW_RIGHT: Property<View, Int> = object : Property<View, Int>(Int::class.java, "right") {
+        override fun get(view: View): Int {
+            return view.right
+        }
+
+        override fun set(view: View, right: Int) {
+            view.right = right
+        }
+    }
+
+    /**
+     * @param target the view to be morphed
+     * @param from the bounds of the `target` before animating
+     * @param to the bounds of the `target` after animating
+     * @return an animator that morphs the `target` between the `from` bounds and the
+     * `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
+     * `target` bounds.
+     */
+    fun getBoundsAnimator(target: View, from: View, to: View): Animator {
+        // Fetch the content insets for the views. Content bounds are what matter, not total bounds.
+        val targetInsets = Rect()
+        target.background.getPadding(targetInsets)
+        val fromInsets = Rect()
+        from.background.getPadding(fromInsets)
+        val toInsets = Rect()
+        to.background.getPadding(toInsets)
+
+        // Before animating, the content bounds of target must match the content bounds of from.
+        val startLeft = from.left - fromInsets.left + targetInsets.left
+        val startTop = from.top - fromInsets.top + targetInsets.top
+        val startRight = from.right - fromInsets.right + targetInsets.right
+        val startBottom = from.bottom - fromInsets.bottom + targetInsets.bottom
+
+        // After animating, the content bounds of target must match the content bounds of to.
+        val endLeft = to.left - toInsets.left + targetInsets.left
+        val endTop = to.top - toInsets.top + targetInsets.top
+        val endRight = to.right - toInsets.right + targetInsets.right
+        val endBottom = to.bottom - 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.
+     */
+    @JvmStatic
+    fun getBoundsAnimator(
+        view: View,
+        fromLeft: Int,
+        fromTop: Int,
+        fromRight: Int,
+        fromBottom: Int,
+        toLeft: Int,
+        toTop: Int,
+        toRight: Int,
+        toBottom: Int
+    ): Animator {
+        view.left = fromLeft
+        view.top = fromTop
+        view.right = fromRight
+        view.bottom = 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))
+    }
+
+    @JvmStatic
+    fun startDrawableAnimation(view: ImageView) {
+        val d = view.drawable
+        if (d is Animatable) {
+            d.start()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AsyncHandler.java b/src/com/android/deskclock/AsyncHandler.java
deleted file mode 100644
index 026d74e..0000000
--- a/src/com/android/deskclock/AsyncHandler.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.os.Handler;
-import android.os.HandlerThread;
-
-/**
- * Helper class for managing the background thread used to perform io operations
- * and handle async broadcasts.
- */
-public final class AsyncHandler {
-    private static final HandlerThread sHandlerThread = new HandlerThread("AsyncHandler");
-    private static final Handler sHandler;
-
-    static {
-        sHandlerThread.start();
-        sHandler = new Handler(sHandlerThread.getLooper());
-    }
-
-    public static void post(Runnable r) {
-        sHandler.post(r);
-    }
-
-    private AsyncHandler() {}
-}
diff --git a/src/com/android/deskclock/AsyncHandler.kt b/src/com/android/deskclock/AsyncHandler.kt
new file mode 100644
index 0000000..903c7b4
--- /dev/null
+++ b/src/com/android/deskclock/AsyncHandler.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.Handler
+import android.os.HandlerThread
+
+/**
+ * Helper class for managing the background thread used to perform io operations
+ * and handle async broadcasts.
+ */
+object AsyncHandler {
+    private val sHandlerThread = HandlerThread("AsyncHandler")
+    private val sHandler: Handler
+
+    init {
+        sHandlerThread.start()
+        sHandler = Handler(sHandlerThread.looper)
+    }
+
+    fun post(r: () -> Unit) {
+        sHandler.post(r)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AsyncRingtonePlayer.java b/src/com/android/deskclock/AsyncRingtonePlayer.java
deleted file mode 100644
index afb46d6..0000000
--- a/src/com/android/deskclock/AsyncRingtonePlayer.java
+++ /dev/null
@@ -1,639 +0,0 @@
-package com.android.deskclock;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.MediaPlayer;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.telephony.TelephonyManager;
-
-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>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>
- *
- * <ul>
- *     <li>Prior to the M platform release, ringtone playback is accomplished using
- *     {@link MediaPlayer}. android.permission.READ_EXTERNAL_STORAGE is required to play custom
- *     ringtones located on the SD card using this mechanism. {@link MediaPlayer} allows clients to
- *     adjust the volume of the stream and specify that the stream should be looped.</li>
- *
- *     <li>Starting with the M platform release, ringtone playback is accomplished using
- *     {@link Ringtone}. android.permission.READ_EXTERNAL_STORAGE is <strong>NOT</strong> required
- *     to play custom ringtones located on the SD card using this mechanism. {@link Ringtone} allows
- *     clients to adjust the volume of the stream and specify that the stream should be looped but
- *     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");
-
-    // Volume suggested by media team for in-call alarms.
-    private static final float IN_CALL_VOLUME = 0.125f;
-
-    // Message codes used with the ringtone thread.
-    private static final int EVENT_PLAY = 1;
-    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;
-
-    /** {@link MediaPlayerPlaybackDelegate} on pre M; {@link RingtonePlaybackDelegate} on M+ */
-    private PlaybackDelegate mPlaybackDelegate;
-
-    /** The context. */
-    private final Context mContext;
-
-    public AsyncRingtonePlayer(Context context) {
-        mContext = context;
-    }
-
-    /** Plays the ringtone. */
-    public void play(Uri ringtoneUri, long crescendoDuration) {
-        LOGGER.d("Posting play.");
-        postMessage(EVENT_PLAY, ringtoneUri, crescendoDuration, 0);
-    }
-
-    /** Stops playing the ringtone. */
-    public void stop() {
-        LOGGER.d("Posting stop.");
-        postMessage(EVENT_STOP, null, 0, 0);
-    }
-
-    /** Schedules an adjustment of the playback volume 50ms in the future. */
-    private void scheduleVolumeAdjustment() {
-        LOGGER.v("Adjusting volume.");
-
-        // Ensure we never have more than one volume adjustment queued.
-        mHandler.removeMessages(EVENT_VOLUME);
-
-        // Queue the next volume adjustment.
-        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 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 crescendoDuration,
-            long delayMillis) {
-        synchronized (this) {
-            if (mHandler == null) {
-                mHandler = getNewHandler();
-            }
-
-            final Message message = mHandler.obtainMessage(messageCode);
-            if (ringtoneUri != null) {
-                final Bundle bundle = new Bundle();
-                bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri);
-                bundle.putLong(CRESCENDO_DURATION_KEY, crescendoDuration);
-                message.setData(bundle);
-            }
-
-            mHandler.sendMessageDelayed(message, delayMillis);
-        }
-    }
-
-    /**
-     * Creates a new ringtone Handler running in its own thread.
-     */
-    @SuppressLint("HandlerLeak")
-    private Handler getNewHandler() {
-        final HandlerThread thread = new HandlerThread("ringtone-player");
-        thread.start();
-
-        return new Handler(thread.getLooper()) {
-            @Override
-            public void handleMessage(Message msg) {
-                switch (msg.what) {
-                    case EVENT_PLAY:
-                        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;
-                    case EVENT_STOP:
-                        getPlaybackDelegate().stop(mContext);
-                        break;
-                    case EVENT_VOLUME:
-                        if (getPlaybackDelegate().adjustVolume(mContext)) {
-                            scheduleVolumeAdjustment();
-                        }
-                        break;
-                }
-            }
-        };
-    }
-
-    /**
-     * @return <code>true</code> iff the device is currently in a telephone call
-     */
-    private static boolean isInTelephoneCall(Context context) {
-        final TelephonyManager tm = (TelephonyManager)
-                context.getSystemService(Context.TELEPHONY_SERVICE);
-        return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
-    }
-
-    /**
-     * @return Uri of the ringtone to play when the user is in a telephone call
-     */
-    private static Uri getInCallRingtoneUri(Context context) {
-        return Utils.getResourceUri(context, R.raw.alarm_expire);
-    }
-
-    /**
-     * @return Uri of the ringtone to play when the chosen ringtone fails to play
-     */
-    private static Uri getFallbackRingtoneUri(Context context) {
-        return Utils.getResourceUri(context, R.raw.alarm_expire);
-    }
-
-    /**
-     * Check if the executing thread is the one dedicated to controlling the ringtone playback.
-     */
-    private void checkAsyncRingtonePlayerThread() {
-        if (Looper.myLooper() != mHandler.getLooper()) {
-            LOGGER.e("Must be on the AsyncRingtonePlayer thread!",
-                    new IllegalStateException());
-        }
-    }
-
-    /**
-     * @param currentTime current time of the device
-     * @param stopTime time at which the crescendo finishes
-     * @param duration length of time over which the crescendo occurs
-     * @return the scalar volume value that produces a linear increase in volume (in decibels)
-     */
-    private static float computeVolume(long currentTime, long stopTime, long duration) {
-        // Compute the percentage of the crescendo that has completed.
-        final float elapsedCrescendoTime = stopTime - currentTime;
-        final float fractionComplete = 1 - (elapsedCrescendoTime / duration);
-
-        // Use the fraction to compute a target decibel between -40dB (near silent) and 0dB (max).
-        final float gain = (fractionComplete * 40) - 40;
-
-        // Convert the target gain (in decibels) into the corresponding volume scalar.
-        final float volume = (float) Math.pow(10f, gain/20f);
-
-        LOGGER.v("Ringtone crescendo %,.2f%% complete (scalar: %f, volume: %f dB)",
-                fractionComplete * 100, volume, gain);
-
-        return volume;
-    }
-
-    /**
-     * @return the platform-specific playback delegate to use to play the ringtone
-     */
-    private PlaybackDelegate getPlaybackDelegate() {
-        checkAsyncRingtonePlayerThread();
-
-        if (mPlaybackDelegate == null) {
-            if (Utils.isMOrLater()) {
-                // Use the newer Ringtone-based playback delegate because it does not require
-                // any permissions to read from the SD card. (M+)
-                mPlaybackDelegate = new RingtonePlaybackDelegate();
-            } else {
-                // Fall back to the older MediaPlayer-based playback delegate because it is the only
-                // way to force the looping of the ringtone before M. (pre M)
-                mPlaybackDelegate = new MediaPlayerPlaybackDelegate();
-            }
-        }
-
-        return mPlaybackDelegate;
-    }
-
-    /**
-     * This interface abstracts away the differences between playing ringtones via {@link Ringtone}
-     * vs {@link MediaPlayer}.
-     */
-    private interface PlaybackDelegate {
-        /**
-         * @return {@code true} iff a {@link #adjustVolume volume adjustment} should be scheduled
-         */
-        boolean play(Context context, Uri ringtoneUri, long crescendoDuration);
-
-        /**
-         * Stop any ongoing ringtone playback.
-         */
-        void stop(Context context);
-
-        /**
-         * @return {@code true} iff another volume adjustment should be scheduled
-         */
-        boolean adjustVolume(Context context);
-    }
-
-    /**
-     * Loops playback of a ringtone using {@link MediaPlayer}.
-     */
-    private class MediaPlayerPlaybackDelegate implements PlaybackDelegate {
-
-        /** The audio focus manager. Only used by the ringtone thread. */
-        private AudioManager mAudioManager;
-
-        /** Non-{@code null} while playing a ringtone; {@code null} otherwise. */
-        private MediaPlayer mMediaPlayer;
-
-        /** The duration over which to increase the volume. */
-        private long mCrescendoDuration = 0;
-
-        /** The time at which the crescendo shall cease; 0 if no crescendo is present. */
-        private long mCrescendoStopTime = 0;
-
-        /**
-         * Starts the actual playback of the ringtone. Executes on ringtone-thread.
-         */
-        @Override
-        public boolean play(final Context context, Uri ringtoneUri, long crescendoDuration) {
-            checkAsyncRingtonePlayerThread();
-            mCrescendoDuration = crescendoDuration;
-
-            LOGGER.i("Play ringtone via android.media.MediaPlayer.");
-
-            if (mAudioManager == null) {
-                mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-            }
-
-            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());
-            }
-
-            mMediaPlayer = new MediaPlayer();
-            mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
-                @Override
-                public boolean onError(MediaPlayer mp, int what, int extra) {
-                    LOGGER.e("Error occurred while playing audio. Stopping AlarmKlaxon.");
-                    stop(context);
-                    return true;
-                }
-            });
-
-            try {
-                // 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);
-
-                return startPlayback(inTelephoneCall);
-            } catch (Throwable 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));
-                    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 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 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;
-        }
-
-        /**
-         * Stops the playback of the ringtone. Executes on the ringtone-thread.
-         */
-        @Override
-        public void stop(Context context) {
-            checkAsyncRingtonePlayerThread();
-
-            LOGGER.i("Stop ringtone via android.media.MediaPlayer.");
-
-            mCrescendoDuration = 0;
-            mCrescendoStopTime = 0;
-
-            // Stop audio playing
-            if (mMediaPlayer != null) {
-                mMediaPlayer.stop();
-                mMediaPlayer.release();
-                mMediaPlayer = null;
-            }
-
-            if (mAudioManager != null) {
-                mAudioManager.abandonAudioFocus(null);
-            }
-        }
-
-        /**
-         * Adjusts the volume of the ringtone being played to create a crescendo effect.
-         */
-        @Override
-        public boolean adjustVolume(Context context) {
-            checkAsyncRingtonePlayerThread();
-
-            // If media player is absent or not playing, ignore volume adjustment.
-            if (mMediaPlayer == null || !mMediaPlayer.isPlaying()) {
-                mCrescendoDuration = 0;
-                mCrescendoStopTime = 0;
-                return false;
-            }
-
-            // If the crescendo is complete set the volume to the maximum; we're done.
-            final long currentTime = Utils.now();
-            if (currentTime > mCrescendoStopTime) {
-                mCrescendoDuration = 0;
-                mCrescendoStopTime = 0;
-                mMediaPlayer.setVolume(1, 1);
-                return false;
-            }
-
-            // The current volume of the crescendo is the percentage of the crescendo completed.
-            final float volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration);
-            mMediaPlayer.setVolume(volume, volume);
-            LOGGER.i("MediaPlayer volume set to " + volume);
-
-            // Schedule the next volume bump in the crescendo.
-            return true;
-        }
-    }
-
-    /**
-     * Loops playback of a ringtone using {@link Ringtone}.
-     */
-    private class RingtonePlaybackDelegate implements PlaybackDelegate {
-
-        /** The audio focus manager. Only used by the ringtone thread. */
-        private AudioManager mAudioManager;
-
-        /** The current ringtone. Only used by the ringtone thread. */
-        private Ringtone mRingtone;
-
-        /** The method to adjust playback volume; cannot be null. */
-        private Method mSetVolumeMethod;
-
-        /** The method to adjust playback looping; cannot be null. */
-        private Method mSetLoopingMethod;
-
-        /** The duration over which to increase the volume. */
-        private long mCrescendoDuration = 0;
-
-        /** The time at which the crescendo shall cease; 0 if no crescendo is present. */
-        private long mCrescendoStopTime = 0;
-
-        private RingtonePlaybackDelegate() {
-            try {
-                mSetVolumeMethod = Ringtone.class.getDeclaredMethod("setVolume", float.class);
-            } catch (NoSuchMethodException nsme) {
-                LOGGER.e("Unable to locate method: Ringtone.setVolume(float).", nsme);
-            }
-
-            try {
-                mSetLoopingMethod = Ringtone.class.getDeclaredMethod("setLooping", boolean.class);
-            } catch (NoSuchMethodException nsme) {
-                LOGGER.e("Unable to locate method: Ringtone.setLooping(boolean).", nsme);
-            }
-        }
-
-        /**
-         * Starts the actual playback of the ringtone. Executes on ringtone-thread.
-         */
-        @Override
-        public boolean play(Context context, Uri ringtoneUri, long crescendoDuration) {
-            checkAsyncRingtonePlayerThread();
-            mCrescendoDuration = crescendoDuration;
-
-            LOGGER.i("Play ringtone via android.media.Ringtone.");
-
-            if (mAudioManager == null) {
-                mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-            }
-
-            final boolean inTelephoneCall = isInTelephoneCall(context);
-            if (inTelephoneCall) {
-                ringtoneUri = getInCallRingtoneUri(context);
-            }
-
-            // Attempt to fetch the specified ringtone.
-            mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
-
-            if (mRingtone == null) {
-                // Fall back to the system default ringtone.
-                ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
-                mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
-            }
-
-            // Attempt to enable looping the ringtone.
-            try {
-                mSetLoopingMethod.invoke(mRingtone, true);
-            } catch (Exception e) {
-                LOGGER.e("Unable to turn looping on for android.media.Ringtone", e);
-
-                // Fall back to the default ringtone if looping could not be enabled.
-                // (Default alarm ringtone most likely has looping tags set within the .ogg file)
-                mRingtone = null;
-            }
-
-            // 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.");
-                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)
-                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                        .build());
-            }
-
-            // Attempt to adjust the ringtone volume if the user is in a telephone call.
-            boolean scheduleVolumeAdjustment = false;
-            if (inTelephoneCall) {
-                LOGGER.v("Using the in-call alarm");
-                setRingtoneVolume(IN_CALL_VOLUME);
-            } else if (mCrescendoDuration > 0) {
-                setRingtoneVolume(0);
-
-                // Compute the time at which the crescendo will stop.
-                mCrescendoStopTime = Utils.now() + mCrescendoDuration;
-                scheduleVolumeAdjustment = true;
-            }
-
-            mAudioManager.requestAudioFocus(null, STREAM_ALARM, AUDIOFOCUS_GAIN_TRANSIENT);
-
-            mRingtone.play();
-
-            return scheduleVolumeAdjustment;
-        }
-
-        /**
-         * Sets the volume of the ringtone.
-         *
-         * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
-         *               corresponds to no attenuation being applied.
-         */
-        private void setRingtoneVolume(float volume) {
-            try {
-                mSetVolumeMethod.invoke(mRingtone, volume);
-            } catch (Exception e) {
-                LOGGER.e("Unable to set volume for android.media.Ringtone", e);
-            }
-        }
-
-        /**
-         * Stops the playback of the ringtone. Executes on the ringtone-thread.
-         */
-        @Override
-        public void stop(Context context) {
-            checkAsyncRingtonePlayerThread();
-
-            LOGGER.i("Stop ringtone via android.media.Ringtone.");
-
-            mCrescendoDuration = 0;
-            mCrescendoStopTime = 0;
-
-            if (mRingtone != null && mRingtone.isPlaying()) {
-                LOGGER.d("Ringtone.stop() invoked.");
-                mRingtone.stop();
-            }
-
-            mRingtone = null;
-
-            if (mAudioManager != null) {
-                mAudioManager.abandonAudioFocus(null);
-            }
-        }
-
-        /**
-         * Adjusts the volume of the ringtone being played to create a crescendo effect.
-         */
-        @Override
-        public boolean adjustVolume(Context context) {
-            checkAsyncRingtonePlayerThread();
-
-            // If ringtone is absent or not playing, ignore volume adjustment.
-            if (mRingtone == null || !mRingtone.isPlaying()) {
-                mCrescendoDuration = 0;
-                mCrescendoStopTime = 0;
-                return false;
-            }
-
-            // If the crescendo is complete set the volume to the maximum; we're done.
-            final long currentTime = Utils.now();
-            if (currentTime > mCrescendoStopTime) {
-                mCrescendoDuration = 0;
-                mCrescendoStopTime = 0;
-                setRingtoneVolume(1);
-                return false;
-            }
-
-            final float volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration);
-            setRingtoneVolume(volume);
-
-            // Schedule the next volume bump in the crescendo.
-            return true;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AsyncRingtonePlayer.kt b/src/com/android/deskclock/AsyncRingtonePlayer.kt
new file mode 100644
index 0000000..20a3f33
--- /dev/null
+++ b/src/com/android/deskclock/AsyncRingtonePlayer.kt
@@ -0,0 +1,638 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Context
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.telephony.TelephonyManager
+
+import java.io.IOException
+import java.lang.reflect.Method
+
+import kotlin.math.pow
+
+/**
+ *
+ * This class controls playback of ringtones. Uses [Ringtone] or [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.
+ *
+ * This class also serves a second purpose. It accomplishes alarm ringtone playback using two
+ * different mechanisms depending on the underlying platform.
+ *
+ * Prior to the M platform release, ringtone playback is accomplished using
+ * [MediaPlayer]. android.permission.READ_EXTERNAL_STORAGE is required to play custom
+ * ringtones located on the SD card using this mechanism. [MediaPlayer] allows clients to
+ * adjust the volume of the stream and specify that the stream should be looped.
+ *
+ * Starting with the M platform release, ringtone playback is accomplished using
+ * [Ringtone]. android.permission.READ_EXTERNAL_STORAGE is **NOT** required
+ * to play custom ringtones located on the SD card using this mechanism. [Ringtone] allows
+ * clients to adjust the volume of the stream and specify that the stream should be looped but
+ * 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+.
+ *
+ * If either the [Ringtone] or [MediaPlayer] fails to play the requested audio, an
+ * [in-app fallback][.getFallbackRingtoneUri] is used because playing **some**
+ * sort of noise is always preferable to remaining silent.
+ */
+class AsyncRingtonePlayer(private val mContext: Context) {
+    /** Handler running on the ringtone thread.  */
+    private var mHandler: Handler? = null
+
+    /** [MediaPlayerPlaybackDelegate] on pre M; [RingtonePlaybackDelegate] on M+  */
+    private var mPlaybackDelegate: PlaybackDelegate? = null
+
+    /** Plays the ringtone.  */
+    fun play(ringtoneUri: Uri?, crescendoDuration: Long) {
+        LOGGER.d("Posting play.")
+        postMessage(EVENT_PLAY, ringtoneUri, crescendoDuration, 0)
+    }
+
+    /** Stops playing the ringtone.  */
+    fun stop() {
+        LOGGER.d("Posting stop.")
+        postMessage(EVENT_STOP, null, 0, 0)
+    }
+
+    /** Schedules an adjustment of the playback volume 50ms in the future.  */
+    private fun scheduleVolumeAdjustment() {
+        LOGGER.v("Adjusting volume.")
+
+        // Ensure we never have more than one volume adjustment queued.
+        mHandler!!.removeMessages(EVENT_VOLUME)
+
+        // Queue the next volume adjustment.
+        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 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 fun postMessage(
+        messageCode: Int,
+        ringtoneUri: Uri?,
+        crescendoDuration: Long,
+        delayMillis: Long
+    ) {
+        synchronized(this) {
+            if (mHandler == null) {
+                mHandler = getNewHandler()
+            }
+
+            val message = mHandler!!.obtainMessage(messageCode)
+            if (ringtoneUri != null) {
+                val bundle = Bundle()
+                bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri)
+                bundle.putLong(CRESCENDO_DURATION_KEY, crescendoDuration)
+                message.data = bundle
+            }
+
+            mHandler!!.sendMessageDelayed(message, delayMillis)
+        }
+    }
+
+    /**
+     * Creates a new ringtone Handler running in its own thread.
+     */
+    @SuppressLint("HandlerLeak")
+    private fun getNewHandler(): Handler {
+            val thread = HandlerThread("ringtone-player")
+            thread.start()
+
+            return object : Handler(thread.looper) {
+                override fun handleMessage(msg: Message) {
+                    when (msg.what) {
+                        EVENT_PLAY -> {
+                            val data = msg.data
+                            val ringtoneUri = data.getParcelable<Uri>(RINGTONE_URI_KEY)
+                            val crescendoDuration = data.getLong(CRESCENDO_DURATION_KEY)
+                            if (playbackDelegate.play(mContext, ringtoneUri, crescendoDuration)) {
+                                scheduleVolumeAdjustment()
+                            }
+                        }
+                        EVENT_STOP -> playbackDelegate.stop(mContext)
+                        EVENT_VOLUME -> if (playbackDelegate.adjustVolume(mContext)) {
+                            scheduleVolumeAdjustment()
+                        }
+                    }
+                }
+            }
+        }
+
+    /**
+     * Check if the executing thread is the one dedicated to controlling the ringtone playback.
+     */
+    private fun checkAsyncRingtonePlayerThread() {
+        if (Looper.myLooper() != mHandler!!.looper) {
+            LOGGER.e("Must be on the AsyncRingtonePlayer thread!",
+                    IllegalStateException())
+        }
+    }
+
+    /**
+     * @return the platform-specific playback delegate to use to play the ringtone
+     */
+    private val playbackDelegate: PlaybackDelegate
+        get() {
+            checkAsyncRingtonePlayerThread()
+            if (mPlaybackDelegate == null) {
+                mPlaybackDelegate = if (Utils.isMOrLater) {
+                    // Use the newer Ringtone-based playback delegate because it does not require
+                    // any permissions to read from the SD card. (M+)
+                    RingtonePlaybackDelegate()
+                } else {
+                    // Fall back to the older MediaPlayer-based playback delegate because it is the
+                    // only way to force the looping of the ringtone before M. (pre M)
+                    MediaPlayerPlaybackDelegate()
+                }
+            }
+            return mPlaybackDelegate!!
+        }
+
+    /**
+     * This interface abstracts away the differences between playing ringtones via [Ringtone]
+     * vs [MediaPlayer].
+     */
+    private interface PlaybackDelegate {
+        /**
+         * @return `true` iff a [volume adjustment][.adjustVolume] should be scheduled
+         */
+        fun play(context: Context, ringtoneUri: Uri?, crescendoDuration: Long): Boolean
+
+        /**
+         * Stop any ongoing ringtone playback.
+         */
+        fun stop(context: Context?)
+
+        /**
+         * @return `true` iff another volume adjustment should be scheduled
+         */
+        fun adjustVolume(context: Context?): Boolean
+    }
+
+    /**
+     * Loops playback of a ringtone using [MediaPlayer].
+     */
+    private inner class MediaPlayerPlaybackDelegate : PlaybackDelegate {
+        /** The audio focus manager. Only used by the ringtone thread.  */
+        private var mAudioManager: AudioManager? = null
+
+        /** Non-`null` while playing a ringtone; `null` otherwise.  */
+        private var mMediaPlayer: MediaPlayer? = null
+
+        /** The duration over which to increase the volume.  */
+        private var mCrescendoDuration: Long = 0
+
+        /** The time at which the crescendo shall cease; 0 if no crescendo is present.  */
+        private var mCrescendoStopTime: Long = 0
+
+        /**
+         * Starts the actual playback of the ringtone. Executes on ringtone-thread.
+         */
+        override fun play(context: Context, ringtoneUri: Uri?, crescendoDuration: Long): Boolean {
+            checkAsyncRingtonePlayerThread()
+            mCrescendoDuration = crescendoDuration
+
+            LOGGER.i("Play ringtone via android.media.MediaPlayer.")
+
+            if (mAudioManager == null) {
+                mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+            }
+
+            val inTelephoneCall = isInTelephoneCall(context)
+            var alarmNoise = if (inTelephoneCall) getInCallRingtoneUri(context) else 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")
+            }
+
+            mMediaPlayer = MediaPlayer()
+            mMediaPlayer!!.setOnErrorListener { _, _, _ ->
+                LOGGER.e("Error occurred while playing audio. Stopping AlarmKlaxon.")
+                stop(context)
+                true
+            }
+
+            try {
+                // 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!!)
+
+                return startPlayback(inTelephoneCall)
+            } catch (t: Throwable) {
+                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))
+                    return startPlayback(inTelephoneCall)
+                } catch (t2: Throwable) {
+                    // At this point we just don't play anything.
+                    LOGGER.e("Failed to play fallback ringtone", t2)
+                }
+            }
+
+            return false
+        }
+
+        /**
+         * Prepare the MediaPlayer for playback if the alarm stream is not muted, then start the
+         * playback.
+         *
+         * @param inTelephoneCall `true` if there is currently an active telephone call
+         * @return `true` if a crescendo has started and future volume adjustments are
+         * required to advance the crescendo effect
+         */
+        @Throws(IOException::class)
+        private fun startPlayback(inTelephoneCall: Boolean): Boolean {
+            // Do not play alarms if stream volume is 0 (typically because ringer mode is silent).
+            if (mAudioManager!!.getStreamVolume(AudioManager.STREAM_ALARM) == 0) {
+                return false
+            }
+
+            // Indicate the ringtone should be played via the alarm stream.
+            if (Utils.isLOrLater) {
+                mMediaPlayer!!.setAudioAttributes(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.
+            var 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(0f, 0f)
+
+                // Compute the time at which the crescendo will stop.
+                mCrescendoStopTime = Utils.now() + mCrescendoDuration
+                scheduleVolumeAdjustment = true
+            }
+
+            mMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_ALARM)
+            mMediaPlayer!!.isLooping = true
+            mMediaPlayer!!.prepare()
+            mAudioManager!!.requestAudioFocus(null, AudioManager.STREAM_ALARM,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+            mMediaPlayer!!.start()
+
+            return scheduleVolumeAdjustment
+        }
+
+        /**
+         * Stops the playback of the ringtone. Executes on the ringtone-thread.
+         */
+        override fun stop(context: Context?) {
+            checkAsyncRingtonePlayerThread()
+
+            LOGGER.i("Stop ringtone via android.media.MediaPlayer.")
+
+            mCrescendoDuration = 0
+            mCrescendoStopTime = 0
+
+            // Stop audio playing
+            if (mMediaPlayer != null) {
+                mMediaPlayer?.stop()
+                mMediaPlayer?.release()
+                mMediaPlayer = null
+            }
+
+            if (mAudioManager != null) {
+                mAudioManager?.abandonAudioFocus(null)
+            }
+        }
+
+        /**
+         * Adjusts the volume of the ringtone being played to create a crescendo effect.
+         */
+        override fun adjustVolume(context: Context?): Boolean {
+            checkAsyncRingtonePlayerThread()
+
+            // If media player is absent or not playing, ignore volume adjustment.
+            if (mMediaPlayer == null || !mMediaPlayer!!.isPlaying) {
+                mCrescendoDuration = 0
+                mCrescendoStopTime = 0
+                return false
+            }
+
+            // If the crescendo is complete set the volume to the maximum; we're done.
+            val currentTime = Utils.now()
+            if (currentTime > mCrescendoStopTime) {
+                mCrescendoDuration = 0
+                mCrescendoStopTime = 0
+                mMediaPlayer!!.setVolume(1f, 1f)
+                return false
+            }
+
+            // The current volume of the crescendo is the percentage of the crescendo completed.
+            val volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration)
+            mMediaPlayer!!.setVolume(volume, volume)
+            LOGGER.i("MediaPlayer volume set to $volume")
+
+            // Schedule the next volume bump in the crescendo.
+            return true
+        }
+    }
+
+    /**
+     * Loops playback of a ringtone using [Ringtone].
+     */
+    private inner class RingtonePlaybackDelegate : PlaybackDelegate {
+        /** The audio focus manager. Only used by the ringtone thread.  */
+        private var mAudioManager: AudioManager? = null
+
+        /** The current ringtone. Only used by the ringtone thread.  */
+        private var mRingtone: Ringtone? = null
+
+        /** The method to adjust playback volume; cannot be null.  */
+        private lateinit var mSetVolumeMethod: Method
+
+        /** The method to adjust playback looping; cannot be null.  */
+        private lateinit var mSetLoopingMethod: Method
+
+        /** The duration over which to increase the volume.  */
+        private var mCrescendoDuration: Long = 0
+
+        /** The time at which the crescendo shall cease; 0 if no crescendo is present.  */
+        private var mCrescendoStopTime: Long = 0
+
+        init {
+            try {
+                mSetVolumeMethod = Ringtone::class.java.getDeclaredMethod("setVolume",
+                        Float::class.javaPrimitiveType)
+            } catch (nsme: NoSuchMethodException) {
+                LOGGER.e("Unable to locate method: Ringtone.setVolume(float).", nsme)
+            }
+            try {
+                mSetLoopingMethod = Ringtone::class.java.getDeclaredMethod("setLooping",
+                        Boolean::class.javaPrimitiveType)
+            } catch (nsme: NoSuchMethodException) {
+                LOGGER.e("Unable to locate method: Ringtone.setLooping(boolean).", nsme)
+            }
+        }
+
+        /**
+         * Starts the actual playback of the ringtone. Executes on ringtone-thread.
+         */
+        override fun play(context: Context, ringtoneUri: Uri?, crescendoDuration: Long): Boolean {
+            var ringtoneUriVariable = ringtoneUri
+            checkAsyncRingtonePlayerThread()
+            mCrescendoDuration = crescendoDuration
+
+            LOGGER.i("Play ringtone via android.media.Ringtone.")
+
+            if (mAudioManager == null) {
+                mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+            }
+
+            val inTelephoneCall = isInTelephoneCall(context)
+            if (inTelephoneCall) {
+                ringtoneUriVariable = getInCallRingtoneUri(context)
+            }
+
+            // Attempt to fetch the specified ringtone.
+            mRingtone = RingtoneManager.getRingtone(context, ringtoneUriVariable)
+
+            if (mRingtone == null) {
+                // Fall back to the system default ringtone.
+                ringtoneUriVariable = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+                mRingtone = RingtoneManager.getRingtone(context, ringtoneUriVariable)
+            }
+
+            // Attempt to enable looping the ringtone.
+            try {
+                mSetLoopingMethod.invoke(mRingtone, true)
+            } catch (e: Exception) {
+                LOGGER.e("Unable to turn looping on for android.media.Ringtone", e)
+
+                // Fall back to the default ringtone if looping could not be enabled.
+                // (Default alarm ringtone most likely has looping tags set within the .ogg file)
+                mRingtone = null
+            }
+
+            // 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.")
+                ringtoneUriVariable = getFallbackRingtoneUri(context)
+                mRingtone = RingtoneManager.getRingtone(context, ringtoneUriVariable)
+            }
+
+            try {
+                return startPlayback(inTelephoneCall)
+            } catch (t: Throwable) {
+                LOGGER.e("Using the fallback ringtone, could not play $ringtoneUriVariable", 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 (t2: Throwable) {
+                    // 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 `true` if there is currently an active telephone call
+         * @return `true` if a crescendo has started and future volume adjustments are
+         * required to advance the crescendo effect
+         */
+        private fun startPlayback(inTelephoneCall: Boolean): Boolean {
+            // Indicate the ringtone should be played via the alarm stream.
+            if (Utils.isLOrLater) {
+                mRingtone!!.audioAttributes = AudioAttributes.Builder()
+                        .setUsage(AudioAttributes.USAGE_ALARM)
+                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                        .build()
+            }
+
+            // Attempt to adjust the ringtone volume if the user is in a telephone call.
+            var scheduleVolumeAdjustment = false
+            if (inTelephoneCall) {
+                LOGGER.v("Using the in-call alarm")
+                setRingtoneVolume(IN_CALL_VOLUME)
+            } else if (mCrescendoDuration > 0) {
+                setRingtoneVolume(0f)
+
+                // Compute the time at which the crescendo will stop.
+                mCrescendoStopTime = Utils.now() + mCrescendoDuration
+                scheduleVolumeAdjustment = true
+            }
+
+            mAudioManager!!.requestAudioFocus(null, AudioManager.STREAM_ALARM,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+
+            mRingtone!!.play()
+
+            return scheduleVolumeAdjustment
+        }
+
+        /**
+         * Sets the volume of the ringtone.
+         *
+         * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
+         * corresponds to no attenuation being applied.
+         */
+        private fun setRingtoneVolume(volume: Float) {
+            try {
+                mSetVolumeMethod.invoke(mRingtone, volume)
+            } catch (e: Exception) {
+                LOGGER.e("Unable to set volume for android.media.Ringtone", e)
+            }
+        }
+
+        /**
+         * Stops the playback of the ringtone. Executes on the ringtone-thread.
+         */
+        override fun stop(context: Context?) {
+            checkAsyncRingtonePlayerThread()
+
+            LOGGER.i("Stop ringtone via android.media.Ringtone.")
+
+            mCrescendoDuration = 0
+            mCrescendoStopTime = 0
+
+            if (mRingtone != null && mRingtone!!.isPlaying) {
+                LOGGER.d("Ringtone.stop() invoked.")
+                mRingtone!!.stop()
+            }
+
+            mRingtone = null
+
+            if (mAudioManager != null) {
+                mAudioManager!!.abandonAudioFocus(null)
+            }
+        }
+
+        /**
+         * Adjusts the volume of the ringtone being played to create a crescendo effect.
+         */
+        override fun adjustVolume(context: Context?): Boolean {
+            checkAsyncRingtonePlayerThread()
+
+            // If ringtone is absent or not playing, ignore volume adjustment.
+            if (mRingtone == null || !mRingtone!!.isPlaying) {
+                mCrescendoDuration = 0
+                mCrescendoStopTime = 0
+                return false
+            }
+
+            // If the crescendo is complete set the volume to the maximum; we're done.
+            val currentTime = Utils.now()
+            if (currentTime > mCrescendoStopTime) {
+                mCrescendoDuration = 0
+                mCrescendoStopTime = 0
+                setRingtoneVolume(1f)
+                return false
+            }
+
+            val volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration)
+            setRingtoneVolume(volume)
+
+            // Schedule the next volume bump in the crescendo.
+            return true
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("AsyncRingtonePlayer")
+
+        // Volume suggested by media team for in-call alarms.
+        private const val IN_CALL_VOLUME = 0.125f
+
+        // Message codes used with the ringtone thread.
+        private const val EVENT_PLAY = 1
+        private const val EVENT_STOP = 2
+        private const val EVENT_VOLUME = 3
+        private const val RINGTONE_URI_KEY = "RINGTONE_URI_KEY"
+        private const val CRESCENDO_DURATION_KEY = "CRESCENDO_DURATION_KEY"
+
+        /**
+         * @return `true` iff the device is currently in a telephone call
+         */
+        private fun isInTelephoneCall(context: Context): Boolean {
+            val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+            return tm.callState != TelephonyManager.CALL_STATE_IDLE
+        }
+
+        /**
+         * @return Uri of the ringtone to play when the user is in a telephone call
+         */
+        private fun getInCallRingtoneUri(context: Context): Uri {
+            return Utils.getResourceUri(context, R.raw.alarm_expire)
+        }
+
+        /**
+         * @return Uri of the ringtone to play when the chosen ringtone fails to play
+         */
+        private fun getFallbackRingtoneUri(context: Context): Uri {
+            return Utils.getResourceUri(context, R.raw.alarm_expire)
+        }
+
+        /**
+         * @param currentTime current time of the device
+         * @param stopTime time at which the crescendo finishes
+         * @param duration length of time over which the crescendo occurs
+         * @return the scalar volume value that produces a linear increase in volume (in decibels)
+         */
+        private fun computeVolume(currentTime: Long, stopTime: Long, duration: Long): Float {
+            // Compute the percentage of the crescendo that has completed.
+            val elapsedCrescendoTime = stopTime - currentTime.toFloat()
+            val fractionComplete = 1 - elapsedCrescendoTime / duration
+
+            // Use the fraction to compute a target decibel between
+            // -40dB (near silent) and 0dB (max).
+            val gain = fractionComplete * 40 - 40
+
+            // Convert the target gain (in decibels) into the corresponding volume scalar.
+            val volume = 10.0.pow(gain / 20f.toDouble()).toFloat()
+
+            LOGGER.v("Ringtone crescendo %,.2f%% complete (scalar: %f, volume: %f dB)",
+                    fractionComplete * 100, volume, gain)
+
+            return volume
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/BaseActivity.java b/src/com/android/deskclock/BaseActivity.java
deleted file mode 100644
index 143f238..0000000
--- a/src/com/android/deskclock/BaseActivity.java
+++ /dev/null
@@ -1,123 +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.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Bundle;
-import androidx.annotation.ColorInt;
-import androidx.appcompat.app.AppCompatActivity;
-import android.view.View;
-
-import static com.android.deskclock.AnimatorUtils.ARGB_EVALUATOR;
-
-/**
- * Base activity class that changes the app window's color based on the current hour.
- */
-public abstract class BaseActivity extends AppCompatActivity {
-
-    /** Sets the app window color on each frame of the {@link #mAppColorAnimator}. */
-    private final AppColorAnimationListener mAppColorAnimationListener
-            = new AppColorAnimationListener();
-
-    /** The current animator that is changing the app window color or {@code null}. */
-    private ValueAnimator mAppColorAnimator;
-
-    /** Draws the app window's color. */
-    private ColorDrawable mBackground;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        // 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();
-
-        // Ensure the app window color is up-to-date.
-        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 animate {@code true} if the change should be animated
-     */
-    protected void adjustAppColor(@ColorInt int color, boolean animate) {
-        // Create and install the drawable that defines the window color.
-        if (mBackground == null) {
-            mBackground = new ColorDrawable(color);
-            getWindow().setBackgroundDrawable(mBackground);
-        }
-
-        // Cancel the current window color animation if one exists.
-        if (mAppColorAnimator != null) {
-            mAppColorAnimator.cancel();
-        }
-
-        final @ColorInt int currentColor = mBackground.getColor();
-        if (currentColor != color) {
-            if (animate) {
-                mAppColorAnimator = ValueAnimator.ofObject(ARGB_EVALUATOR, currentColor, color)
-                        .setDuration(3000L);
-                mAppColorAnimator.addUpdateListener(mAppColorAnimationListener);
-                mAppColorAnimator.addListener(mAppColorAnimationListener);
-                mAppColorAnimator.start();
-            } else {
-                setAppColor(color);
-            }
-        }
-    }
-
-    private void setAppColor(@ColorInt int color) {
-        mBackground.setColor(color);
-    }
-
-    /**
-     * Sets the app window color to the current color produced by the animator.
-     */
-    private final class AppColorAnimationListener extends AnimatorListenerAdapter
-            implements AnimatorUpdateListener {
-        @Override
-        public void onAnimationUpdate(ValueAnimator valueAnimator) {
-            final @ColorInt int color = (int) valueAnimator.getAnimatedValue();
-            setAppColor(color);
-        }
-
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            if (mAppColorAnimator == animation) {
-                mAppColorAnimator = null;
-            }
-        }
-    }
-}
diff --git a/src/com/android/deskclock/BaseActivity.kt b/src/com/android/deskclock/BaseActivity.kt
new file mode 100644
index 0000000..79213c5
--- /dev/null
+++ b/src/com/android/deskclock/BaseActivity.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.R
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Base activity class that changes the app window's color based on the current hour.
+ */
+abstract class BaseActivity : AppCompatActivity() {
+    /** Sets the app window color on each frame of the [.mAppColorAnimator].  */
+    private val mAppColorAnimationListener = AppColorAnimationListener()
+
+    /** The current animator that is changing the app window color or `null`.  */
+    private var mAppColorAnimator: ValueAnimator? = null
+
+    /** Draws the app window's color.  */
+    private var mBackground: ColorDrawable? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // Allow the content to layout behind the status and navigation bars.
+        getWindow().getDecorView().setSystemUiVisibility(
+                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                        or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                        or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
+
+        @ColorInt val color = ThemeUtils.resolveColor(this, R.attr.windowBackground)
+        adjustAppColor(color, animate = false)
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        // Ensure the app window color is up-to-date.
+        @ColorInt val color = ThemeUtils.resolveColor(this, R.attr.windowBackground)
+        adjustAppColor(color, animate = false)
+    }
+
+    /**
+     * 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 animate `true` if the change should be animated
+     */
+    protected fun adjustAppColor(@ColorInt color: Int, animate: Boolean) {
+        // Create and install the drawable that defines the window color.
+        if (mBackground == null) {
+            mBackground = ColorDrawable(color)
+            getWindow().setBackgroundDrawable(mBackground)
+        }
+
+        // Cancel the current window color animation if one exists.
+        mAppColorAnimator?.cancel()
+
+        @ColorInt val currentColor = mBackground!!.color
+        if (currentColor != color) {
+            if (animate) {
+                mAppColorAnimator = ValueAnimator.ofObject(AnimatorUtils.ARGB_EVALUATOR,
+                        currentColor, color)
+                        .setDuration(3000L)
+                mAppColorAnimator!!.addUpdateListener(mAppColorAnimationListener)
+                mAppColorAnimator!!.addListener(mAppColorAnimationListener)
+                mAppColorAnimator!!.start()
+            } else {
+                setAppColor(color)
+            }
+        }
+    }
+
+    private fun setAppColor(@ColorInt color: Int) {
+        mBackground!!.color = color
+    }
+
+    /**
+     * Sets the app window color to the current color produced by the animator.
+     */
+    private inner class AppColorAnimationListener
+        : AnimatorListenerAdapter(), AnimatorUpdateListener {
+        override fun onAnimationUpdate(valueAnimator: ValueAnimator) {
+            @ColorInt val color = valueAnimator.animatedValue as Int
+            setAppColor(color)
+        }
+
+        override fun onAnimationEnd(animation: Animator) {
+            if (mAppColorAnimator === animation) {
+                mAppColorAnimator = null
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/CircleButtonsLayout.java b/src/com/android/deskclock/CircleButtonsLayout.java
deleted file mode 100644
index 079472e..0000000
--- a/src/com/android/deskclock/CircleButtonsLayout.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package com.android.deskclock;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-/**
- * This class adjusts the locations of children buttons and text of this view group by adjusting the
- * margins of each item. The left and right buttons are aligned with the bottom of the circle. The
- * stop button and label text are located within the circle with the stop button near the bottom and
- * the label text near the top. The maximum text size for the label text view is also calculated.
- */
-public class CircleButtonsLayout extends FrameLayout {
-
-    private float mDiamOffset;
-    private View mCircleView;
-    private Button mResetAddButton;
-    private TextView mLabel;
-
-    @SuppressWarnings("unused")
-    public CircleButtonsLayout(Context context) {
-        this(context, null);
-    }
-
-    public CircleButtonsLayout(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        final Resources res = getContext().getResources();
-        final float strokeSize = res.getDimension(R.dimen.circletimer_circle_size);
-        final float dotStrokeSize = res.getDimension(R.dimen.circletimer_dot_size);
-        final float markerStrokeSize = res.getDimension(R.dimen.circletimer_marker_size);
-        mDiamOffset = Utils.calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize) * 2;
-    }
-
-    @Override
-    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        // We must call onMeasure both before and after re-measuring our views because the circle
-        // may not always be drawn here yet. The first onMeasure will force the circle to be drawn,
-        // and the second will force our re-measurements to take effect.
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        remeasureViews();
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-
-    protected void remeasureViews() {
-        if (mLabel == null) {
-            mCircleView = findViewById(R.id.timer_time);
-            mLabel = (TextView) findViewById(R.id.timer_label);
-            mResetAddButton = (Button) findViewById(R.id.reset_add);
-        }
-
-        final int frameWidth = mCircleView.getMeasuredWidth();
-        final int frameHeight = mCircleView.getMeasuredHeight();
-        final int minBound = Math.min(frameWidth, frameHeight);
-        final int circleDiam = (int) (minBound - mDiamOffset);
-
-        if (mResetAddButton != null) {
-            final MarginLayoutParams resetAddParams = (MarginLayoutParams) mResetAddButton
-                    .getLayoutParams();
-            resetAddParams.bottomMargin = circleDiam / 6;
-            if (minBound == frameWidth) {
-                resetAddParams.bottomMargin += (frameHeight - frameWidth) / 2;
-            }
-        }
-
-        if (mLabel != null) {
-            MarginLayoutParams labelParams = (MarginLayoutParams) mLabel.getLayoutParams();
-            labelParams.topMargin = circleDiam/6;
-            if (minBound == frameWidth) {
-                labelParams.topMargin += (frameHeight-frameWidth)/2;
-            }
-            /* The following formula has been simplified based on the following:
-             * Our goal is to calculate the maximum width for the label frame.
-             * We may do this with the following diagram to represent the top half of the circle:
-             *                 ___
-             *            .     |     .
-             *        ._________|         .
-             *     .       ^    |            .
-             *   /         x    |              \
-             *  |_______________|_______________|
-             *
-             *  where x represents the value we would like to calculate, and the final width of the
-             *  label will be w = 2 * x.
-             *
-             *  We may find x by drawing a right triangle from the center of the circle:
-             *                 ___
-             *            .     |     .
-             *        ._________|         .
-             *     .    .       |            .
-             *   /          .   | }y           \
-             *  |_____________.t|_______________|
-             *
-             *  where t represents the angle of that triangle, and y is the height of that triangle.
-             *
-             *  If r = radius of the circle, we know the following trigonometric identities:
-             *        cos(t) = y / r
-             *  and   sin(t) = x / r
-             *     => r * sin(t) = x
-             *  and   sin^2(t) = 1 - cos^2(t)
-             *     => sin(t) = +/- sqrt(1 - cos^2(t))
-             *  (note: because we need the positive value, we may drop the +/-).
-             *
-             *  To calculate the final width, we may combine our formulas:
-             *        w = 2 * x
-             *     => w = 2 * r * sin(t)
-             *     => w = 2 * r * sqrt(1 - cos^2(t))
-             *     => w = 2 * r * sqrt(1 - (y / r)^2)
-             *
-             *  Simplifying even further, to mitigate the complexity of the final formula:
-             *        sqrt(1 - (y / r)^2)
-             *     => sqrt(1 - (y^2 / r^2))
-             *     => sqrt((r^2 / r^2) - (y^2 / r^2))
-             *     => sqrt((r^2 - y^2) / (r^2))
-             *     => sqrt(r^2 - y^2) / sqrt(r^2)
-             *     => sqrt(r^2 - y^2) / r
-             *     => sqrt((r + y)*(r - y)) / r
-             *
-             * Placing this back in our formula, we end up with, as our final, reduced equation:
-             *        w = 2 * r * sqrt(1 - (y / r)^2)
-             *     => w = 2 * r * sqrt((r + y)*(r - y)) / r
-             *     => w = 2 * sqrt((r + y)*(r - y))
-             */
-            // Radius of the circle.
-            int r = circleDiam / 2;
-            // Y value of the top of the label, calculated from the center of the circle.
-            int y = frameHeight / 2 - labelParams.topMargin;
-            // New maximum width of the label.
-            double w = 2 * Math.sqrt((r + y) * (r - y));
-
-            mLabel.setMaxWidth((int) w);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/CircleButtonsLayout.kt b/src/com/android/deskclock/CircleButtonsLayout.kt
new file mode 100644
index 0000000..a1dae04
--- /dev/null
+++ b/src/com/android/deskclock/CircleButtonsLayout.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util.AttributeSet
+import android.view.View
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.TextView
+
+import kotlin.math.min
+import kotlin.math.sqrt
+
+/**
+ * This class adjusts the locations of child buttons and text of this view group by adjusting the
+ * margins of each item. The left and right buttons are aligned with the bottom of the circle. The
+ * stop button and label text are located within the circle with the stop button near the bottom and
+ * the label text near the top. The maximum text size for the label text view is also calculated.
+ */
+class CircleButtonsLayout @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : FrameLayout(context, attrs) {
+    private val mDiamOffset: Float
+    private var mCircleView: View? = null
+    private var mResetAddButton: Button? = null
+    private var mLabel: TextView? = null
+
+    init {
+        val res = getContext().resources
+        val strokeSize = res.getDimension(R.dimen.circletimer_circle_size)
+        val dotStrokeSize = res.getDimension(R.dimen.circletimer_dot_size)
+        val markerStrokeSize = res.getDimension(R.dimen.circletimer_marker_size)
+        mDiamOffset = Utils.calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize) * 2
+    }
+
+    public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        // We must call onMeasure both before and after re-measuring our views because the circle
+        // may not always be drawn here yet. The first onMeasure will force the circle to be drawn,
+        // and the second will force our re-measurements to take effect.
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        remeasureViews()
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+    }
+
+    private fun remeasureViews() {
+        if (mLabel == null) {
+            mCircleView = findViewById(R.id.timer_time)
+            mLabel = findViewById<View>(R.id.timer_label) as TextView
+            mResetAddButton = findViewById<View>(R.id.reset_add) as Button
+        }
+
+        val frameWidth = mCircleView!!.measuredWidth
+        val frameHeight = mCircleView!!.measuredHeight
+        val minBound = min(frameWidth, frameHeight)
+        val circleDiam = (minBound - mDiamOffset).toInt()
+
+        mResetAddButton?.let {
+            val resetAddParams = it.layoutParams as MarginLayoutParams
+            resetAddParams.bottomMargin = circleDiam / 6
+            if (minBound == frameWidth) {
+                resetAddParams.bottomMargin += (frameHeight - frameWidth) / 2
+            }
+        }
+
+        mLabel?.let {
+            val labelParams = it.layoutParams as MarginLayoutParams
+            labelParams.topMargin = circleDiam / 6
+            if (minBound == frameWidth) {
+                labelParams.topMargin += (frameHeight - frameWidth) / 2
+            }
+            /* The following formula has been simplified based on the following:
+             * Our goal is to calculate the maximum width for the label frame.
+             * We may do this with the following diagram to represent the top half of the circle:
+             *                 ___
+             *            .     |     .
+             *        ._________|         .
+             *     .       ^    |            .
+             *   /         x    |              \
+             *  |_______________|_______________|
+             *
+             *  where x represents the value we would like to calculate, and the final width of the
+             *  label will be w = 2 * x.
+             *
+             *  We may find x by drawing a right triangle from the center of the circle:
+             *                 ___
+             *            .     |     .
+             *        ._________|         .
+             *     .    .       |            .
+             *   /          .   | }y           \
+             *  |_____________.t|_______________|
+             *
+             *  where t represents the angle of that triangle, and y is the height of that triangle.
+             *
+             *  If r = radius of the circle, we know the following trigonometric identities:
+             *        cos(t) = y / r
+             *  and   sin(t) = x / r
+             *     => r * sin(t) = x
+             *  and   sin^2(t) = 1 - cos^2(t)
+             *     => sin(t) = +/- sqrt(1 - cos^2(t))
+             *  (note: because we need the positive value, we may drop the +/-).
+             *
+             *  To calculate the final width, we may combine our formulas:
+             *        w = 2 * x
+             *     => w = 2 * r * sin(t)
+             *     => w = 2 * r * sqrt(1 - cos^2(t))
+             *     => w = 2 * r * sqrt(1 - (y / r)^2)
+             *
+             *  Simplifying even further, to mitigate the complexity of the final formula:
+             *        sqrt(1 - (y / r)^2)
+             *     => sqrt(1 - (y^2 / r^2))
+             *     => sqrt((r^2 / r^2) - (y^2 / r^2))
+             *     => sqrt((r^2 - y^2) / (r^2))
+             *     => sqrt(r^2 - y^2) / sqrt(r^2)
+             *     => sqrt(r^2 - y^2) / r
+             *     => sqrt((r + y)*(r - y)) / r
+             *
+             * Placing this back in our formula, we end up with, as our final, reduced equation:
+             *        w = 2 * r * sqrt(1 - (y / r)^2)
+             *     => w = 2 * r * sqrt((r + y)*(r - y)) / r
+             *     => w = 2 * sqrt((r + y)*(r - y))
+             */
+            // Radius of the circle.
+            val r = circleDiam / 2
+            // Y value of the top of the label, calculated from the center of the circle.
+            val y = frameHeight / 2 - labelParams.topMargin
+            // New maximum width of the label.
+            val w = 2 * sqrt((r + y) * (r - y).toDouble())
+
+            it.maxWidth = w.toInt()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ClockFragment.java b/src/com/android/deskclock/ClockFragment.java
deleted file mode 100644
index 1536b55..0000000
--- a/src/com/android/deskclock/ClockFragment.java
+++ /dev/null
@@ -1,547 +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;
-
-import android.app.Activity;
-import android.app.AlarmManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.provider.Settings;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.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.Button;
-import android.widget.ImageView;
-import android.widget.TextClock;
-import android.widget.TextView;
-
-import com.android.deskclock.data.City;
-import com.android.deskclock.data.CityListener;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.worldclock.CitySelectionActivity;
-
-import java.util.Calendar;
-import java.util.List;
-import java.util.TimeZone;
-
-import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS;
-import static java.util.Calendar.DAY_OF_WEEK;
-
-/**
- * Fragment that shows the clock (analog or digital), the next alarm info and the world clock.
- */
-public final class ClockFragment extends DeskClockFragment {
-
-    // Updates dates in the UI on every quarter-hour.
-    private final Runnable mQuarterHourUpdater = new QuarterHourRunnable();
-
-    // Updates the UI in response to changes to the scheduled alarm.
-    private BroadcastReceiver mAlarmChangeReceiver;
-
-    // Detects changes to the next scheduled alarm pre-L.
-    private ContentObserver mAlarmObserver;
-
-    private TextClock mDigitalClock;
-    private AnalogClock mAnalogClock;
-    private View mClockFrame;
-    private SelectedCitiesAdapter mCityAdapter;
-    private RecyclerView mCityList;
-    private String mDateFormat;
-    private String mDateFormatForAccessibility;
-
-    /**
-     * The public no-arg constructor required by all fragments.
-     */
-    public ClockFragment() {
-        super(CLOCKS);
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        mAlarmObserver = Utils.isPreL() ? new AlarmObserverPreL() : null;
-        mAlarmChangeReceiver = Utils.isLOrLater() ? new AlarmChangedBroadcastReceiver() : null;
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
-        super.onCreateView(inflater, container, icicle);
-
-        final View fragmentView = inflater.inflate(R.layout.clock_fragment, container, false);
-
-        mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
-        mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
-
-        mCityAdapter = new SelectedCitiesAdapter(getActivity(), mDateFormat,
-                mDateFormatForAccessibility);
-
-        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();
-        mCityList.addOnScrollListener(scrollPositionWatcher);
-
-        final Context context = container.getContext();
-        mCityList.setOnTouchListener(new CityListOnLongClickListener(context));
-        fragmentView.setOnLongClickListener(new StartScreenSaverListener());
-
-        // On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added
-        // on as a header to the main listview.
-        mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane);
-        if (mClockFrame != null) {
-            mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock);
-            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.
-        UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater, 100);
-
-        return fragmentView;
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        final Activity activity = getActivity();
-
-        mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
-        mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
-
-        // Watch for system events that effect clock time or format.
-        if (mAlarmChangeReceiver != null) {
-            final IntentFilter filter = new IntentFilter(ACTION_NEXT_ALARM_CLOCK_CHANGED);
-            activity.registerReceiver(mAlarmChangeReceiver, filter);
-        }
-
-        // 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();
-        if (view != null && view.findViewById(R.id.main_clock_left_pane) != null) {
-            // Center the main clock frame by hiding the world clocks when none are selected.
-            mCityList.setVisibility(mCityAdapter.getItemCount() == 0 ? GONE : VISIBLE);
-        }
-
-        refreshAlarm();
-
-        // Alarm observer is null on L or later.
-        if (mAlarmObserver != null) {
-            @SuppressWarnings("deprecation")
-            final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
-            activity.getContentResolver().registerContentObserver(uri, false, mAlarmObserver);
-        }
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-
-        final Activity activity = getActivity();
-        if (mAlarmChangeReceiver != null) {
-            activity.unregisterReceiver(mAlarmChangeReceiver);
-        }
-        if (mAlarmObserver != null) {
-            activity.getContentResolver().unregisterContentObserver(mAlarmObserver);
-        }
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-        UiDataModel.getUiDataModel().removePeriodicCallback(mQuarterHourUpdater);
-        DataModel.getDataModel().removeCityListener(mCityAdapter);
-    }
-
-    @Override
-    public void onFabClick(@NonNull ImageView fab) {
-        startActivity(new Intent(getActivity(), CitySelectionActivity.class));
-    }
-
-    @Override
-    public void onUpdateFab(@NonNull ImageView fab) {
-        fab.setVisibility(VISIBLE);
-        fab.setImageResource(R.drawable.ic_public);
-        fab.setContentDescription(fab.getResources().getString(R.string.button_cities));
-    }
-
-    @Override
-    public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
-        left.setVisibility(INVISIBLE);
-        right.setVisibility(INVISIBLE);
-    }
-
-    /**
-     * Refresh the next alarm time.
-     */
-    private void refreshAlarm() {
-        if (mClockFrame != null) {
-            Utils.refreshAlarm(getActivity(), mClockFrame);
-        } else {
-            mCityAdapter.refreshAlarm();
-        }
-    }
-
-    /**
-     * Long pressing over the main clock starts the screen saver.
-     */
-    private final class StartScreenSaverListener implements View.OnLongClickListener {
-
-        @Override
-        public boolean onLongClick(View view) {
-            startActivity(new Intent(getActivity(), ScreensaverActivity.class)
-                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                    .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock));
-            return true;
-        }
-    }
-
-    /**
-     * Long pressing over the city list starts the screen saver.
-     */
-    private final class CityListOnLongClickListener extends GestureDetector.SimpleOnGestureListener
-            implements View.OnTouchListener {
-
-        private final GestureDetector mGestureDetector;
-
-        private CityListOnLongClickListener(Context context) {
-            mGestureDetector = new GestureDetector(context, this);
-        }
-
-        @Override
-        public void onLongPress(MotionEvent e) {
-            final View view = getView();
-            if (view != null) {
-                view.performLongClick();
-            }
-        }
-
-        @Override
-        public boolean onDown(MotionEvent e) {
-            return true;
-        }
-
-        @Override
-        public boolean onTouch(View v, MotionEvent event) {
-            return mGestureDetector.onTouchEvent(event);
-        }
-    }
-
-    /**
-     * This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and
-     * updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate
-     * the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45).
-     */
-    private final class QuarterHourRunnable implements Runnable {
-        @Override
-        public void run() {
-            mCityAdapter.notifyDataSetChanged();
-        }
-    }
-
-    /**
-     * Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm.
-     * In L and beyond this is accomplished via a system broadcast of
-     * {@link AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED}.
-     */
-    private final class AlarmObserverPreL extends ContentObserver {
-        private AlarmObserverPreL() {
-            super(new Handler());
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            refreshAlarm();
-        }
-    }
-
-    /**
-     * Update the display of the scheduled alarm as it changes.
-     */
-    private final class AlarmChangedBroadcastReceiver extends BroadcastReceiver {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            refreshAlarm();
-        }
-    }
-
-    /**
-     * 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.
-     */
-    private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
-            implements View.OnLayoutChangeListener {
-        @Override
-        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-            setTabScrolledToTop(Utils.isScrolledToTop(mCityList));
-        }
-
-        @Override
-        public void onLayoutChange(View v, int left, int top, int right, int bottom,
-                int oldLeft, int oldTop, int oldRight, int oldBottom) {
-            setTabScrolledToTop(Utils.isScrolledToTop(mCityList));
-        }
-    }
-
-    /**
-     * This adapter lists all of the selected world clocks. Optionally, it also includes a clock at
-     * the top for the home timezone if "Automatic home clock" is turned on in settings and the
-     * current time at home does not match the current time in the timezone of the current location.
-     * If the phone is in portrait mode it will also include the main clock at the top.
-     */
-    private static final class SelectedCitiesAdapter extends RecyclerView.Adapter
-            implements CityListener {
-
-        private final static int MAIN_CLOCK = R.layout.main_clock_frame;
-        private final static int WORLD_CLOCK = R.layout.world_clock_item;
-
-        private final LayoutInflater mInflater;
-        private final Context mContext;
-        private final boolean mIsPortrait;
-        private final boolean mShowHomeClock;
-        private final String mDateFormat;
-        private final String mDateFormatForAccessibility;
-
-        private SelectedCitiesAdapter(Context context, String dateFormat,
-                String dateFormatForAccessibility) {
-            mContext = context;
-            mDateFormat = dateFormat;
-            mDateFormatForAccessibility = dateFormatForAccessibility;
-            mInflater = LayoutInflater.from(context);
-            mIsPortrait = Utils.isPortrait(context);
-            mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            if (position == 0 && mIsPortrait) {
-                return MAIN_CLOCK;
-            }
-            return WORLD_CLOCK;
-        }
-
-        @Override
-        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-            final View view = mInflater.inflate(viewType, parent, false);
-            switch (viewType) {
-                case WORLD_CLOCK:
-                    return new CityViewHolder(view);
-                case MAIN_CLOCK:
-                    return new MainClockViewHolder(view);
-                default:
-                    throw new IllegalArgumentException("View type not recognized");
-            }
-        }
-
-        @Override
-        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
-            final int viewType = getItemViewType(position);
-            switch (viewType) {
-                case WORLD_CLOCK:
-                    // Retrieve the city to bind.
-                    final City city;
-                    // If showing home clock, put it at the top
-                    if (mShowHomeClock && position == (mIsPortrait ? 1 : 0)) {
-                        city = getHomeCity();
-                    } else {
-                        final int positionAdjuster = (mIsPortrait ? 1 : 0)
-                                + (mShowHomeClock ? 1 : 0);
-                        city = getCities().get(position - positionAdjuster);
-                    }
-                    ((CityViewHolder) holder).bind(mContext, city, position, mIsPortrait);
-                    break;
-                case MAIN_CLOCK:
-                    ((MainClockViewHolder) holder).bind(mContext, mDateFormat,
-                            mDateFormatForAccessibility, getItemCount() > 1);
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unexpected view type: " + viewType);
-            }
-        }
-
-        @Override
-        public int getItemCount() {
-            final int mainClockCount = mIsPortrait ? 1 : 0;
-            final int homeClockCount = mShowHomeClock ? 1 : 0;
-            final int worldClockCount = getCities().size();
-            return mainClockCount + homeClockCount + worldClockCount;
-        }
-
-        private City getHomeCity() {
-            return DataModel.getDataModel().getHomeCity();
-        }
-
-        private List<City> getCities() {
-            return DataModel.getDataModel().getSelectedCities();
-        }
-
-        private void refreshAlarm() {
-            if (mIsPortrait && getItemCount() > 0) {
-                notifyItemChanged(0);
-            }
-        }
-
-        @Override
-        public void citiesChanged(List<City> oldCities, List<City> newCities) {
-            notifyDataSetChanged();
-        }
-
-        private static final class CityViewHolder extends RecyclerView.ViewHolder {
-
-            private final TextView mName;
-            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);
-                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(cityTimeZoneId);
-                    mAnalogClock.enableSeconds(false);
-                } else {
-                    mAnalogClock.setVisibility(GONE);
-                    mDigitalClock.setVisibility(VISIBLE);
-                    mDigitalClock.setTimeZone(cityTimeZoneId);
-                    mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */,
-                            false));
-                    mDigitalClock.setFormat24Hour(Utils.get24ModeFormat(false));
-                }
-
-                // Supply top and bottom padding dynamically.
-                final Resources res = context.getResources();
-                final int padding = res.getDimensionPixelSize(R.dimen.medium_space_top);
-                final int top = position == 0 && !isPortrait ? 0 : padding;
-                final int left = itemView.getPaddingLeft();
-                final int right = itemView.getPaddingRight();
-                final int bottom = itemView.getPaddingBottom();
-                itemView.setPadding(left, top, right, bottom);
-
-                // Bind the city name.
-                mName.setText(city.getName());
-
-                // Compute if the city week day matches the weekday of the current timezone.
-                final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
-                final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
-                final boolean displayDayOfWeek =
-                        localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
-
-                // 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));
-                    }
-
-                }
-            }
-        }
-
-        private static final class MainClockViewHolder extends RecyclerView.ViewHolder {
-
-            private final View mHairline;
-            private final TextClock mDigitalClock;
-            private final AnalogClock mAnalogClock;
-
-            private MainClockViewHolder(View itemView) {
-                super(itemView);
-
-                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.updateDate(dateFormat, dateFormatForAccessibility, itemView);
-                Utils.setClockStyle(mDigitalClock, mAnalogClock);
-                mHairline.setVisibility(showHairline ? VISIBLE : GONE);
-
-                Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
-            }
-        }
-    }
-}
diff --git a/src/com/android/deskclock/ClockFragment.kt b/src/com/android/deskclock/ClockFragment.kt
new file mode 100644
index 0000000..bdcb235
--- /dev/null
+++ b/src/com/android/deskclock/ClockFragment.kt
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AlarmManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.os.Bundle
+import android.os.Handler
+import android.provider.Settings
+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.view.GestureDetector.SimpleOnGestureListener
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextClock
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.data.City
+import com.android.deskclock.data.CityListener
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.worldclock.CitySelectionActivity
+
+import java.util.Calendar
+import java.util.TimeZone
+
+/**
+ * Fragment that shows the clock (analog or digital), the next alarm info and the world clock.
+ */
+class ClockFragment : DeskClockFragment(UiDataModel.Tab.CLOCKS) {
+    // Updates dates in the UI on every quarter-hour.
+    private val mQuarterHourUpdater: Runnable = QuarterHourRunnable()
+
+    // Updates the UI in response to changes to the scheduled alarm.
+    private var mAlarmChangeReceiver: BroadcastReceiver? = null
+
+    // Detects changes to the next scheduled alarm pre-L.
+    private var mAlarmObserver: ContentObserver? = null
+
+    private var mDigitalClock: TextClock? = null
+    private var mAnalogClock: AnalogClock? = null
+    private var mClockFrame: View? = null
+    private lateinit var mCityAdapter: SelectedCitiesAdapter
+    private lateinit var mCityList: RecyclerView
+    private lateinit var mDateFormat: String
+    private lateinit var mDateFormatForAccessibility: String
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        mAlarmObserver = if (Utils.isPreL) AlarmObserverPreL() else null
+        mAlarmChangeReceiver = if (Utils.isLOrLater) AlarmChangedBroadcastReceiver() else null
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        icicle: Bundle?
+    ): View? {
+        super.onCreateView(inflater, container, icicle)
+
+        val fragmentView = inflater.inflate(R.layout.clock_fragment, container, false)
+
+        mDateFormat = getString(R.string.abbrev_wday_month_day_no_year)
+        mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year)
+
+        mCityAdapter = SelectedCitiesAdapter(requireActivity(), mDateFormat,
+                mDateFormatForAccessibility)
+
+        mCityList = fragmentView.findViewById<View>(R.id.cities) as RecyclerView
+        mCityList.setLayoutManager(LinearLayoutManager(requireActivity()))
+        mCityList.setAdapter(mCityAdapter)
+        mCityList.setItemAnimator(null)
+        DataModel.dataModel.addCityListener(mCityAdapter)
+
+        val scrollPositionWatcher = ScrollPositionWatcher()
+        mCityList.addOnScrollListener(scrollPositionWatcher)
+
+        val context = container!!.context
+        mCityList.setOnTouchListener(CityListOnLongClickListener(context))
+        fragmentView.setOnLongClickListener(StartScreenSaverListener())
+
+        // On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added
+        // on as a header to the main listview.
+        mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane)
+        if (mClockFrame != null) {
+            mDigitalClock = mClockFrame!!.findViewById<View>(R.id.digital_clock) as TextClock
+            mAnalogClock = mClockFrame!!.findViewById<View>(R.id.analog_clock) as AnalogClock
+            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.
+        UiDataModel.uiDataModel.addQuarterHourCallback(mQuarterHourUpdater)
+
+        return fragmentView
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        val activity = requireActivity()
+
+        mDateFormat = getString(R.string.abbrev_wday_month_day_no_year)
+        mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year)
+
+        // Watch for system events that effect clock time or format.
+        if (mAlarmChangeReceiver != null) {
+            val filter = IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)
+            activity.registerReceiver(mAlarmChangeReceiver, filter)
+        }
+
+        // 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!!)
+        }
+
+        val view = view
+        if (view?.findViewById<View?>(R.id.main_clock_left_pane) != null) {
+            // Center the main clock frame by hiding the world clocks when none are selected.
+            mCityList.setVisibility(if (mCityAdapter.getItemCount() == 0) {
+                View.GONE
+            } else {
+                View.VISIBLE
+            })
+        }
+
+        refreshAlarm()
+
+        // Alarm observer is null on L or later.
+        mAlarmObserver?.let {
+            val uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED)
+            activity.contentResolver.registerContentObserver(uri, false, it)
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+
+        val activity = requireActivity()
+        if (mAlarmChangeReceiver != null) {
+            activity.unregisterReceiver(mAlarmChangeReceiver)
+        }
+        if (mAlarmObserver != null) {
+            activity.contentResolver.unregisterContentObserver(mAlarmObserver!!)
+        }
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        UiDataModel.uiDataModel.removePeriodicCallback(mQuarterHourUpdater)
+        DataModel.dataModel.removeCityListener(mCityAdapter)
+    }
+
+    override fun onFabClick(fab: ImageView) {
+        startActivity(Intent(requireActivity(), CitySelectionActivity::class.java))
+    }
+
+    override fun onUpdateFab(fab: ImageView) {
+        fab.visibility = View.VISIBLE
+        fab.setImageResource(R.drawable.ic_public)
+        fab.contentDescription = fab.resources.getString(R.string.button_cities)
+    }
+
+    override fun onUpdateFabButtons(left: Button, right: Button) {
+        left.visibility = View.INVISIBLE
+        right.visibility = View.INVISIBLE
+    }
+
+    /**
+     * Refresh the next alarm time.
+     */
+    private fun refreshAlarm() {
+        if (mClockFrame != null) {
+            Utils.refreshAlarm(requireActivity(), mClockFrame)
+        } else {
+            mCityAdapter.refreshAlarm()
+        }
+    }
+
+    /**
+     * Long pressing over the main clock starts the screen saver.
+     */
+    private inner class StartScreenSaverListener : View.OnLongClickListener {
+        override fun onLongClick(view: View): Boolean {
+            startActivity(Intent(requireActivity(), ScreensaverActivity::class.java)
+                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock))
+            return true
+        }
+    }
+
+    /**
+     * Long pressing over the city list starts the screen saver.
+     */
+    private inner class CityListOnLongClickListener(
+        context: Context
+    ) : SimpleOnGestureListener(), View.OnTouchListener {
+        private val mGestureDetector = GestureDetector(context, this)
+
+        override fun onLongPress(e: MotionEvent) {
+            val view = view
+            view?.performLongClick()
+        }
+
+        override fun onDown(e: MotionEvent): Boolean {
+            return true
+        }
+
+        override fun onTouch(v: View, event: MotionEvent): Boolean {
+            return mGestureDetector.onTouchEvent(event)
+        }
+    }
+
+    /**
+     * This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and
+     * updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate
+     * the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45).
+     */
+    private inner class QuarterHourRunnable : Runnable {
+        override fun run() {
+            mCityAdapter.notifyDataSetChanged()
+        }
+    }
+
+    /**
+     * Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm.
+     * In L and beyond this is accomplished via a system broadcast of
+     * [AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED].
+     */
+    private inner class AlarmObserverPreL : ContentObserver(Handler()) {
+        override fun onChange(selfChange: Boolean) {
+            refreshAlarm()
+        }
+    }
+
+    /**
+     * Update the display of the scheduled alarm as it changes.
+     */
+    private inner class AlarmChangedBroadcastReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            refreshAlarm()
+        }
+    }
+
+    /**
+     * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls
+     * the recyclerview or when the size/position of elements within the recyclerview changes.
+     */
+    private inner class ScrollPositionWatcher
+        : RecyclerView.OnScrollListener(), View.OnLayoutChangeListener {
+        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mCityList))
+        }
+
+        override fun onLayoutChange(
+            v: View,
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            oldLeft: Int,
+            oldTop: Int,
+            oldRight: Int,
+            oldBottom: Int
+        ) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mCityList))
+        }
+    }
+
+    /**
+     * This adapter lists all of the selected world clocks. Optionally, it also includes a clock at
+     * the top for the home timezone if "Automatic home clock" is turned on in settings and the
+     * current time at home does not match the current time in the timezone of the current location.
+     * If the phone is in portrait mode it will also include the main clock at the top.
+     */
+    private class SelectedCitiesAdapter(
+        private val mContext: Context,
+        private val mDateFormat: String?,
+        private val mDateFormatForAccessibility: String?
+    ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CityListener {
+        private val mInflater = LayoutInflater.from(mContext)
+        private val mIsPortrait: Boolean = Utils.isPortrait(mContext)
+        private val mShowHomeClock: Boolean = DataModel.dataModel.showHomeClock
+
+        override fun getItemViewType(position: Int): Int {
+            return if (position == 0 && mIsPortrait) {
+                MAIN_CLOCK
+            } else WORLD_CLOCK
+        }
+
+        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+            val view = mInflater.inflate(viewType, parent, false)
+            return when (viewType) {
+                WORLD_CLOCK -> CityViewHolder(view)
+                MAIN_CLOCK -> MainClockViewHolder(view)
+                else -> throw IllegalArgumentException("View type not recognized")
+            }
+        }
+
+        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+            when (val viewType = getItemViewType(position)) {
+                WORLD_CLOCK -> {
+                    // Retrieve the city to bind.
+                    val city: City
+                    // If showing home clock, put it at the top
+                    city = if (mShowHomeClock && position == (if (mIsPortrait) 1 else 0)) {
+                        homeCity
+                    } else {
+                        val positionAdjuster = ((if (mIsPortrait) 1 else 0) +
+                                if (mShowHomeClock) 1 else 0)
+                        cities[position - positionAdjuster]
+                    }
+                    (holder as CityViewHolder).bind(mContext, city, position, mIsPortrait)
+                }
+                MAIN_CLOCK -> (holder as MainClockViewHolder).bind(mContext, mDateFormat,
+                        mDateFormatForAccessibility, getItemCount() > 1)
+                else -> throw IllegalArgumentException("Unexpected view type: $viewType")
+            }
+        }
+
+        override fun getItemCount(): Int {
+            val mainClockCount = if (mIsPortrait) 1 else 0
+            val homeClockCount = if (mShowHomeClock) 1 else 0
+            val worldClockCount = cities.size
+            return mainClockCount + homeClockCount + worldClockCount
+        }
+
+        private val homeCity: City
+            get() = DataModel.dataModel.homeCity
+
+        private val cities: List<City>
+            get() = DataModel.dataModel.selectedCities as List<City>
+
+        fun refreshAlarm() {
+            if (mIsPortrait && getItemCount() > 0) {
+                notifyItemChanged(0)
+            }
+        }
+
+        override fun citiesChanged(oldCities: List<City>, newCities: List<City>) {
+            notifyDataSetChanged()
+        }
+
+        private class CityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+            private val mName: TextView = itemView.findViewById(R.id.city_name)
+            private val mDigitalClock: TextClock = itemView.findViewById(R.id.digital_clock)
+            private val mAnalogClock: AnalogClock = itemView.findViewById(R.id.analog_clock)
+            private val mHoursAhead: TextView = itemView.findViewById(R.id.hours_ahead)
+
+            fun bind(context: Context, city: City, position: Int, isPortrait: Boolean) {
+                val cityTimeZoneId: String = city.timeZone.id
+
+                // Configure the digital clock or analog clock depending on the user preference.
+                if (DataModel.dataModel.clockStyle == DataModel.ClockStyle.ANALOG) {
+                    mDigitalClock.visibility = View.GONE
+                    mAnalogClock.visibility = View.VISIBLE
+                    mAnalogClock.setTimeZone(cityTimeZoneId)
+                    mAnalogClock.enableSeconds(false)
+                } else {
+                    mAnalogClock.visibility = View.GONE
+                    mDigitalClock.visibility = View.VISIBLE
+                    mDigitalClock.timeZone = cityTimeZoneId
+                    mDigitalClock.format12Hour = Utils.get12ModeFormat(0.3f, false)
+                    mDigitalClock.format24Hour = Utils.get24ModeFormat(false)
+                }
+
+                // Supply top and bottom padding dynamically.
+                val res = context.resources
+                val padding = res.getDimensionPixelSize(R.dimen.medium_space_top)
+                val top = if (position == 0 && !isPortrait) 0 else padding
+                val left: Int = itemView.paddingLeft
+                val right: Int = itemView.paddingRight
+                val bottom: Int = itemView.paddingBottom
+                itemView.setPadding(left, top, right, bottom)
+
+                // Bind the city name.
+                mName.text = city.name
+
+                // Compute if the city week day matches the weekday of the current timezone.
+                val localCal = Calendar.getInstance(TimeZone.getDefault())
+                val cityCal: Calendar = Calendar.getInstance(city.timeZone)
+                val displayDayOfWeek =
+                        localCal[Calendar.DAY_OF_WEEK] != cityCal[Calendar.DAY_OF_WEEK]
+
+                // Compare offset from UTC time on today's date (daylight savings time, etc.)
+                val currentTimeZone = TimeZone.getDefault()
+                val cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId)
+                val currentTimeMillis = System.currentTimeMillis()
+                val currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis).toLong()
+                val cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis).toLong()
+                val offsetDelta = cityUtcOffset - currentUtcOffset
+
+                val hoursDifferent = (offsetDelta / DateUtils.HOUR_IN_MILLIS).toInt()
+                val minutesDifferent = (offsetDelta / DateUtils.MINUTE_IN_MILLIS).toInt() % 60
+                val displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0L
+                val 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.
+                    val displayDifference = hoursDifferent != 0 || displayMinutes
+                    mHoursAhead.visibility = if (displayDifference) View.VISIBLE else View.GONE
+                    val timeString = Utils.createHoursDifferentString(
+                            context, displayMinutes, isAhead, hoursDifferent, minutesDifferent)
+                    mHoursAhead.text = if (displayDayOfWeek) {
+                        context.getString(if (isAhead) {
+                            R.string.world_hours_tomorrow
+                        } else {
+                            R.string.world_hours_yesterday
+                        }, timeString)
+                    } else {
+                        timeString
+                    }
+                } else {
+                    // Only tomorrow/yesterday should be shown in landscape view.
+                    mHoursAhead.visibility = if (displayDayOfWeek) View.VISIBLE else View.GONE
+                    if (displayDayOfWeek) {
+                        mHoursAhead.text = context.getString(if (isAhead) {
+                            R.string.world_tomorrow
+                        } else {
+                            R.string.world_yesterday
+                        })
+                    }
+                }
+            }
+        }
+
+        private class MainClockViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+            private val mHairline: View = itemView.findViewById(R.id.hairline)
+            private val mDigitalClock: TextClock = itemView.findViewById(R.id.digital_clock)
+            private val mAnalogClock: AnalogClock = itemView.findViewById(R.id.analog_clock)
+
+            init {
+                Utils.setClockIconTypeface(itemView)
+            }
+
+            fun bind(
+                context: Context,
+                dateFormat: String?,
+                dateFormatForAccessibility: String?,
+                showHairline: Boolean
+            ) {
+                Utils.refreshAlarm(context, itemView)
+
+                Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView)
+                Utils.setClockStyle(mDigitalClock, mAnalogClock)
+                mHairline.visibility = if (showHairline) View.VISIBLE else View.GONE
+
+                Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock)
+            }
+        }
+
+        companion object {
+            private const val MAIN_CLOCK = R.layout.main_clock_frame
+            private const val WORLD_CLOCK = R.layout.world_clock_item
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClock.java b/src/com/android/deskclock/DeskClock.java
deleted file mode 100644
index e53fbeb..0000000
--- a/src/com/android/deskclock/DeskClock.java
+++ /dev/null
@@ -1,680 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.ValueAnimator;
-import android.app.Fragment;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import androidx.annotation.StringRes;
-import com.google.android.material.snackbar.Snackbar;
-import com.google.android.material.tabs.TabLayout;
-import androidx.viewpager.widget.ViewPager;
-import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.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.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.widget.toast.SnackbarManager;
-
-import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING;
-import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
-import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static com.android.deskclock.AnimatorUtils.getScaleAnimator;
-
-/**
- * The main activity of the application which displays 4 different tabs contains alarms, world
- * clocks, timers and a stopwatch.
- */
-public class DeskClock extends BaseActivity
-        implements FabContainer, LabelDialogFragment.AlarmLabelDialogHandler {
-
-    /** Models the interesting state of display the {@link #mFab} button may inhabit. */
-    private enum FabState { SHOWING, HIDE_ARMED, HIDING }
-
-    /** Coordinates handling of context menu items. */
-    private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
-
-    /** Shrinks the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to nothing. */
-    private final AnimatorSet mHideAnimation = new AnimatorSet();
-
-    /** Grows the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to natural sizes. */
-    private final AnimatorSet mShowAnimation = new AnimatorSet();
-
-    /** 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();
-
-    /** Shows/hides a snackbar explaining which setting is suppressing alarms from firing. */
-    private final OnSilentSettingsListener mSilentSettingChangeWatcher =
-            new SilentSettingChangeWatcher();
-
-    /** 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;
-
-    /** The current display state of the {@link #mFab}. */
-    private FabState mFabState = FabState.SHOWING;
-
-    /** The single floating-action button shared across all tabs in the user interface. */
-    private ImageView mFab;
-
-    /** The button left of the {@link #mFab} shared across all tabs in the user interface. */
-    private Button mLeftButton;
-
-    /** The button right of the {@link #mFab} shared across all tabs in the user interface. */
-    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 ViewPager mFragmentTabPager;
-
-    /** Generates the fragments that are displayed by the {@link #mFragmentTabPager}. */
-    private FragmentTabPagerAdapter mFragmentTabPagerAdapter;
-
-    /** The container that stores the tab headers. */
-    private TabLayout mTabLayout;
-
-    /** {@code true} when a settings change necessitates recreating this activity. */
-    private boolean mRecreateActivity;
-
-    @Override
-    public void onNewIntent(Intent newIntent) {
-        super.onNewIntent(newIntent);
-
-        // Fragments may query the latest intent for information, so update the intent.
-        setIntent(newIntent);
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.desk_clock);
-        mSnackbarAnchor = findViewById(R.id.content);
-
-        // Configure the toolbar.
-        final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
-        setSupportActionBar(toolbar);
-
-        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), 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.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 = (Button) findViewById(R.id.left_button);
-        mRightButton = (Button) findViewById(R.id.right_button);
-
-        mFab.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                getSelectedDeskClockFragment().onFabClick(mFab);
-            }
-        });
-        mLeftButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                getSelectedDeskClockFragment().onLeftButtonClick(mLeftButton);
-            }
-        });
-        mRightButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                getSelectedDeskClockFragment().onRightButtonClick(mRightButton);
-            }
-        });
-
-        final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
-
-        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);
-            }
-        });
-
-        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 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.addOnPageChangeListener(new PageChangeWatcher());
-        mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter);
-
-        // 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);
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        DataModel.getDataModel().addSilentSettingsListener(mSilentSettingChangeWatcher);
-        DataModel.getDataModel().setApplicationInForeground(true);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        final View dropShadow = findViewById(R.id.drop_shadow);
-        mDropShadowController = new DropShadowController(dropShadow, UiDataModel.getUiDataModel(),
-                mSnackbarAnchor.findViewById(R.id.tab_hairline));
-
-        // ViewPager does not save state; this honors the selected tab in the user interface.
-        updateCurrentTab();
-    }
-
-    @Override
-    protected void onPostResume() {
-        super.onPostResume();
-
-        if (mRecreateActivity) {
-            mRecreateActivity = false;
-
-            // A runnable must be posted here or the new DeskClock activity will be recreated in a
-            // paused state, even though it is the foreground activity.
-            mFragmentTabPager.post(new Runnable() {
-                @Override
-                public void run() {
-                    recreate();
-                }
-            });
-        }
-    }
-
-    @Override
-    public void onPause() {
-        if (mDropShadowController != null) {
-            mDropShadowController.stop();
-            mDropShadowController = null;
-        }
-
-        super.onPause();
-    }
-
-    @Override
-    protected void onStop() {
-        DataModel.getDataModel().removeSilentSettingsListener(mSilentSettingChangeWatcher);
-        if (!isChangingConfigurations()) {
-            DataModel.getDataModel().setApplicationInForeground(false);
-        }
-
-        super.onStop();
-    }
-
-    @Override
-    protected void onDestroy() {
-        UiDataModel.getUiDataModel().removeTabListener(mTabChangeWatcher);
-        super.onDestroy();
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        mOptionsMenuManager.onCreateOptionsMenu(menu);
-        return true;
-    }
-
-    @Override
-    public boolean onPrepareOptionsMenu(Menu menu) {
-        super.onPrepareOptionsMenu(menu);
-        mOptionsMenuManager.onPrepareOptionsMenu(menu);
-        return true;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
-    }
-
-    /**
-     * Called by the LabelDialogFormat class after the dialog is finished.
-     */
-    @Override
-    public void onDialogLabelSet(Alarm alarm, String label, String tag) {
-        final Fragment frag = getFragmentManager().findFragmentByTag(tag);
-        if (frag instanceof AlarmClockFragment) {
-            ((AlarmClockFragment) frag).setLabel(alarm, label);
-        }
-    }
-
-    /**
-     * Listens for keyboard activity for the tab fragments to handle if necessary. A tab may want to
-     * respond to key presses even if they are not currently focused.
-     */
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        return getSelectedDeskClockFragment().onKeyDown(keyCode,event)
-                || super.onKeyDown(keyCode, event);
-    }
-
-    @Override
-    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_IMMEDIATE:
-                f.onUpdateFab(mFab);
-                break;
-            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;
-        }
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        // Recreate the activity if any settings have been changed
-        if (requestCode == SettingsMenuItemController.REQUEST_CHANGE_SETTINGS
-                && resultCode == RESULT_OK) {
-            mRecreateActivity = true;
-        }
-    }
-
-    /**
-     * Configure the {@link #mFragmentTabPager} and {@link #mTabLayout} to display UiDataModel's
-     * selected tab.
-     */
-    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;
-            }
-        }
-
-        // 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() {
-        for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
-            final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
-            if (fragment.isTabSelected()) {
-                return fragment;
-            }
-        }
-        final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
-        throw new IllegalStateException("Unable to locate selected fragment (" + selectedTab + ")");
-    }
-
-    /**
-     * @return a Snackbar that displays the message with the given id for 5 seconds
-     */
-    private Snackbar createSnackbar(@StringRes int messageId) {
-        return Snackbar.make(mSnackbarAnchor, messageId, 5000 /* duration */);
-    }
-
-    /**
-     * As the view pager changes the selected page, update the model to record the new selected tab.
-     */
-    private final class PageChangeWatcher implements OnPageChangeListener {
-
-        /** The last reported page scroll state; used to detect exotic state changes. */
-        private int mPriorState = SCROLL_STATE_IDLE;
-
-        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
-            // Only hide the fab when a non-zero drag distance is detected. This prevents
-            // over-scrolling from needlessly hiding the fab.
-            if (mFabState == FabState.HIDE_ARMED && positionOffsetPixels != 0) {
-                mFabState = FabState.HIDING;
-                mHideAnimation.start();
-            }
-        }
-
-        @Override
-        public void onPageScrollStateChanged(int state) {
-            if (mPriorState == SCROLL_STATE_IDLE && state == SCROLL_STATE_SETTLING) {
-                // The user has tapped a tab button; play the hide and show animations linearly.
-                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()) {
-                    mShowAnimation.cancel();
-                }
-                if (mHideAnimation.isStarted()) {
-                    // Let the hide animation finish naturally; don't auto show when it ends.
-                    mHideAnimation.removeListener(mAutoStartShowListener);
-                } else {
-                    // Start and immediately end the hide animation to jump to the hidden state.
-                    mHideAnimation.start();
-                    mHideAnimation.end();
-                }
-                mFabState = FabState.HIDING;
-
-            } else if (state != SCROLL_STATE_DRAGGING && mFabState == FabState.HIDING) {
-                // The user has lifted their finger; show the buttons now or after hide ends.
-                if (mHideAnimation.isStarted()) {
-                    // Finish the hide animation and then start the show animation.
-                    mHideAnimation.addListener(mAutoStartShowListener);
-                } else {
-                    updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-                    mShowAnimation.start();
-
-                    // The animation to show the fab has begun; update the state to showing.
-                    mFabState = FabState.SHOWING;
-                }
-            } else if (state == SCROLL_STATE_DRAGGING) {
-                // The user has started a drag so arm the hide animation.
-                mFabState = FabState.HIDE_ARMED;
-            }
-
-            // Update the last known state.
-            mPriorState = state;
-        }
-
-        @Override
-        public void onPageSelected(int position) {
-            mFragmentTabPagerAdapter.getDeskClockFragment(position).selectTab();
-        }
-    }
-
-    /**
-     * If this listener is attached to {@link #mHideAnimation} when it ends, the corresponding
-     * {@link #mShowAnimation} is automatically started.
-     */
-    private final class AutoStartShowListener extends AnimatorListenerAdapter {
-        @Override
-        public void onAnimationEnd(Animator animation) {
-            // Prepare the hide animation for its next use; by default do not auto-show after hide.
-            mHideAnimation.removeListener(mAutoStartShowListener);
-
-            // Update the buttons now that they are no longer visible.
-            updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-
-            // Automatically start the grow animation now that shrinking is complete.
-            mShowAnimation.start();
-
-            // The animation to show the fab has begun; update the state to showing.
-            mFabState = FabState.SHOWING;
-        }
-    }
-
-    /**
-     * Shows/hides a snackbar as silencing settings are enabled/disabled.
-     */
-    private final class SilentSettingChangeWatcher implements OnSilentSettingsListener {
-        @Override
-        public void onSilentSettingsChange(SilentSetting before, SilentSetting after) {
-            if (mShowSilentSettingSnackbarRunnable != null) {
-                mSnackbarAnchor.removeCallbacks(mShowSilentSettingSnackbarRunnable);
-                mShowSilentSettingSnackbarRunnable = null;
-            }
-
-            if (after == null) {
-                SnackbarManager.dismiss();
-            } else {
-                mShowSilentSettingSnackbarRunnable = new ShowSilentSettingSnackbarRunnable(after);
-                mSnackbarAnchor.postDelayed(mShowSilentSettingSnackbarRunnable, SECOND_IN_MILLIS);
-            }
-        }
-    }
-
-    /**
-     * Displays a snackbar that indicates a system setting is currently silencing alarms.
-     */
-    private final class ShowSilentSettingSnackbarRunnable implements Runnable {
-
-        private final SilentSetting mSilentSetting;
-
-        private ShowSilentSettingSnackbarRunnable(SilentSetting silentSetting) {
-            mSilentSetting = silentSetting;
-        }
-
-        public void run() {
-            // Create a snackbar with a message explaining the setting that is silencing alarms.
-            final Snackbar snackbar = createSnackbar(mSilentSetting.getLabelResId());
-
-            // 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);
-        }
-    }
-
-    /**
-     * As the model reports changes to the selected tab, update the user interface.
-     */
-    private final class TabChangeWatcher implements TabListener {
-        @Override
-        public void selectedTabChanged(UiDataModel.Tab oldSelectedTab,
-                UiDataModel.Tab newSelectedTab) {
-            // Update the view pager and tab layout to agree with the model.
-            updateCurrentTab();
-
-            // Avoid sending events for the initial tab selection on launch and re-selecting a tab
-            // after a configuration change.
-            if (DataModel.getDataModel().isApplicationInForeground()) {
-                switch (newSelectedTab) {
-                    case ALARMS:
-                        Events.sendAlarmEvent(R.string.action_show, R.string.label_deskclock);
-                        break;
-                    case CLOCKS:
-                        Events.sendClockEvent(R.string.action_show, R.string.label_deskclock);
-                        break;
-                    case TIMERS:
-                        Events.sendTimerEvent(R.string.action_show, R.string.label_deskclock);
-                        break;
-                    case STOPWATCH:
-                        Events.sendStopwatchEvent(R.string.action_show, R.string.label_deskclock);
-                        break;
-                }
-            }
-
-            // If the hide animation has already completed, the buttons must be updated now when the
-            // new tab is known. Otherwise they are updated at the end of the hide animation.
-            if (!mHideAnimation.isStarted()) {
-                updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClock.kt b/src/com/android/deskclock/DeskClock.kt
new file mode 100644
index 0000000..363a3da
--- /dev/null
+++ b/src/com/android/deskclock/DeskClock.kt
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Intent
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.appcompat.app.ActionBar
+import androidx.appcompat.widget.Toolbar
+import androidx.fragment.app.Fragment
+import androidx.viewpager.widget.ViewPager
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener
+import androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING
+import androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE
+import androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING
+
+import com.android.deskclock.FabContainer.UpdateFabFlag
+import com.android.deskclock.LabelDialogFragment.AlarmLabelDialogHandler
+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.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.widget.toast.SnackbarManager
+
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.tabs.TabLayout
+
+/**
+ * The main activity of the application which displays 4 different tabs contains alarms, world
+ * clocks, timers and a stopwatch.
+ */
+class DeskClock : BaseActivity(), FabContainer, AlarmLabelDialogHandler {
+    /** Models the interesting state of display the [.mFab] button may inhabit.  */
+    private enum class FabState {
+        SHOWING, HIDE_ARMED, HIDING
+    }
+
+    /** Coordinates handling of context menu items.  */
+    private val mOptionsMenuManager = OptionsMenuManager()
+
+    /** Shrinks the [.mFab], [.mLeftButton] and [.mRightButton] to nothing.  */
+    private val mHideAnimation = AnimatorSet()
+
+    /** Grows the [.mFab], [.mLeftButton] and [.mRightButton] to natural sizes.  */
+    private val mShowAnimation = AnimatorSet()
+
+    /** Hides, updates, and shows only the [.mFab]; the buttons are untouched.  */
+    private val mUpdateFabOnlyAnimation = AnimatorSet()
+
+    /** Hides, updates, and shows only the [.mLeftButton] and [.mRightButton].  */
+    private val mUpdateButtonsOnlyAnimation = AnimatorSet()
+
+    /** Automatically starts the [.mShowAnimation] after [.mHideAnimation] ends.  */
+    private val mAutoStartShowListener: AnimatorListenerAdapter = AutoStartShowListener()
+
+    /** Updates the user interface to reflect the selected tab from the backing model.  */
+    private val mTabChangeWatcher: TabListener = TabChangeWatcher()
+
+    /** Shows/hides a snackbar explaining which setting is suppressing alarms from firing.  */
+    private val mSilentSettingChangeWatcher: OnSilentSettingsListener = SilentSettingChangeWatcher()
+
+    /** Displays a snackbar explaining why alarms may not fire or may fire silently.  */
+    private var mShowSilentSettingSnackbarRunnable: Runnable? = null
+
+    /** The view to which snackbar items are anchored.  */
+    private lateinit var mSnackbarAnchor: View
+
+    /** The current display state of the [.mFab].  */
+    private var mFabState = FabState.SHOWING
+
+    /** The single floating-action button shared across all tabs in the user interface.  */
+    private lateinit var mFab: ImageView
+
+    /** The button left of the [.mFab] shared across all tabs in the user interface.  */
+    private lateinit var mLeftButton: Button
+
+    /** The button right of the [.mFab] shared across all tabs in the user interface.  */
+    private lateinit var mRightButton: Button
+
+    /** The controller that shows the drop shadow when content is not scrolled to the top.  */
+    private var mDropShadowController: DropShadowController? = null
+
+    /** The ViewPager that pages through the fragments representing the content of the tabs.  */
+    private lateinit var mFragmentTabPager: ViewPager
+
+    /** Generates the fragments that are displayed by the [.mFragmentTabPager].  */
+    private lateinit var mFragmentTabPagerAdapter: FragmentTabPagerAdapter
+
+    /** The container that stores the tab headers.  */
+    private lateinit var mTabLayout: TabLayout
+
+    /** `true` when a settings change necessitates recreating this activity.  */
+    private var mRecreateActivity = false
+
+    override fun onNewIntent(newIntent: Intent) {
+        super.onNewIntent(newIntent)
+
+        // Fragments may query the latest intent for information, so update the intent.
+        setIntent(newIntent)
+    }
+
+    protected override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(R.layout.desk_clock)
+        mSnackbarAnchor = findViewById(R.id.content)
+
+        // Configure the toolbar.
+        val toolbar: Toolbar = findViewById(R.id.toolbar) as Toolbar
+        setSupportActionBar(toolbar)
+
+        val actionBar: ActionBar? = getSupportActionBar()
+        actionBar?.setDisplayShowTitleEnabled(false)
+
+        // Configure the menu item controllers add behavior to the toolbar.
+        mOptionsMenuManager.addMenuItemController(
+                NightModeMenuItemController(this), SettingsMenuItemController(this))
+        mOptionsMenuManager.addMenuItemController(
+                *MenuItemControllerFactory.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 = findViewById(R.id.tabs) as TabLayout
+        val tabCount: Int = UiDataModel.uiDataModel.tabCount
+        val showTabLabel: Boolean = getResources().getBoolean(R.bool.showTabLabel)
+        val showTabHorizontally: Boolean = getResources().getBoolean(R.bool.showTabHorizontally)
+        for (i in 0 until tabCount) {
+            val tabModel: UiDataModel.Tab = UiDataModel.uiDataModel.getTab(i)
+            @StringRes val labelResId: Int = tabModel.labelResId
+
+            val tab: TabLayout.Tab = mTabLayout.newTab()
+                    .setTag(tabModel)
+                    .setIcon(tabModel.iconResId)
+                    .setContentDescription(labelResId)
+
+            if (showTabLabel) {
+                tab.setText(labelResId)
+                tab.setCustomView(R.layout.tab_item)
+
+                val text = tab.getCustomView()!!.findViewById(android.R.id.text1) as TextView
+                text.setTextColor(mTabLayout.getTabTextColors())
+
+                // Bind the icon to the TextView.
+                val icon: Drawable? = 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 = findViewById(R.id.fab) as ImageView
+        mLeftButton = findViewById(R.id.left_button) as Button
+        mRightButton = findViewById(R.id.right_button) as Button
+
+        mFab.setOnClickListener { selectedDeskClockFragment.onFabClick(mFab) }
+        mLeftButton.setOnClickListener {
+            selectedDeskClockFragment.onLeftButtonClick(mLeftButton)
+        }
+        mRightButton.setOnClickListener {
+            selectedDeskClockFragment.onRightButtonClick(mRightButton)
+        }
+
+        val duration: Long = UiDataModel.uiDataModel.shortAnimationDuration
+
+        val hideFabAnimation = AnimatorUtils.getScaleAnimator(mFab, 1f, 0f)
+        val showFabAnimation = AnimatorUtils.getScaleAnimator(mFab, 0f, 1f)
+
+        val leftHideAnimation = AnimatorUtils.getScaleAnimator(mLeftButton, 1f, 0f)
+        val rightHideAnimation = AnimatorUtils.getScaleAnimator(mRightButton, 1f, 0f)
+        val leftShowAnimation = AnimatorUtils.getScaleAnimator(mLeftButton, 0f, 1f)
+        val rightShowAnimation = AnimatorUtils.getScaleAnimator(mRightButton, 0f, 1f)
+
+        hideFabAnimation.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                selectedDeskClockFragment.onUpdateFab(mFab)
+            }
+        })
+
+        leftHideAnimation.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                selectedDeskClockFragment.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 = FragmentTabPagerAdapter(this)
+        mFragmentTabPager = findViewById(R.id.desk_clock_pager) as ViewPager
+        // 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.addOnPageChangeListener(PageChangeWatcher())
+        mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter)
+
+        // Mirror changes made to the selected tab into UiDataModel.
+        mTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+            override fun onTabSelected(tab: TabLayout.Tab) {
+                UiDataModel.uiDataModel.selectedTab = tab.getTag() as UiDataModel.Tab
+            }
+
+            override fun onTabUnselected(tab: TabLayout.Tab) {
+            }
+
+            override fun onTabReselected(tab: TabLayout.Tab) {
+            }
+        })
+
+        // Honor changes to the selected tab from outside entities.
+        UiDataModel.uiDataModel.addTabListener(mTabChangeWatcher)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        DataModel.dataModel.addSilentSettingsListener(mSilentSettingChangeWatcher)
+        DataModel.dataModel.isApplicationInForeground = true
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        val dropShadow: View = findViewById(R.id.drop_shadow)
+        mDropShadowController = DropShadowController(dropShadow, UiDataModel.uiDataModel,
+                mSnackbarAnchor.findViewById(R.id.tab_hairline))
+
+        // ViewPager does not save state; this honors the selected tab in the user interface.
+        updateCurrentTab()
+    }
+
+    override fun onPostResume() {
+        super.onPostResume()
+
+        if (mRecreateActivity) {
+            mRecreateActivity = false
+
+            // A runnable must be posted here or the new DeskClock activity will be recreated in a
+            // paused state, even though it is the foreground activity.
+            mFragmentTabPager.post(Runnable { recreate() })
+        }
+    }
+
+    override fun onPause() {
+        if (mDropShadowController != null) {
+            mDropShadowController!!.stop()
+            mDropShadowController = null
+        }
+
+        super.onPause()
+    }
+
+    override fun onStop() {
+        DataModel.dataModel.removeSilentSettingsListener(mSilentSettingChangeWatcher)
+        if (!isChangingConfigurations()) {
+            DataModel.dataModel.isApplicationInForeground = false
+        }
+
+        super.onStop()
+    }
+
+    override fun onDestroy() {
+        UiDataModel.uiDataModel.removeTabListener(mTabChangeWatcher)
+        super.onDestroy()
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        mOptionsMenuManager.onCreateOptionsMenu(menu)
+        return true
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+        super.onPrepareOptionsMenu(menu)
+        mOptionsMenuManager.onPrepareOptionsMenu(menu)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item)
+    }
+
+    /**
+     * Called by the LabelDialogFormat class after the dialog is finished.
+     */
+    override fun onDialogLabelSet(alarm: Alarm, label: String, tag: String) {
+        val frag: Fragment? = supportFragmentManager.findFragmentByTag(tag)
+        if (frag is AlarmClockFragment) {
+            frag.setLabel(alarm, label)
+        }
+    }
+
+    /**
+     * Listens for keyboard activity for the tab fragments to handle if necessary. A tab may want to
+     * respond to key presses even if they are not currently focused.
+     */
+    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+        return (selectedDeskClockFragment.onKeyDown(keyCode, event) ||
+                super.onKeyDown(keyCode, event))
+    }
+
+    override fun updateFab(@UpdateFabFlag updateType: Int) {
+        val f = selectedDeskClockFragment
+
+        when (updateType and FabContainer.FAB_ANIMATION_MASK) {
+            FabContainer.FAB_SHRINK_AND_EXPAND -> mUpdateFabOnlyAnimation.start()
+            FabContainer.FAB_IMMEDIATE -> f.onUpdateFab(mFab)
+            FabContainer.FAB_MORPH -> f.onMorphFab(mFab)
+        }
+        when (updateType and FabContainer.FAB_REQUEST_FOCUS_MASK) {
+            FabContainer.FAB_REQUEST_FOCUS -> mFab.requestFocus()
+        }
+        when (updateType and FabContainer.BUTTONS_ANIMATION_MASK) {
+            FabContainer.BUTTONS_IMMEDIATE -> f.onUpdateFabButtons(mLeftButton, mRightButton)
+            FabContainer.BUTTONS_SHRINK_AND_EXPAND -> mUpdateButtonsOnlyAnimation.start()
+        }
+        when (updateType and FabContainer.BUTTONS_DISABLE_MASK) {
+            FabContainer.BUTTONS_DISABLE -> {
+                mLeftButton.isClickable = false
+                mRightButton.isClickable = false
+            }
+        }
+        when (updateType and FabContainer.FAB_AND_BUTTONS_SHRINK_EXPAND_MASK) {
+            FabContainer.FAB_AND_BUTTONS_SHRINK -> mHideAnimation.start()
+            FabContainer.FAB_AND_BUTTONS_EXPAND -> mShowAnimation.start()
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        // Recreate the activity if any settings have been changed
+        if (requestCode == SettingsMenuItemController.REQUEST_CHANGE_SETTINGS &&
+                resultCode == RESULT_OK) {
+            mRecreateActivity = true
+        }
+    }
+
+    /**
+     * Configure the [.mFragmentTabPager] and [.mTabLayout] to display UiDataModel's
+     * selected tab.
+     */
+    private fun updateCurrentTab() {
+        // Fetch the selected tab from the source of truth: UiDataModel.
+        val selectedTab: UiDataModel.Tab = UiDataModel.uiDataModel.selectedTab
+
+        // Update the selected tab in the tablayout if it does not agree with UiDataModel.
+        for (i in 0 until mTabLayout.getTabCount()) {
+            val tab: TabLayout.Tab? = mTabLayout.getTabAt(i)
+            if (tab?.getTag() == selectedTab && !tab.isSelected()) {
+                tab.select()
+                break
+            }
+        }
+
+        // Update the selected fragment in the viewpager if it does not agree with UiDataModel.
+        for (i in 0 until mFragmentTabPagerAdapter.count) {
+            val 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 val selectedDeskClockFragment: DeskClockFragment
+        get() {
+            for (i in 0 until mFragmentTabPagerAdapter.count) {
+                val fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i)
+                if (fragment.isTabSelected) {
+                    return fragment
+                }
+            }
+            val selectedTab: UiDataModel.Tab = UiDataModel.uiDataModel.selectedTab
+            throw IllegalStateException("Unable to locate selected fragment ($selectedTab)")
+        }
+
+    /**
+     * @return a Snackbar that displays the message with the given id for 5 seconds
+     */
+    private fun createSnackbar(@StringRes messageId: Int): Snackbar {
+        return Snackbar.make(mSnackbarAnchor, messageId, 5000)
+    }
+
+    /**
+     * As the view pager changes the selected page, update the model to record the new selected tab.
+     */
+    private inner class PageChangeWatcher : OnPageChangeListener {
+        /** The last reported page scroll state; used to detect exotic state changes.  */
+        private var mPriorState: Int = SCROLL_STATE_IDLE
+
+        override fun onPageScrolled(
+            position: Int,
+            positionOffset: Float,
+            positionOffsetPixels: Int
+        ) {
+            // Only hide the fab when a non-zero drag distance is detected. This prevents
+            // over-scrolling from needlessly hiding the fab.
+            if (mFabState == FabState.HIDE_ARMED && positionOffsetPixels != 0) {
+                mFabState = FabState.HIDING
+                mHideAnimation.start()
+            }
+        }
+
+        override fun onPageScrollStateChanged(state: Int) {
+            if (mPriorState == SCROLL_STATE_IDLE && state == SCROLL_STATE_SETTLING) {
+                // The user has tapped a tab button; play the hide and show animations linearly.
+                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) {
+                    mShowAnimation.cancel()
+                }
+                if (mHideAnimation.isStarted) {
+                    // Let the hide animation finish naturally; don't auto show when it ends.
+                    mHideAnimation.removeListener(mAutoStartShowListener)
+                } else {
+                    // Start and immediately end the hide animation to jump to the hidden state.
+                    mHideAnimation.start()
+                    mHideAnimation.end()
+                }
+                mFabState = FabState.HIDING
+            } else if (state != SCROLL_STATE_DRAGGING && mFabState == FabState.HIDING) {
+                // The user has lifted their finger; show the buttons now or after hide ends.
+                if (mHideAnimation.isStarted) {
+                    // Finish the hide animation and then start the show animation.
+                    mHideAnimation.addListener(mAutoStartShowListener)
+                } else {
+                    updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+                    mShowAnimation.start()
+
+                    // The animation to show the fab has begun; update the state to showing.
+                    mFabState = FabState.SHOWING
+                }
+            } else if (state == SCROLL_STATE_DRAGGING) {
+                // The user has started a drag so arm the hide animation.
+                mFabState = FabState.HIDE_ARMED
+            }
+
+            // Update the last known state.
+            mPriorState = state
+        }
+
+        override fun onPageSelected(position: Int) {
+            mFragmentTabPagerAdapter.getDeskClockFragment(position).selectTab()
+        }
+    }
+
+    /**
+     * If this listener is attached to [.mHideAnimation] when it ends, the corresponding
+     * [.mShowAnimation] is automatically started.
+     */
+    private inner class AutoStartShowListener : AnimatorListenerAdapter() {
+        override fun onAnimationEnd(animation: Animator) {
+            // Prepare the hide animation for its next use; by default do not auto-show after hide.
+            mHideAnimation.removeListener(mAutoStartShowListener)
+
+            // Update the buttons now that they are no longer visible.
+            updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+            // Automatically start the grow animation now that shrinking is complete.
+            mShowAnimation.start()
+
+            // The animation to show the fab has begun; update the state to showing.
+            mFabState = FabState.SHOWING
+        }
+    }
+
+    /**
+     * Shows/hides a snackbar as silencing settings are enabled/disabled.
+     */
+    private inner class SilentSettingChangeWatcher : OnSilentSettingsListener {
+        override fun onSilentSettingsChange(
+            before: DataModel.SilentSetting?,
+            after: DataModel.SilentSetting?
+        ) {
+            if (mShowSilentSettingSnackbarRunnable != null) {
+                mSnackbarAnchor.removeCallbacks(mShowSilentSettingSnackbarRunnable)
+                mShowSilentSettingSnackbarRunnable = null
+            }
+
+            if (after == null) {
+                SnackbarManager.dismiss()
+            } else {
+                mShowSilentSettingSnackbarRunnable = ShowSilentSettingSnackbarRunnable(after)
+                mSnackbarAnchor.postDelayed(mShowSilentSettingSnackbarRunnable,
+                        DateUtils.SECOND_IN_MILLIS)
+            }
+        }
+    }
+
+    /**
+     * Displays a snackbar that indicates a system setting is currently silencing alarms.
+     */
+    private inner class ShowSilentSettingSnackbarRunnable(
+        private val mSilentSetting: DataModel.SilentSetting
+    ) : Runnable {
+        override fun run() {
+            // Create a snackbar with a message explaining the setting that is silencing alarms.
+            val snackbar: Snackbar = createSnackbar(mSilentSetting.labelResId)
+
+            // Set the associated corrective action if one exists.
+            if (mSilentSetting.isActionEnabled(this@DeskClock)) {
+                val actionResId: Int = mSilentSetting.actionResId
+                snackbar.setAction(actionResId, mSilentSetting.actionListener)
+            }
+
+            SnackbarManager.show(snackbar)
+        }
+    }
+
+    /**
+     * As the model reports changes to the selected tab, update the user interface.
+     */
+    private inner class TabChangeWatcher : TabListener {
+        override fun selectedTabChanged(
+            oldSelectedTab: UiDataModel.Tab,
+            newSelectedTab: UiDataModel.Tab
+        ) {
+            // Update the view pager and tab layout to agree with the model.
+            updateCurrentTab()
+
+            // Avoid sending events for the initial tab selection on launch and re-selecting a tab
+            // after a configuration change.
+            if (DataModel.dataModel.isApplicationInForeground) {
+                when (newSelectedTab) {
+                    UiDataModel.Tab.ALARMS -> {
+                        Events.sendAlarmEvent(R.string.action_show, R.string.label_deskclock)
+                    }
+                    UiDataModel.Tab.CLOCKS -> {
+                        Events.sendClockEvent(R.string.action_show, R.string.label_deskclock)
+                    }
+                    UiDataModel.Tab.TIMERS -> {
+                        Events.sendTimerEvent(R.string.action_show, R.string.label_deskclock)
+                    }
+                    UiDataModel.Tab.STOPWATCH -> {
+                        Events.sendStopwatchEvent(R.string.action_show, R.string.label_deskclock)
+                    }
+                }
+            }
+
+            // If the hide animation has already completed, the buttons must be updated now when the
+            // new tab is known. Otherwise they are updated at the end of the hide animation.
+            if (!mHideAnimation.isStarted) {
+                updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockApplication.java b/src/com/android/deskclock/DeskClockApplication.java
deleted file mode 100644
index 395d385..0000000
--- a/src/com/android/deskclock/DeskClockApplication.java
+++ /dev/null
@@ -1,65 +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.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.LogEventTracker;
-import com.android.deskclock.uidata.UiDataModel;
-
-public class DeskClockApplication extends Application {
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-
-        final Context applicationContext = getApplicationContext();
-        final SharedPreferences prefs = getDefaultSharedPreferences(applicationContext);
-
-        DataModel.getDataModel().init(applicationContext, prefs);
-        UiDataModel.getUiDataModel().init(applicationContext, prefs);
-        Controller.getController().setContext(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/DeskClockApplication.kt b/src/com/android/deskclock/DeskClockApplication.kt
new file mode 100644
index 0000000..7f0df93
--- /dev/null
+++ b/src/com/android/deskclock/DeskClockApplication.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.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.LogEventTracker
+import com.android.deskclock.uidata.UiDataModel
+
+class DeskClockApplication : Application() {
+    override fun onCreate() {
+        super.onCreate()
+
+        val applicationContext = applicationContext
+        val prefs = getDefaultSharedPreferences(applicationContext)
+
+        DataModel.dataModel.init(applicationContext, prefs)
+        UiDataModel.uiDataModel.init(applicationContext, prefs)
+        Controller.getController().setContext(applicationContext)
+        Controller.getController().addEventTracker(LogEventTracker(applicationContext))
+    }
+
+    companion object {
+        /**
+         * Returns the default [SharedPreferences] instance from the underlying storage context.
+         */
+        @TargetApi(Build.VERSION_CODES.N)
+        private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
+            val storageContext: Context
+            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.
+                val 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
deleted file mode 100644
index 4b66659..0000000
--- a/src/com/android/deskclock/DeskClockBackupAgent.java
+++ /dev/null
@@ -1,146 +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.app.AlarmManager;
-import android.app.PendingIntent;
-import android.app.backup.BackupAgent;
-import android.app.backup.BackupDataInput;
-import android.app.backup.BackupDataOutput;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import androidx.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;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Calendar;
-import java.util.List;
-
-public class DeskClockBackupAgent extends BackupAgent {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DeskClockBackupAgent");
-
-    public static final String ACTION_COMPLETE_RESTORE =
-            "com.android.deskclock.action.COMPLETE_RESTORE";
-
-    @Override
-    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
-            ParcelFileDescriptor newState) throws IOException { }
-
-    @Override
-    public void onRestore(BackupDataInput data, int appVersionCode,
-            ParcelFileDescriptor newState) throws IOException { }
-
-    @Override
-    public void onRestoreFile(@NonNull ParcelFileDescriptor data, long size, File destination,
-            int type, long mode, long mtime) throws IOException {
-        // The preference file on the backup device may not be the same on the restore device.
-        // Massage the file name here before writing it.
-        if (destination.getName().endsWith("_preferences.xml")) {
-            final String prefFileName = getPackageName() + "_preferences.xml";
-            destination = new File(destination.getParentFile(), prefFileName);
-        }
-
-        super.onRestoreFile(data, size, destination, type, mode, mtime);
-    }
-
-    /**
-     * When this method is called during backup/restore, the application is executing in a
-     * "minimalist" state. Because of this, the application's ContentResolver cannot be used.
-     * Consequently, the work of scheduling alarms on the restore device cannot be done here.
-     * Instead, a future callback to DeskClock is used as a signal to reschedule the alarms. The
-     * future callback may take the form of ACTION_BOOT_COMPLETED if the device is not yet fully
-     * booted (i.e. the restore occurred as part of the setup wizard). If the device is booted, an
-     * ACTION_COMPLETE_RESTORE broadcast is scheduled 10 seconds in the future to give
-     * backup/restore enough time to kill the Clock process. Both of these future callbacks result
-     * in the execution of {@link #processRestoredData(Context)}.
-     */
-    @Override
-    public void onRestoreFinished() {
-        if (Utils.isNOrLater()) {
-            // TODO: migrate restored database and preferences over into
-            // the device-encrypted storage area
-        }
-
-        // 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,
-                new Intent(ACTION_COMPLETE_RESTORE).setClass(this, AlarmInitReceiver.class),
-                PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT);
-
-        // Deliver the Intent 10 seconds from now.
-        final long triggerAtMillis = SystemClock.elapsedRealtime() + 10000;
-
-        // Schedule the Intent delivery in AlarmManager.
-        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
-        alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, restoreIntent);
-
-        LOGGER.i("Waiting for %s to complete the data restore", ACTION_COMPLETE_RESTORE);
-    }
-
-    /**
-     * @param context a context to access resources and services
-     * @return {@code true} if restore data was processed; {@code false} otherwise.
-     */
-    public static boolean processRestoredData(Context context) {
-        // If data was not recently restored, there is nothing to do.
-        if (!DataModel.getDataModel().isRestoreBackupFinished()) {
-            return false;
-        }
-
-        LOGGER.i("processRestoredData() started");
-
-        // Now that alarms have been restored, schedule new instances in AlarmManager.
-        final ContentResolver contentResolver = context.getContentResolver();
-        final List<Alarm> alarms = Alarm.getAlarms(contentResolver, null);
-
-        final Calendar now = Calendar.getInstance();
-        for (Alarm alarm : alarms) {
-            // Remove any instances that may currently exist for the alarm;
-            // these aren't relevant on the restore device and we'll recreate them below.
-            AlarmStateManager.deleteAllInstances(context, alarm.id);
-
-            if (alarm.enabled) {
-                // Create the next alarm instance to schedule.
-                AlarmInstance alarmInstance = alarm.createInstanceAfter(now);
-
-                // Add the next alarm instance to the database.
-                alarmInstance = AlarmInstance.addInstance(contentResolver, alarmInstance);
-
-                // Schedule the next alarm instance in AlarmManager.
-                AlarmStateManager.registerInstance(context, alarmInstance, true);
-                LOGGER.i("DeskClockBackupAgent scheduled alarm instance: %s", alarmInstance);
-            }
-        }
-
-        // Remove the preference to avoid executing this logic multiple times.
-        DataModel.getDataModel().setRestoreBackupFinished(false);
-
-        LOGGER.i("processRestoredData() completed");
-        return true;
-    }
-}
diff --git a/src/com/android/deskclock/DeskClockBackupAgent.kt b/src/com/android/deskclock/DeskClockBackupAgent.kt
new file mode 100644
index 0000000..71ccf0e
--- /dev/null
+++ b/src/com/android/deskclock/DeskClockBackupAgent.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AlarmManager
+import android.app.PendingIntent
+import android.app.backup.BackupAgent
+import android.app.backup.BackupDataInput
+import android.app.backup.BackupDataOutput
+import android.content.Context
+import android.content.Intent
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+
+import java.io.File
+import java.io.IOException
+import java.util.Calendar
+
+class DeskClockBackupAgent : BackupAgent() {
+    @Throws(IOException::class)
+    override fun onBackup(
+        oldState: ParcelFileDescriptor,
+        data: BackupDataOutput,
+        newState: ParcelFileDescriptor
+    ) {
+    }
+
+    @Throws(IOException::class)
+    override fun onRestore(
+        data: BackupDataInput,
+        appVersionCode: Int,
+        newState: ParcelFileDescriptor
+    ) {
+    }
+
+    @Throws(IOException::class)
+    override fun onRestoreFile(
+        data: ParcelFileDescriptor,
+        size: Long,
+        destination: File,
+        type: Int,
+        mode: Long,
+        mtime: Long
+    ) {
+        // The preference file on the backup device may not be the same on the restore device.
+        // Massage the file name here before writing it.
+        var variableDestination = destination
+        if (variableDestination.name.endsWith("_preferences.xml")) {
+            val prefFileName = packageName + "_preferences.xml"
+            variableDestination = File(variableDestination.parentFile, prefFileName)
+        }
+
+        super.onRestoreFile(data, size, variableDestination, type, mode, mtime)
+    }
+
+    /**
+     * When this method is called during backup/restore, the application is executing in a
+     * "minimalist" state. Because of this, the application's ContentResolver cannot be used.
+     * Consequently, the work of scheduling alarms on the restore device cannot be done here.
+     * Instead, a future callback to DeskClock is used as a signal to reschedule the alarms. The
+     * future callback may take the form of ACTION_BOOT_COMPLETED if the device is not yet fully
+     * booted (i.e. the restore occurred as part of the setup wizard). If the device is booted, an
+     * ACTION_COMPLETE_RESTORE broadcast is scheduled 10 seconds in the future to give
+     * backup/restore enough time to kill the Clock process. Both of these future callbacks result
+     * in the execution of [.processRestoredData].
+     */
+    override fun onRestoreFinished() {
+        if (Utils.isNOrLater) {
+            // TODO: migrate restored database and preferences over into
+            // the device-encrypted storage area
+        }
+
+        // Indicate a data restore has been completed.
+        DataModel.dataModel.isRestoreBackupFinished = true
+
+        // Create an Intent to send into DeskClock indicating restore is complete.
+        val restoreIntent = PendingIntent.getBroadcast(this, 0,
+                Intent(ACTION_COMPLETE_RESTORE).setClass(this, AlarmInitReceiver::class.java),
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_CANCEL_CURRENT)
+
+        // Deliver the Intent 10 seconds from now.
+        val triggerAtMillis = SystemClock.elapsedRealtime() + 10000
+
+        // Schedule the Intent delivery in AlarmManager.
+        val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
+        alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, restoreIntent)
+
+        LOGGER.i("Waiting for %s to complete the data restore", ACTION_COMPLETE_RESTORE)
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("DeskClockBackupAgent")
+
+        const val ACTION_COMPLETE_RESTORE = "com.android.deskclock.action.COMPLETE_RESTORE"
+
+        /**
+         * @param context a context to access resources and services
+         * @return `true` if restore data was processed; `false` otherwise.
+         */
+        @JvmStatic
+        fun processRestoredData(context: Context): Boolean {
+            // If data was not recently restored, there is nothing to do.
+            if (!DataModel.dataModel.isRestoreBackupFinished) {
+                return false
+            }
+
+            LOGGER.i("processRestoredData() started")
+
+            // Now that alarms have been restored, schedule new instances in AlarmManager.
+            val contentResolver = context.contentResolver
+            val alarms = Alarm.getAlarms(contentResolver, null)
+
+            val now = Calendar.getInstance()
+            for (alarm in alarms) {
+                // Remove any instances that may currently exist for the alarm;
+                // these aren't relevant on the restore device and we'll recreate them below.
+                AlarmStateManager.deleteAllInstances(context, alarm.id)
+
+                if (alarm.enabled) {
+                    // Create the next alarm instance to schedule.
+                    var alarmInstance = alarm.createInstanceAfter(now)
+
+                    // Add the next alarm instance to the database.
+                    alarmInstance = AlarmInstance.addInstance(contentResolver, alarmInstance)
+
+                    // Schedule the next alarm instance in AlarmManager.
+                    AlarmStateManager.registerInstance(context, alarmInstance, true)
+                    LOGGER.i("DeskClockBackupAgent scheduled alarm instance: %s", alarmInstance)
+                }
+            }
+
+            // Remove the preference to avoid executing this logic multiple times.
+            DataModel.dataModel.isRestoreBackupFinished = false
+
+            LOGGER.i("processRestoredData() completed")
+            return true
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockFragment.java b/src/com/android/deskclock/DeskClockFragment.java
deleted file mode 100644
index a9e3fc6..0000000
--- a/src/com/android/deskclock/DeskClockFragment.java
+++ /dev/null
@@ -1,119 +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;
-
-import android.app.Fragment;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import android.view.KeyEvent;
-import android.widget.Button;
-import android.widget.ImageView;
-
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.uidata.UiDataModel.Tab;
-
-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;
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        // Update the fab and buttons in case their state changed while the fragment was paused.
-        if (isTabSelected()) {
-            updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-        }
-    }
-
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        // By default return false so event continues to propagate
-        return false;
-    }
-
-    @Override
-    public void onLeftButtonClick(@NonNull Button left) {
-        // Do nothing here, only in derived classes
-    }
-
-    @Override
-    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
-    }
-
-    /**
-     * @param color the newly installed app window color
-     */
-    protected void onAppColorChanged(@ColorInt int color) {
-        // Do nothing here, only in derived classes
-    }
-
-    /**
-     * @param fabContainer the container that houses the fab and its left and right buttons
-     */
-    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 final void updateFab(@UpdateFabFlag int updateTypes) {
-        if (mFabContainer != null) {
-            mFabContainer.updateFab(updateTypes);
-        }
-    }
-
-    /**
-     * @return {@code true} iff the currently selected tab displays this fragment
-     */
-    public final boolean isTabSelected() {
-        return UiDataModel.getUiDataModel().getSelectedTab() == mTab;
-    }
-
-    /**
-     * 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
-     */
-    public final void setTabScrolledToTop(boolean scrolledToTop) {
-        UiDataModel.getUiDataModel().setTabScrolledToTop(mTab, scrolledToTop);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockFragment.kt b/src/com/android/deskclock/DeskClockFragment.kt
new file mode 100644
index 0000000..b7f1fa9
--- /dev/null
+++ b/src/com/android/deskclock/DeskClockFragment.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.KeyEvent
+import android.widget.Button
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.fragment.app.Fragment
+
+import com.android.deskclock.FabContainer.UpdateFabFlag
+import com.android.deskclock.uidata.UiDataModel
+
+abstract class DeskClockFragment(
+    /** The tab associated with this fragment.  */
+    private val mTab: UiDataModel.Tab
+) : Fragment(), FabContainer, FabController {
+
+    /** The container that houses the fab and its left and right buttons.  */
+    private var mFabContainer: FabContainer? = null
+
+    override fun onResume() {
+        super.onResume()
+
+        // Update the fab and buttons in case their state changed while the fragment was paused.
+        if (isTabSelected) {
+            updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+        }
+    }
+
+    open fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+        // By default return false so event continues to propagate
+        return false
+    }
+
+    override fun onLeftButtonClick(left: Button) {
+        // Do nothing here, only in derived classes
+    }
+
+    override fun onRightButtonClick(right: Button) {
+        // Do nothing here, only in derived classes
+    }
+
+    override fun onMorphFab(fab: ImageView) {
+        // Do nothing here, only in derived classes
+    }
+
+    /**
+     * @param color the newly installed app window color
+     */
+    protected open fun onAppColorChanged(@ColorInt color: Int) {
+        // Do nothing here, only in derived classes
+    }
+
+    /**
+     * @param fabContainer the container that houses the fab and its left and right buttons
+     */
+    fun 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 fun updateFab(@UpdateFabFlag updateTypes: Int) {
+        mFabContainer?.updateFab(updateTypes)
+    }
+
+    /**
+     * @return `true` iff the currently selected tab displays this fragment
+     */
+    val isTabSelected: Boolean
+        get() = UiDataModel.uiDataModel.selectedTab == mTab
+
+    /**
+     * Select the tab that displays this fragment.
+     */
+    fun selectTab() {
+        UiDataModel.uiDataModel.selectedTab = mTab
+    }
+
+    /**
+     * Updates the scrolling state in the [UiDataModel] for this tab.
+     *
+     * @param scrolledToTop `true` iff the vertical scroll position of this tab is at the top
+     */
+    fun setTabScrolledToTop(scrolledToTop: Boolean) {
+        UiDataModel.uiDataModel.setTabScrolledToTop(mTab, scrolledToTop)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DropShadowController.java b/src/com/android/deskclock/DropShadowController.java
deleted file mode 100644
index 53edd77..0000000
--- a/src/com/android/deskclock/DropShadowController.java
+++ /dev/null
@@ -1,170 +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.animation.ValueAnimator;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.View;
-import android.widget.AbsListView;
-import android.widget.ListView;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.uidata.TabScrollListener;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.uidata.UiDataModel.Tab;
-
-import static com.android.deskclock.AnimatorUtils.getAlphaAnimator;
-
-/**
- * This controller encapsulates the logic that watches a model for changes to scroll state and
- * updates the display state of an associated drop shadow. The observable model may take many forms
- * including ListViews, RecyclerViews and this application's UiDataModel. Each of these models can
- * indicate when content is scrolled to its top. When the content is scrolled to the top the drop
- * shadow is hidden and the content appears flush with the app bar. When the content is scrolled
- * up the drop shadow is displayed making the content appear to scroll below the app bar.
- */
-public final class DropShadowController {
-
-    /** Updates {@link #mDropShadowView} in response to changes in the backing scroll model. */
-    private final ScrollChangeWatcher mScrollChangeWatcher = new ScrollChangeWatcher();
-
-    /** Fades the {@link @mDropShadowView} in/out as scroll state changes. */
-    private final ValueAnimator mDropShadowAnimator;
-
-    /** 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;
-    private ListView mListView;
-
-    /**
-     * @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, View hairlineView) {
-        this(dropShadowView);
-        mUiDataModel = uiDataModel;
-        mUiDataModel.addTabScrollListener(mScrollChangeWatcher);
-        mHairlineView = hairlineView;
-        updateDropShadow(!uiDataModel.isSelectedTabScrolledToTop());
-    }
-
-    /**
-     * @param dropShadowView to be hidden/shown as {@code listView} reports scrolling changes
-     * @param listView a scrollable view that dictates the visibility of {@code dropShadowView}
-     */
-    public DropShadowController(View dropShadowView, ListView listView) {
-        this(dropShadowView);
-        mListView = listView;
-        mListView.setOnScrollListener(mScrollChangeWatcher);
-        updateDropShadow(!Utils.isScrolledToTop(listView));
-    }
-
-    /**
-     * @param dropShadowView to be hidden/shown as {@code recyclerView} reports scrolling changes
-     * @param recyclerView a scrollable view that dictates the visibility of {@code dropShadowView}
-     */
-    public DropShadowController(View dropShadowView, RecyclerView recyclerView) {
-        this(dropShadowView);
-        mRecyclerView = recyclerView;
-        mRecyclerView.addOnScrollListener(mScrollChangeWatcher);
-        updateDropShadow(!Utils.isScrolledToTop(recyclerView));
-    }
-
-    private DropShadowController(View dropShadowView) {
-        mDropShadowView = dropShadowView;
-        mDropShadowAnimator = getAlphaAnimator(mDropShadowView, 0f, 1f)
-                .setDuration(UiDataModel.getUiDataModel().getShortAnimationDuration());
-    }
-
-    /**
-     * Stop updating the drop shadow in response to scrolling changes. Stop listening to the backing
-     * scrollable entity for changes. This is important to avoid memory leaks.
-     */
-    public void stop() {
-        if (mRecyclerView != null) {
-            mRecyclerView.removeOnScrollListener(mScrollChangeWatcher);
-        } else if (mListView != null) {
-            mListView.setOnScrollListener(null);
-        } else if (mUiDataModel != null) {
-            mUiDataModel.removeTabScrollListener(mScrollChangeWatcher);
-        }
-    }
-
-    /**
-     * @param shouldShowDropShadow {@code true} indicates the drop shadow should be displayed;
-     *      {@code false} indicates the drop shadow should be hidden
-     */
-    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 (shouldShowDropShadow && mDropShadowView.getAlpha() != 1f) {
-            if (DataModel.getDataModel().isApplicationInForeground()) {
-                mDropShadowAnimator.start();
-            } else {
-                mDropShadowView.setAlpha(1f);
-            }
-            if (mHairlineView != null) {
-                mHairlineView.setVisibility(View.INVISIBLE);
-            }
-        }
-    }
-
-    /**
-     * Update the drop shadow as the scrollable entity is scrolled.
-     */
-    private final class ScrollChangeWatcher extends RecyclerView.OnScrollListener
-            implements TabScrollListener, AbsListView.OnScrollListener {
-
-        // RecyclerView scrolled.
-        @Override
-        public void onScrolled(RecyclerView view, int dx, int dy) {
-            updateDropShadow(!Utils.isScrolledToTop(view));
-        }
-
-        // ListView scrolled.
-        @Override
-        public void onScrollStateChanged(AbsListView view, int scrollState) {}
-
-        @Override
-        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
-                int totalItemCount) {
-            updateDropShadow(!Utils.isScrolledToTop(view));
-        }
-
-        // UiDataModel reports scroll change.
-        public void selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop) {
-            updateDropShadow(!scrolledToTop);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DropShadowController.kt b/src/com/android/deskclock/DropShadowController.kt
new file mode 100644
index 0000000..4fa9101
--- /dev/null
+++ b/src/com/android/deskclock/DropShadowController.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ValueAnimator
+import android.view.View
+import android.widget.AbsListView
+import android.widget.ListView
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.uidata.TabScrollListener
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * This controller encapsulates the logic that watches a model for changes to scroll state and
+ * updates the display state of an associated drop shadow. The observable model may take many forms
+ * including ListViews, RecyclerViews and this application's UiDataModel. Each of these models can
+ * indicate when content is scrolled to its top. When the content is scrolled to the top the drop
+ * shadow is hidden and the content appears flush with the app bar. When the content is scrolled
+ * up the drop shadow is displayed making the content appear to scroll below the app bar.
+ */
+class DropShadowController private constructor(
+    /** The component that displays a drop shadow.  */
+    private val mDropShadowView: View
+) {
+    /** Updates [.mDropShadowView] in response to changes in the backing scroll model.  */
+    private val mScrollChangeWatcher = ScrollChangeWatcher()
+
+    /** Fades the [@mDropShadowView] in/out as scroll state changes.  */
+    private val mDropShadowAnimator: ValueAnimator =
+            AnimatorUtils.getAlphaAnimator(mDropShadowView, 0f, 1f)
+                    .setDuration(UiDataModel.uiDataModel.shortAnimationDuration)
+
+    /** Tab bar's hairline, which is hidden whenever the drop shadow is displayed.  */
+    private var mHairlineView: View? = null
+
+    // Supported sources of scroll position include: ListView, RecyclerView and UiDataModel.
+    private var mRecyclerView: RecyclerView? = null
+    private var mUiDataModel: UiDataModel? = null
+    private var mListView: ListView? = null
+
+    /**
+     * @param dropShadowView to be hidden/shown as `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.
+     */
+    constructor(
+        dropShadowView: View,
+        uiDataModel: UiDataModel,
+        hairlineView: View
+    ) : this(dropShadowView) {
+        mUiDataModel = uiDataModel
+        mUiDataModel?.addTabScrollListener(mScrollChangeWatcher)
+        mHairlineView = hairlineView
+        updateDropShadow(!uiDataModel.isSelectedTabScrolledToTop)
+    }
+
+    /**
+     * @param dropShadowView to be hidden/shown as `listView` reports scrolling changes
+     * @param listView a scrollable view that dictates the visibility of `dropShadowView`
+     */
+    constructor(dropShadowView: View, listView: ListView) : this(dropShadowView) {
+        mListView = listView
+        mListView?.setOnScrollListener(mScrollChangeWatcher)
+        updateDropShadow(!Utils.isScrolledToTop(listView))
+    }
+
+    /**
+     * @param dropShadowView to be hidden/shown as `recyclerView` reports scrolling changes
+     * @param recyclerView a scrollable view that dictates the visibility of `dropShadowView`
+     */
+    constructor(dropShadowView: View, recyclerView: RecyclerView) : this(dropShadowView) {
+        mRecyclerView = recyclerView
+        mRecyclerView?.addOnScrollListener(mScrollChangeWatcher)
+        updateDropShadow(!Utils.isScrolledToTop(recyclerView))
+    }
+
+    /**
+     * Stop updating the drop shadow in response to scrolling changes. Stop listening to the backing
+     * scrollable entity for changes. This is important to avoid memory leaks.
+     */
+    fun stop() {
+        when {
+            mRecyclerView != null -> mRecyclerView?.removeOnScrollListener(mScrollChangeWatcher)
+            mListView != null -> mListView?.setOnScrollListener(null)
+            mUiDataModel != null -> mUiDataModel?.removeTabScrollListener(mScrollChangeWatcher)
+        }
+    }
+
+    /**
+     * @param shouldShowDropShadow `true` indicates the drop shadow should be displayed;
+     * `false` indicates the drop shadow should be hidden
+     */
+    private fun updateDropShadow(shouldShowDropShadow: Boolean) {
+        if (!shouldShowDropShadow && mDropShadowView.alpha != 0f) {
+            if (DataModel.dataModel.isApplicationInForeground) {
+                mDropShadowAnimator.reverse()
+            } else {
+                mDropShadowView.alpha = 0f
+            }
+            mHairlineView?.visibility = View.VISIBLE
+        }
+        if (shouldShowDropShadow && mDropShadowView.alpha != 1f) {
+            if (DataModel.dataModel.isApplicationInForeground) {
+                mDropShadowAnimator.start()
+            } else {
+                mDropShadowView.alpha = 1f
+            }
+            mHairlineView?.visibility = View.INVISIBLE
+        }
+    }
+
+    /**
+     * Update the drop shadow as the scrollable entity is scrolled.
+     */
+    private inner class ScrollChangeWatcher
+        : RecyclerView.OnScrollListener(), TabScrollListener, AbsListView.OnScrollListener {
+        // RecyclerView scrolled.
+        override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
+            updateDropShadow(!Utils.isScrolledToTop(view))
+        }
+
+        // ListView scrolled.
+        override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
+        }
+
+        override fun onScroll(
+            view: AbsListView,
+            firstVisibleItem: Int,
+            visibleItemCount: Int,
+            totalItemCount: Int
+        ) {
+            updateDropShadow(!Utils.isScrolledToTop(view))
+        }
+
+        // UiDataModel reports scroll change.
+        override fun selectedTabScrollToTopChanged(
+            selectedTab: UiDataModel.Tab,
+            scrolledToTop: Boolean
+        ) {
+            updateDropShadow(!scrolledToTop)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabContainer.java b/src/com/android/deskclock/FabContainer.java
deleted file mode 100644
index 2813e70..0000000
--- a/src/com/android/deskclock/FabContainer.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.android.deskclock;
-
-import androidx.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
- * commands.
- */
-public interface FabContainer {
-
-    /** Bit field for updates */
-
-    /** 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;
-
-    /** Bit 2 */
-    int FAB_REQUEST_FOCUS_MASK = 0b100;
-    /** Signals that the fab should request focus. */
-    int FAB_REQUEST_FOCUS = 0b100;
-
-    /** 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;
-
-    /** Bit 5 */
-    int BUTTONS_DISABLE_MASK = 0b100000;
-    /** Disable the buttons of the fab so they do not respond to clicks. */
-    int BUTTONS_DISABLE = 0b100000;
-
-    /** 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 updateTypes}.
-     *
-     * @param updateTypes indicates the types of update to apply to the fab and its buttons
-     */
-    void updateFab(@UpdateFabFlag int updateTypes);
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabContainer.kt b/src/com/android/deskclock/FabContainer.kt
new file mode 100644
index 0000000..58738f3
--- /dev/null
+++ b/src/com/android/deskclock/FabContainer.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.annotation.IntDef
+
+/**
+ * Implemented by containers that house the fab and its associated buttons. Also implemented by
+ * containers that know how to contact the **true** fab container to ferry through
+ * commands.
+ */
+interface FabContainer {
+    @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
+    ])
+    annotation class 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
+     * `updateTypes`.
+     *
+     * @param updateTypes indicates the types of update to apply to the fab and its buttons
+     */
+    fun updateFab(@UpdateFabFlag updateTypes: Int)
+
+    companion object {
+        /** Bit field for updates  */
+        /** Bit 0-1  */
+        const val FAB_ANIMATION_MASK = 3
+
+        /** Signals that the fab should be updated in place with no animation.  */
+        const val FAB_IMMEDIATE = 1
+
+        /** Signals the fab should be "animated away", updated, and "animated back".  */
+        const val FAB_SHRINK_AND_EXPAND = 2
+
+        /** Signals that the fab should morph into a new state in place.  */
+        const val FAB_MORPH = 3
+
+        /** Bit 2  */
+        const val FAB_REQUEST_FOCUS_MASK = 4
+
+        /** Signals that the fab should request focus.  */
+        const val FAB_REQUEST_FOCUS = 4
+
+        /** Bit 3-4  */
+        const val BUTTONS_ANIMATION_MASK = 24
+
+        /** Signals that the buttons should be updated in place with no animation.  */
+        const val BUTTONS_IMMEDIATE = 8
+
+        /** Signals that the buttons should be "animated away", updated, and "animated back".  */
+        const val BUTTONS_SHRINK_AND_EXPAND = 16
+
+        /** Bit 5  */
+        const val BUTTONS_DISABLE_MASK = 32
+
+        /** Disable the buttons of the fab so they do not respond to clicks.  */
+        const val BUTTONS_DISABLE = 32
+
+        /** Bit 6-7  */
+        const val FAB_AND_BUTTONS_SHRINK_EXPAND_MASK = 192
+
+        /** Signals the fab and buttons should be "animated away".  */
+        const val FAB_AND_BUTTONS_SHRINK = 128
+
+        /** Signals the fab and buttons should be "animated back".  */
+        const val FAB_AND_BUTTONS_EXPAND = 64
+
+        /** Convenience flags  */
+        const val FAB_AND_BUTTONS_IMMEDIATE = FAB_IMMEDIATE or BUTTONS_IMMEDIATE
+        const val FAB_AND_BUTTONS_SHRINK_AND_EXPAND =
+                FAB_SHRINK_AND_EXPAND or BUTTONS_SHRINK_AND_EXPAND
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabController.java b/src/com/android/deskclock/FabController.java
deleted file mode 100644
index bab7f46..0000000
--- a/src/com/android/deskclock/FabController.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.android.deskclock;
-
-import androidx.annotation.NonNull;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-
-/**
- * Implementers of this interface are able to {@link #onUpdateFab configure the fab} and associated
- * {@link #onUpdateFabButtons left/right buttons} including setting them {@link View#INVISIBLE} if
- * they are unnecessary. Implementers also attach click handler logic to the
- * {@link #onFabClick fab}, {@link #onLeftButtonClick left button} and
- * {@link #onRightButtonClick right button}.
- */
-public interface FabController {
-
-    /**
-     * Configures the display of the fab component to match the current state of this controller.
-     *
-     * @param fab the fab component to be configured based on current state
-     */
-    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 Button left, @NonNull Button right);
-
-    /**
-     * Handles a click on the fab.
-     *
-     * @param fab the fab component on which the click occurred
-     */
-    void onFabClick(@NonNull ImageView fab);
-
-    /**
-     * Handles a click on the button to the left of the fab component.
-     *
-     * @param left the button to the left of the fab component
-     */
-    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 Button right);
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabController.kt b/src/com/android/deskclock/FabController.kt
new file mode 100644
index 0000000..cc1096a
--- /dev/null
+++ b/src/com/android/deskclock/FabController.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.View
+import android.widget.Button
+import android.widget.ImageView
+
+/**
+ * Implementers of this interface are able to [configure the fab][.onUpdateFab] and associated
+ * [left/right buttons][.onUpdateFabButtons] including setting them [View.INVISIBLE] if
+ * they are unnecessary. Implementers also attach click handler logic to the
+ * [fab][.onFabClick], [left button][.onLeftButtonClick] and
+ * [right button][.onRightButtonClick].
+ */
+interface FabController {
+    /**
+     * Configures the display of the fab component to match the current state of this controller.
+     *
+     * @param fab the fab component to be configured based on current state
+     */
+    fun onUpdateFab(fab: ImageView)
+
+    /**
+     * Called before onUpdateFab when the fab should be animated.
+     *
+     * @param fab the fab component to be configured based on current state
+     */
+    fun onMorphFab(fab: ImageView)
+
+    /**
+     * 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
+     */
+    fun onUpdateFabButtons(left: Button, right: Button)
+
+    /**
+     * Handles a click on the fab.
+     *
+     * @param fab the fab component on which the click occurred
+     */
+    fun onFabClick(fab: ImageView)
+
+    /**
+     * Handles a click on the button to the left of the fab component.
+     *
+     * @param left the button to the left of the fab component
+     */
+    fun onLeftButtonClick(left: Button)
+
+    /**
+     * Handles a click on the button to the right of the fab component.
+     *
+     * @param right the button to the right of the fab component
+     */
+    fun onRightButtonClick(right: Button)
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FetchMatchingAlarmsAction.java b/src/com/android/deskclock/FetchMatchingAlarmsAction.java
deleted file mode 100644
index 5575348..0000000
--- a/src/com/android/deskclock/FetchMatchingAlarmsAction.java
+++ /dev/null
@@ -1,178 +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.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Looper;
-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;
-
-import java.text.DateFormatSymbols;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-/**
- * Returns a list of alarms that are specified by the intent
- * processed by HandleDeskClockApiCalls
- * if there are more than 1 matching alarms and the SEARCH_MODE is not ALL
- * we show a picker UI dialog
- */
-class FetchMatchingAlarmsAction implements Runnable {
-
-    private final Context mContext;
-    private final List<Alarm> mAlarms;
-    private final Intent mIntent;
-    private final List<Alarm> mMatchingAlarms = new ArrayList<>();
-    private final Activity mActivity;
-
-    public FetchMatchingAlarmsAction(Context context, List<Alarm> alarms, Intent intent,
-                                     Activity activity) {
-        mContext = context;
-        // only enabled alarms are passed
-        mAlarms = alarms;
-        mIntent = intent;
-        mActivity = activity;
-    }
-
-    @Override
-    public void run() {
-        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
-        if (searchMode == null) {
-            mMatchingAlarms.addAll(mAlarms);
-            return;
-        }
-
-        final ContentResolver cr = mContext.getContentResolver();
-        switch (searchMode) {
-            case AlarmClock.ALARM_SEARCH_MODE_TIME:
-                // at least one of these has to be specified in this search mode.
-                final int hour = mIntent.getIntExtra(AlarmClock.EXTRA_HOUR, -1);
-                // if minutes weren't specified default to 0
-                final int minutes = mIntent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
-                final Boolean isPm = (Boolean) mIntent.getExtras().get(AlarmClock.EXTRA_IS_PM);
-                boolean badInput = isPm != null && hour > 12 && isPm;
-                badInput |= hour < 0 || hour > 23;
-                badInput |= minutes < 0 || minutes > 59;
-
-                if (badInput) {
-                    final String[] ampm = new DateFormatSymbols().getAmPmStrings();
-                    final String amPm = isPm == null ? "" : (isPm ? ampm[1] : ampm[0]);
-                    final String reason = mContext.getString(R.string.invalid_time, hour, minutes,
-                            amPm);
-                    notifyFailureAndLog(reason, mActivity);
-                    return;
-                }
-
-                final int hour24 = Boolean.TRUE.equals(isPm) && hour < 12 ? (hour + 12) : hour;
-
-                // there might me multiple alarms at the same time
-                for (Alarm alarm : mAlarms) {
-                    if (alarm.hour == hour24 && alarm.minutes == minutes) {
-                        mMatchingAlarms.add(alarm);
-                    }
-                }
-                if (mMatchingAlarms.isEmpty()) {
-                    final String reason = mContext.getString(R.string.no_alarm_at, hour24, minutes);
-                    notifyFailureAndLog(reason, mActivity);
-                    return;
-                }
-                break;
-            case AlarmClock.ALARM_SEARCH_MODE_NEXT:
-                // Match currently firing alarms before scheduled alarms.
-                for (Alarm alarm : mAlarms) {
-                    final AlarmInstance alarmInstance =
-                            AlarmInstance.getNextUpcomingInstanceByAlarmId(cr, alarm.id);
-                    if (alarmInstance != null
-                            && alarmInstance.mAlarmState == AlarmInstance.FIRED_STATE) {
-                        mMatchingAlarms.add(alarm);
-                    }
-                }
-                if (!mMatchingAlarms.isEmpty()) {
-                    // return the matched firing alarms
-                    return;
-                }
-
-                final AlarmInstance nextAlarm = AlarmStateManager.getNextFiringAlarm(mContext);
-                if (nextAlarm == null) {
-                    final String reason = mContext.getString(R.string.no_scheduled_alarms);
-                    notifyFailureAndLog(reason, mActivity);
-                    return;
-                }
-
-                // get time from nextAlarm and see if there are any other alarms matching this time
-                final Calendar nextTime = nextAlarm.getAlarmTime();
-                final List<Alarm> alarmsFiringAtSameTime = getAlarmsByHourMinutes(
-                        nextTime.get(Calendar.HOUR_OF_DAY), nextTime.get(Calendar.MINUTE), cr);
-                // there might me multiple alarms firing next
-                mMatchingAlarms.addAll(alarmsFiringAtSameTime);
-                break;
-            case AlarmClock.ALARM_SEARCH_MODE_ALL:
-                mMatchingAlarms.addAll(mAlarms);
-                break;
-            case AlarmClock.ALARM_SEARCH_MODE_LABEL:
-                // EXTRA_MESSAGE has to be set in this mode
-                final String label = mIntent.getStringExtra(AlarmClock.EXTRA_MESSAGE);
-                if (label == null) {
-                    final String reason = mContext.getString(R.string.no_label_specified);
-                    notifyFailureAndLog(reason, mActivity);
-                    return;
-                }
-
-                // there might me multiple alarms with this label
-                for (Alarm alarm : mAlarms) {
-                    if (alarm.label.contains(label)) {
-                        mMatchingAlarms.add(alarm);
-                    }
-                }
-
-                if (mMatchingAlarms.isEmpty()) {
-                    final String reason = mContext.getString(R.string.no_alarms_with_label);
-                    notifyFailureAndLog(reason, mActivity);
-                    return;
-                }
-                break;
-        }
-    }
-
-    private List<Alarm> getAlarmsByHourMinutes(int hour24, int minutes, ContentResolver cr) {
-        // if we want to dismiss we should only add enabled alarms
-        final String selection = String.format("%s=? AND %s=? AND %s=?",
-                Alarm.HOUR, Alarm.MINUTES, Alarm.ENABLED);
-        final String[] args = { String.valueOf(hour24), String.valueOf(minutes), "1" };
-        return Alarm.getAlarms(cr, selection, args);
-    }
-
-    public List<Alarm> getMatchingAlarms() {
-        return mMatchingAlarms;
-    }
-
-    private void notifyFailureAndLog(String reason, Activity activity) {
-        LogUtils.e(reason);
-        Controller.getController().notifyVoiceFailure(activity, reason);
-    }
-}
diff --git a/src/com/android/deskclock/FetchMatchingAlarmsAction.kt b/src/com/android/deskclock/FetchMatchingAlarmsAction.kt
new file mode 100644
index 0000000..a706dda
--- /dev/null
+++ b/src/com/android/deskclock/FetchMatchingAlarmsAction.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Activity
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+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
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.text.DateFormatSymbols
+import java.util.Calendar
+
+/**
+ * Returns a list of alarms that are specified by the intent
+ * processed by HandleDeskClockApiCalls
+ * if there are more than 1 matching alarms and the SEARCH_MODE is not ALL
+ * we show a picker UI dialog
+ */
+internal class FetchMatchingAlarmsAction(
+    private val mContext: Context,
+    private val mAlarms: List<Alarm>,
+    private val mIntent: Intent,
+    private val mActivity: Activity
+) : Runnable {
+    private val mMatchingAlarms: MutableList<Alarm> = ArrayList()
+
+    override fun run() {
+        Utils.enforceNotMainLooper()
+
+        val searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE)
+        // if search mode isn't specified show all alarms in the UI picker
+        if (searchMode == null) {
+            mMatchingAlarms.addAll(mAlarms)
+            return
+        }
+
+        val cr = mContext.contentResolver
+        when (searchMode) {
+            AlarmClock.ALARM_SEARCH_MODE_TIME -> {
+                // at least one of these has to be specified in this search mode.
+                val hour = mIntent.getIntExtra(AlarmClock.EXTRA_HOUR, -1)
+                // if minutes weren't specified default to 0
+                val minutes = mIntent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0)
+                val isPm = mIntent.extras!![AlarmClock.EXTRA_IS_PM] as Boolean?
+                var badInput = isPm != null && hour > 12 && isPm
+                badInput = badInput or (hour < 0 || hour > 23)
+                badInput = badInput or (minutes < 0 || minutes > 59)
+
+                if (badInput) {
+                    val ampm = DateFormatSymbols().amPmStrings
+                    val amPm = if (isPm == null) "" else (if (isPm) ampm[1] else ampm[0])
+                    val reason = mContext.getString(R.string.invalid_time, hour, minutes, amPm)
+                    notifyFailureAndLog(reason, mActivity)
+                    return
+                }
+
+                val hour24 = if (java.lang.Boolean.TRUE == isPm && hour < 12) hour + 12 else hour
+
+                // there might me multiple alarms at the same time
+                for (alarm in mAlarms) {
+                    if (alarm.hour == hour24 && alarm.minutes == minutes) {
+                        mMatchingAlarms.add(alarm)
+                    }
+                }
+                if (mMatchingAlarms.isEmpty()) {
+                    val reason = mContext.getString(R.string.no_alarm_at, hour24, minutes)
+                    notifyFailureAndLog(reason, mActivity)
+                    return
+                }
+            }
+            AlarmClock.ALARM_SEARCH_MODE_NEXT -> {
+                // Match currently firing alarms before scheduled alarms.
+                for (alarm in mAlarms) {
+                    val alarmInstance = AlarmInstance.getNextUpcomingInstanceByAlarmId(cr, alarm.id)
+                    if (alarmInstance != null &&
+                            alarmInstance.mAlarmState == InstancesColumns.FIRED_STATE) {
+                        mMatchingAlarms.add(alarm)
+                    }
+                }
+                if (mMatchingAlarms.isNotEmpty()) {
+                    // return the matched firing alarms
+                    return
+                }
+                val nextAlarm = AlarmStateManager.getNextFiringAlarm(mContext)
+                if (nextAlarm == null) {
+                    val reason = mContext.getString(R.string.no_scheduled_alarms)
+                    notifyFailureAndLog(reason, mActivity)
+                    return
+                }
+
+                // get time from nextAlarm and see if there are any other alarms matching this time
+                val nextTime: Calendar = nextAlarm.alarmTime
+                val alarmsFiringAtSameTime = getAlarmsByHourMinutes(
+                        nextTime[Calendar.HOUR_OF_DAY], nextTime[Calendar.MINUTE], cr)
+                // there might me multiple alarms firing next
+                mMatchingAlarms.addAll(alarmsFiringAtSameTime)
+            }
+            AlarmClock.ALARM_SEARCH_MODE_ALL -> mMatchingAlarms.addAll(mAlarms)
+            AlarmClock.ALARM_SEARCH_MODE_LABEL -> {
+                // EXTRA_MESSAGE has to be set in this mode
+                val label = mIntent.getStringExtra(AlarmClock.EXTRA_MESSAGE)
+                if (label == null) {
+                    val reason = mContext.getString(R.string.no_label_specified)
+                    notifyFailureAndLog(reason, mActivity)
+                    return
+                }
+
+                // there might me multiple alarms with this label
+                for (alarm in mAlarms) {
+                    if (alarm.label!!.contains(label)) {
+                        mMatchingAlarms.add(alarm)
+                    }
+                }
+
+                if (mMatchingAlarms.isEmpty()) {
+                    val reason = mContext.getString(R.string.no_alarms_with_label)
+                    notifyFailureAndLog(reason, mActivity)
+                    return
+                }
+            }
+        }
+    }
+
+    private fun getAlarmsByHourMinutes(
+        hour24: Int,
+        minutes: Int,
+        cr: ContentResolver
+    ): List<Alarm> {
+        // if we want to dismiss we should only add enabled alarms
+        val selection = String.format("%s=? AND %s=? AND %s=?",
+                AlarmsColumns.HOUR, AlarmsColumns.MINUTES, AlarmsColumns.ENABLED)
+        val args = arrayOf(hour24.toString(), minutes.toString(), "1")
+        return Alarm.getAlarms(cr, selection, *args)
+    }
+
+    val matchingAlarms: List<Alarm>
+        get() = mMatchingAlarms
+
+    private fun notifyFailureAndLog(reason: String, activity: Activity) {
+        LogUtils.e(reason)
+        Controller.getController().notifyVoiceFailure(activity, reason)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FormattedTextUtils.java b/src/com/android/deskclock/FormattedTextUtils.java
deleted file mode 100644
index 80833ee..0000000
--- a/src/com/android/deskclock/FormattedTextUtils.java
+++ /dev/null
@@ -1,46 +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.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/FormattedTextUtils.kt b/src/com/android/deskclock/FormattedTextUtils.kt
new file mode 100644
index 0000000..bd11690
--- /dev/null
+++ b/src/com/android/deskclock/FormattedTextUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.
+ */
+object FormattedTextUtils {
+    /**
+     * Applies a span over the length of the given text.
+     *
+     * @param text the [CharSequence] to be formatted
+     * @param span the span to apply
+     * @return the text with the span applied
+     */
+    @JvmStatic
+    fun formatText(text: CharSequence?, span: Any?): CharSequence? {
+        if (text == null) {
+            return null
+        }
+
+        val formattedText = SpannableString.valueOf(text)
+        formattedText.setSpan(span, 0, formattedText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+        return formattedText
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FragmentTabPagerAdapter.java b/src/com/android/deskclock/FragmentTabPagerAdapter.java
deleted file mode 100644
index 3682c86..0000000
--- a/src/com/android/deskclock/FragmentTabPagerAdapter.java
+++ /dev/null
@@ -1,168 +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.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import androidx.legacy.app.FragmentCompat;
-import androidx.viewpager.widget.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/FragmentTabPagerAdapter.kt b/src/com/android/deskclock/FragmentTabPagerAdapter.kt
new file mode 100644
index 0000000..1c920f5
--- /dev/null
+++ b/src/com/android/deskclock/FragmentTabPagerAdapter.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util.ArrayMap
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.viewpager.widget.PagerAdapter
+
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * 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.
+ */
+internal class FragmentTabPagerAdapter(private val mDeskClock: DeskClock) : PagerAdapter() {
+
+    /** The manager into which fragments are added.  */
+    private val mFragmentManager: FragmentManager = mDeskClock.supportFragmentManager
+
+    /** A fragment cache that can be accessed before [.instantiateItem] is called.  */
+    private val mFragmentCache: MutableMap<UiDataModel.Tab, DeskClockFragment?> =
+            ArrayMap(getCount())
+
+    /** The active fragment transaction if one exists.  */
+    private var mCurrentTransaction: FragmentTransaction? = null
+
+    /** The current fragment displayed to the user.  */
+    private var mCurrentPrimaryItem: Fragment? = null
+
+    override fun getCount(): Int = UiDataModel.uiDataModel.tabCount
+
+    /**
+     * @param position the left-to-right index of the fragment to be returned
+     * @return the fragment displayed at the given `position`
+     */
+    fun getDeskClockFragment(position: Int): DeskClockFragment {
+        // Fetch the tab the UiDataModel reports for the position.
+        val tab: UiDataModel.Tab = UiDataModel.uiDataModel.getTabAt(position)
+
+        // First check the local cache for the fragment.
+        var fragment = mFragmentCache[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 = mFragmentManager.findFragmentByTag(tab.name) as DeskClockFragment?
+        if (fragment != null) {
+            fragment.setFabContainer(mDeskClock)
+            mFragmentCache[tab] = fragment
+            return fragment
+        }
+
+        // Otherwise, build the fragment from scratch.
+        val fragmentClassName: String = tab.fragmentClassName
+        fragment = Fragment.instantiate(mDeskClock, fragmentClassName) as DeskClockFragment
+        fragment.setFabContainer(mDeskClock)
+        mFragmentCache[tab] = fragment
+        return fragment
+    }
+
+    override fun startUpdate(container: ViewGroup) {
+        check(container.id != View.NO_ID) { "ViewPager with adapter $this has no id" }
+    }
+
+    override fun instantiateItem(container: ViewGroup, position: Int): Any {
+        if (mCurrentTransaction == null) {
+            mCurrentTransaction = mFragmentManager.beginTransaction()
+        }
+
+        // Use the fragment located in the fragment manager if one exists.
+        val tab: UiDataModel.Tab = UiDataModel.uiDataModel.getTabAt(position)
+        var fragment = mFragmentManager.findFragmentByTag(tab.name)
+        if (fragment != null) {
+            mCurrentTransaction!!.attach(fragment)
+        } else {
+            fragment = getDeskClockFragment(position)
+            mCurrentTransaction!!.add(container.id, fragment, tab.name)
+        }
+
+        if (fragment !== mCurrentPrimaryItem) {
+            fragment.setMenuVisibility(false)
+            fragment.setUserVisibleHint(false)
+        }
+
+        return fragment
+    }
+
+    override fun destroyItem(container: ViewGroup, position: Int, any: Any) {
+        if (mCurrentTransaction == null) {
+            mCurrentTransaction = mFragmentManager.beginTransaction()
+        }
+        val fragment = any as DeskClockFragment
+        fragment.setFabContainer(null)
+        mCurrentTransaction!!.detach(fragment)
+    }
+
+    override fun setPrimaryItem(container: ViewGroup, position: Int, any: Any) {
+        val fragment = any as Fragment
+        if (fragment !== mCurrentPrimaryItem) {
+            mCurrentPrimaryItem?.let {
+                it.setMenuVisibility(false)
+                it.setUserVisibleHint(false)
+            }
+            fragment.setMenuVisibility(true)
+            fragment.setUserVisibleHint(true)
+            mCurrentPrimaryItem = fragment
+        }
+    }
+
+    override fun finishUpdate(container: ViewGroup) {
+        if (mCurrentTransaction != null) {
+            mCurrentTransaction!!.commitAllowingStateLoss()
+            mCurrentTransaction = null
+            mFragmentManager.executePendingTransactions()
+        }
+    }
+
+    override fun isViewFromObject(view: View, any: Any): Boolean = (any as Fragment).view === view
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/HandleApiCalls.java b/src/com/android/deskclock/HandleApiCalls.java
deleted file mode 100644
index a4a7012..0000000
--- a/src/com/android/deskclock/HandleApiCalls.java
+++ /dev/null
@@ -1,633 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.Activity;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.provider.AlarmClock;
-import android.text.TextUtils;
-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.timer.TimerFragment;
-import com.android.deskclock.timer.TimerService;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static com.android.deskclock.AlarmSelectionActivity.ACTION_DISMISS;
-import static com.android.deskclock.AlarmSelectionActivity.EXTRA_ACTION;
-import static com.android.deskclock.AlarmSelectionActivity.EXTRA_ALARMS;
-import static com.android.deskclock.provider.AlarmInstance.FIRED_STATE;
-import static com.android.deskclock.provider.AlarmInstance.SNOOZE_STATE;
-import static com.android.deskclock.uidata.UiDataModel.Tab.ALARMS;
-import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
-
-/**
- * This activity is never visible. It processes all public intents defined by {@link AlarmClock}
- * that apply to alarms and timers. Its definition in AndroidManifest.xml requires callers to hold
- * the com.android.alarm.permission.SET_ALARM permission to complete the requested action.
- */
-public class HandleApiCalls extends Activity {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("HandleApiCalls");
-
-    private Context mAppContext;
-
-    @Override
-    protected void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-
-        mAppContext = getApplicationContext();
-
-        try {
-            final Intent intent = getIntent();
-            final String action = intent == null ? null : intent.getAction();
-            if (action == null) {
-                return;
-            }
-            LOGGER.i("onCreate: " + intent);
-
-            switch (action) {
-
-                case AlarmClock.ACTION_SET_ALARM:
-                    handleSetAlarm(intent);
-                    break;
-                case AlarmClock.ACTION_SHOW_ALARMS:
-                    handleShowAlarms();
-                    break;
-                case AlarmClock.ACTION_SET_TIMER:
-                    handleSetTimer(intent);
-                    break;
-                case AlarmClock.ACTION_SHOW_TIMERS:
-                    handleShowTimers(intent);
-                    break;
-                case AlarmClock.ACTION_DISMISS_ALARM:
-                    handleDismissAlarm(intent);
-                    break;
-                case AlarmClock.ACTION_SNOOZE_ALARM:
-                    handleSnoozeAlarm(intent);
-                    break;
-                case AlarmClock.ACTION_DISMISS_TIMER:
-                    handleDismissTimer(intent);
-                    break;
-            }
-        } catch (Exception e) {
-            LOGGER.wtf(e);
-        } finally {
-            finish();
-        }
-    }
-
-
-    private void handleDismissAlarm(Intent intent) {
-        // Change to the alarms tab.
-        UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
-        // Open DeskClock which is now positioned on the alarms tab.
-        startActivity(new Intent(mAppContext, DeskClock.class));
-
-        new DismissAlarmAsync(mAppContext, intent, this).execute();
-    }
-
-    public static void dismissAlarm(Alarm alarm, Activity activity) {
-        final Context context = activity.getApplicationContext();
-        final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
-                context.getContentResolver(), alarm.id);
-        if (instance == null) {
-            final String reason = context.getString(R.string.no_alarm_scheduled_for_this_time);
-            Controller.getController().notifyVoiceFailure(activity, reason);
-            LOGGER.i("No alarm instance to dismiss");
-            return;
-        }
-
-        dismissAlarmInstance(instance, activity);
-    }
-
-    public static void dismissAlarmInstance(AlarmInstance instance, Activity activity) {
-        Utils.enforceNotMainLooper();
-
-        final Context context = activity.getApplicationContext();
-        final Date alarmTime = instance.getAlarmTime().getTime();
-        final String time = DateFormat.getTimeFormat(context).format(alarmTime);
-
-        if (instance.mAlarmState == FIRED_STATE || instance.mAlarmState == SNOOZE_STATE) {
-            // Always dismiss alarms that are fired or snoozed.
-            AlarmStateManager.deleteInstanceAndUpdateParent(context, instance);
-        } else if (Utils.isAlarmWithin24Hours(instance)) {
-            // Upcoming alarms are always predismissed.
-            AlarmStateManager.setPreDismissState(context, instance);
-        } else {
-            // 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);
-            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);
-        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;
-        private final Intent mIntent;
-        private final Activity mActivity;
-
-        public DismissAlarmAsync(Context context, Intent intent, Activity activity) {
-            mContext = context;
-            mIntent = intent;
-            mActivity = activity;
-        }
-
-        @Override
-        protected Void doInBackground(Void... parameters) {
-            final ContentResolver cr = mContext.getContentResolver();
-            final List<Alarm> alarms = getEnabledAlarms(mContext);
-            if (alarms.isEmpty()) {
-                final String reason = mContext.getString(R.string.no_scheduled_alarms);
-                Controller.getController().notifyVoiceFailure(mActivity, reason);
-                LOGGER.i("No scheduled alarms");
-                return null;
-            }
-
-            // remove Alarms in MISSED, DISMISSED, and PREDISMISSED states
-            for (Iterator<Alarm> i = alarms.iterator(); i.hasNext();) {
-                final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
-                        cr, i.next().id);
-                if (instance == null || instance.mAlarmState > FIRED_STATE) {
-                    i.remove();
-                }
-            }
-
-            final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE);
-            if (searchMode == null && alarms.size() > 1) {
-                // shows the UI where user picks which alarm they want to DISMISS
-                final Intent pickSelectionIntent = new Intent(mContext,
-                        AlarmSelectionActivity.class)
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                        .putExtra(EXTRA_ACTION, ACTION_DISMISS)
-                        .putExtra(EXTRA_ALARMS, alarms.toArray(new Parcelable[alarms.size()]));
-                mContext.startActivity(pickSelectionIntent);
-                final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
-                Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
-                return null;
-            }
-
-            // fetch the alarms that are specified by the intent
-            final FetchMatchingAlarmsAction fmaa =
-                    new FetchMatchingAlarmsAction(mContext, alarms, mIntent, mActivity);
-            fmaa.run();
-            final List<Alarm> matchingAlarms = fmaa.getMatchingAlarms();
-
-            // If there are multiple matching alarms and it wasn't expected
-            // disambiguate what the user meant
-            if (!AlarmClock.ALARM_SEARCH_MODE_ALL.equals(searchMode) && matchingAlarms.size() > 1) {
-              final Intent pickSelectionIntent = new Intent(mContext, AlarmSelectionActivity.class)
-                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                        .putExtra(EXTRA_ACTION, ACTION_DISMISS)
-                        .putExtra(EXTRA_ALARMS,
-                                matchingAlarms.toArray(new Parcelable[matchingAlarms.size()]));
-                mContext.startActivity(pickSelectionIntent);
-                final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
-                Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
-                return null;
-            }
-
-            // Apply the action to the matching alarms
-            for (Alarm alarm : matchingAlarms) {
-                dismissAlarm(alarm, mActivity);
-                LOGGER.i("Alarm dismissed: " + alarm);
-            }
-            return null;
-        }
-
-        private static List<Alarm> getEnabledAlarms(Context context) {
-            final String selection = String.format("%s=?", Alarm.ENABLED);
-            final String[] args = { "1" };
-            return Alarm.getAlarms(context.getContentResolver(), selection, args);
-        }
-    }
-
-    private void handleSnoozeAlarm(Intent intent) {
-        new SnoozeAlarmAsync(intent, this).execute();
-    }
-
-    private static class SnoozeAlarmAsync extends AsyncTask<Void, Void, Void> {
-
-        private final Context mContext;
-        private final Intent mIntent;
-        private final Activity mActivity;
-
-        public SnoozeAlarmAsync(Intent intent, Activity activity) {
-            mContext = activity.getApplicationContext();
-            mIntent = intent;
-            mActivity = activity;
-        }
-
-        @Override
-        protected Void doInBackground(Void... parameters) {
-            final ContentResolver cr = mContext.getContentResolver();
-            final List<AlarmInstance> alarmInstances = AlarmInstance.getInstancesByState(
-                    cr, FIRED_STATE);
-            if (alarmInstances.isEmpty()) {
-                final String reason = mContext.getString(R.string.no_firing_alarms);
-                Controller.getController().notifyVoiceFailure(mActivity, reason);
-                LOGGER.i("No firing alarms");
-                return null;
-            }
-
-            for (AlarmInstance firingAlarmInstance : alarmInstances) {
-                snoozeAlarm(firingAlarmInstance, mContext, mActivity);
-            }
-            return null;
-        }
-    }
-
-    static void snoozeAlarm(AlarmInstance alarmInstance, Context context, Activity activity) {
-        Utils.enforceNotMainLooper();
-
-        final String time = DateFormat.getTimeFormat(context).format(
-                alarmInstance.getAlarmTime().getTime());
-        final String reason = context.getString(R.string.alarm_is_snoozed, time);
-        AlarmStateManager.setSnoozeState(context, alarmInstance, true);
-
-        Controller.getController().notifyVoiceSuccess(activity, reason);
-        LOGGER.i("Alarm snoozed: " + alarmInstance);
-        Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
-    }
-
-    /***
-     * Processes the SET_ALARM intent
-     * @param intent Intent passed to the app
-     */
-    private void handleSetAlarm(Intent intent) {
-        // Validate the hour, if one was given.
-        int hour = -1;
-        if (intent.hasExtra(AlarmClock.EXTRA_HOUR)) {
-            hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, hour);
-            if (hour < 0 || hour > 23) {
-                final int mins = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
-                final String voiceMessage = getString(R.string.invalid_time, hour, mins, " ");
-                Controller.getController().notifyVoiceFailure(this, voiceMessage);
-                LOGGER.i("Illegal hour: " + hour);
-                return;
-            }
-        }
-
-        // Validate the minute, if one was given.
-        final int minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
-        if (minutes < 0 || minutes > 59) {
-            final String voiceMessage = getString(R.string.invalid_time, hour, minutes, " ");
-            Controller.getController().notifyVoiceFailure(this, voiceMessage);
-            LOGGER.i("Illegal minute: " + minutes);
-            return;
-        }
-
-        final boolean skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false);
-        final ContentResolver cr = getContentResolver();
-
-        // If time information was not provided an existing alarm cannot be located and a new one
-        // cannot be created so show the UI for creating the alarm from scratch per spec.
-        if (hour == -1) {
-            // Change to the alarms tab.
-            UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
-            // Intent has no time or an invalid time, open the alarm creation UI.
-            final Intent createAlarm = Alarm.createIntent(this, DeskClock.class, Alarm.INVALID_ID)
-                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                    .putExtra(AlarmClockFragment.ALARM_CREATE_NEW_INTENT_EXTRA, true);
-
-            // Open DeskClock which is now positioned on the alarms tab.
-            startActivity(createAlarm);
-            final String voiceMessage = getString(R.string.invalid_time, hour, minutes, " ");
-            Controller.getController().notifyVoiceFailure(this, voiceMessage);
-            LOGGER.i("Missing alarm time; opening UI");
-            return;
-        }
-
-        final StringBuilder selection = new StringBuilder();
-        final List<String> argsList = new ArrayList<>();
-        setSelectionFromIntent(intent, hour, minutes, selection, argsList);
-
-        // Try to locate an existing alarm using the intent data.
-        final String[] args = argsList.toArray(new String[argsList.size()]);
-        final List<Alarm> alarms = Alarm.getAlarms(cr, selection.toString(), args);
-
-        final Alarm alarm;
-        if (!alarms.isEmpty()) {
-            // Enable the first matching alarm.
-            alarm = alarms.get(0);
-            alarm.enabled = true;
-            Alarm.updateAlarm(cr, alarm);
-
-            // Delete all old instances.
-            AlarmStateManager.deleteAllInstances(this, alarm.id);
-
-            Events.sendAlarmEvent(R.string.action_update, R.string.label_intent);
-            LOGGER.i("Updated alarm: " + alarm);
-        } else {
-            // No existing alarm could be located; create one using the intent data.
-            alarm = new Alarm();
-            updateAlarmFromIntent(alarm, intent);
-            alarm.deleteAfterUse = !alarm.daysOfWeek.isRepeating() && skipUi;
-
-            // Save the new alarm.
-            Alarm.addAlarm(cr, alarm);
-
-            Events.sendAlarmEvent(R.string.action_create, R.string.label_intent);
-            LOGGER.i("Created new alarm: " + alarm);
-        }
-
-        // Schedule the next instance.
-        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());
-        Controller.getController().notifyVoiceSuccess(this, getString(R.string.alarm_is_set, time));
-    }
-
-    private void handleDismissTimer(Intent intent) {
-        final Uri dataUri = intent.getData();
-        if (dataUri != null) {
-            final Timer selectedTimer = getSelectedTimer(dataUri);
-            if (selectedTimer != null) {
-                DataModel.getDataModel().resetOrDeleteTimer(selectedTimer, R.string.label_intent);
-                Controller.getController().notifyVoiceSuccess(this,
-                        getResources().getQuantityString(R.plurals.expired_timers_dismissed, 1));
-                LOGGER.i("Timer dismissed: " + selectedTimer);
-            } else {
-                Controller.getController().notifyVoiceFailure(this,
-                        getString(R.string.invalid_timer));
-                LOGGER.e("Could not dismiss timer: invalid URI");
-            }
-        } else {
-            final List<Timer> expiredTimers = DataModel.getDataModel().getExpiredTimers();
-            if (!expiredTimers.isEmpty()) {
-                for (Timer timer : expiredTimers) {
-                    DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_intent);
-                }
-                final int numberOfTimers = expiredTimers.size();
-                final String timersDismissedMessage = getResources().getQuantityString(
-                        R.plurals.expired_timers_dismissed, numberOfTimers, numberOfTimers);
-                Controller.getController().notifyVoiceSuccess(this, timersDismissedMessage);
-                LOGGER.i(timersDismissedMessage);
-            } else {
-                Controller.getController().notifyVoiceFailure(this,
-                        getString(R.string.no_expired_timers));
-                LOGGER.e("Could not dismiss timer: no expired timers");
-            }
-        }
-    }
-
-    private Timer getSelectedTimer(Uri dataUri) {
-        try {
-            final int timerId = (int) ContentUris.parseId(dataUri);
-            return DataModel.getDataModel().getTimer(timerId);
-        } catch (NumberFormatException e) {
-            return null;
-        }
-    }
-
-    private void handleShowAlarms() {
-        Events.sendAlarmEvent(R.string.action_show, R.string.label_intent);
-
-        // Open DeskClock positioned on the alarms tab.
-        UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-        startActivity(new Intent(this, DeskClock.class));
-    }
-
-    private void handleShowTimers(Intent intent) {
-        Events.sendTimerEvent(R.string.action_show, R.string.label_intent);
-
-        final Intent showTimersIntent = new Intent(this, DeskClock.class);
-
-        final List<Timer> timers = DataModel.getDataModel().getTimers();
-        if (!timers.isEmpty()) {
-            final Timer newestTimer = timers.get(timers.size() - 1);
-            showTimersIntent.putExtra(TimerService.EXTRA_TIMER_ID, newestTimer.getId());
-        }
-
-        // Open DeskClock positioned on the timers tab.
-        UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
-        startActivity(showTimersIntent);
-    }
-
-    private void handleSetTimer(Intent intent) {
-        // If no length is supplied, show the timer setup view.
-        if (!intent.hasExtra(AlarmClock.EXTRA_LENGTH)) {
-            // Change to the timers tab.
-            UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
-
-            // Open DeskClock which is now positioned on the timers tab and show the timer setup.
-            startActivity(TimerFragment.createTimerSetupIntent(this));
-            LOGGER.i("Showing timer setup");
-            return;
-        }
-
-        // 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) {
-            final String voiceMessage = getString(R.string.invalid_timer_length);
-            Controller.getController().notifyVoiceFailure(this, voiceMessage);
-            LOGGER.i("Invalid timer length requested: " + lengthMillis);
-            return;
-        }
-
-        final String label = getLabelFromIntent(intent, "");
-        final boolean skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false);
-
-        // Attempt to reuse an existing timer that is Reset with the same length and label.
-        Timer timer = null;
-        for (Timer t : DataModel.getDataModel().getTimers()) {
-            if (!t.isReset()) { continue; }
-            if (t.getLength() != lengthMillis) { continue; }
-            if (!TextUtils.equals(label, t.getLabel())) { continue; }
-
-            timer = t;
-            break;
-        }
-
-        // Create a new timer if one could not be reused.
-        if (timer == null) {
-            timer = DataModel.getDataModel().addTimer(lengthMillis, label, skipUi);
-            Events.sendTimerEvent(R.string.action_create, R.string.label_intent);
-        }
-
-        // Start the selected timer.
-        DataModel.getDataModel().startTimer(timer);
-        Events.sendTimerEvent(R.string.action_start, R.string.label_intent);
-        Controller.getController().notifyVoiceSuccess(this, getString(R.string.timer_created));
-
-        // If not instructed to skip the UI, display the running timer.
-        if (!skipUi) {
-            // Change to the timers tab.
-            UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
-
-            // Open DeskClock which is now positioned on the timers tab.
-            startActivity(new Intent(this, DeskClock.class)
-                    .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()));
-        }
-    }
-
-    private void setupInstance(AlarmInstance instance, boolean skipUi) {
-        instance = AlarmInstance.addInstance(this.getContentResolver(), instance);
-        AlarmStateManager.registerInstance(this, instance, true);
-        AlarmUtils.popAlarmSetToast(this, instance.getAlarmTime().getTimeInMillis());
-        if (!skipUi) {
-            // Change to the alarms tab.
-            UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
-            // Open DeskClock which is now positioned on the alarms tab.
-            final Intent showAlarm = Alarm.createIntent(this, DeskClock.class, instance.mAlarmId)
-                    .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, instance.mAlarmId)
-                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            startActivity(showAlarm);
-        }
-    }
-
-    /**
-     * @param alarm the alarm to be updated
-     * @param intent the intent containing new alarm field values to merge into the {@code alarm}
-     */
-    private static void updateAlarmFromIntent(Alarm alarm, Intent intent) {
-        alarm.enabled = true;
-        alarm.hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, alarm.hour);
-        alarm.minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, alarm.minutes);
-        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);
-    }
-
-    private static String getLabelFromIntent(Intent intent, String defaultLabel) {
-        final String message = intent.getExtras().getString(AlarmClock.EXTRA_MESSAGE, defaultLabel);
-        return message == null ? "" : message;
-    }
-
-    private static Weekdays getDaysFromIntent(Intent intent, Weekdays defaultWeekdays) {
-        if (!intent.hasExtra(AlarmClock.EXTRA_DAYS)) {
-            return defaultWeekdays;
-        }
-
-        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);
-            }
-            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) {
-                return Weekdays.fromCalendarDays(daysArray);
-            }
-        }
-        return defaultWeekdays;
-    }
-
-    private static Uri getAlertFromIntent(Intent intent, Uri defaultUri) {
-        final String alert = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE);
-        if (alert == null) {
-            return defaultUri;
-        } else if (AlarmClock.VALUE_RINGTONE_SILENT.equals(alert) || alert.isEmpty()) {
-            return Alarm.NO_RINGTONE_URI;
-        }
-
-        return Uri.parse(alert);
-    }
-
-    /**
-     * Assemble a database where clause to search for an alarm matching the given {@code hour} and
-     * {@code minutes} as well as all of the optional information within the {@code intent}
-     * including:
-     *
-     * <ul>
-     *     <li>alarm message</li>
-     *     <li>repeat days</li>
-     *     <li>vibration setting</li>
-     *     <li>ringtone uri</li>
-     * </ul>
-     *
-     * @param intent contains details of the alarm to be located
-     * @param hour the hour of the day of the alarm
-     * @param minutes the minute of the hour of the alarm
-     * @param selection an out parameter containing a SQL where clause
-     * @param args an out parameter containing the values to substitute into the {@code selection}
-     */
-    private void setSelectionFromIntent(
-            Intent intent,
-            int hour,
-            int minutes,
-            StringBuilder selection,
-            List<String> args) {
-        selection.append(Alarm.HOUR).append("=?");
-        args.add(String.valueOf(hour));
-        selection.append(" AND ").append(Alarm.MINUTES).append("=?");
-        args.add(String.valueOf(minutes));
-
-        if (intent.hasExtra(AlarmClock.EXTRA_MESSAGE)) {
-            selection.append(" AND ").append(Alarm.LABEL).append("=?");
-            args.add(getLabelFromIntent(intent, ""));
-        }
-
-        // 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, Weekdays.NONE).getBits()));
-
-        if (intent.hasExtra(AlarmClock.EXTRA_VIBRATE)) {
-            selection.append(" AND ").append(Alarm.VIBRATE).append("=?");
-            args.add(intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, false) ? "1" : "0");
-        }
-
-        if (intent.hasExtra(AlarmClock.EXTRA_RINGTONE)) {
-            selection.append(" AND ").append(Alarm.RINGTONE).append("=?");
-
-            // If the intent explicitly specified a NULL ringtone, treat it as the default ringtone.
-            final Uri defaultRingtone = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
-            final Uri ringtone = getAlertFromIntent(intent, defaultRingtone);
-            args.add(ringtone.toString());
-        }
-    }
-}
diff --git a/src/com/android/deskclock/HandleApiCalls.kt b/src/com/android/deskclock/HandleApiCalls.kt
new file mode 100644
index 0000000..4e59cc1
--- /dev/null
+++ b/src/com/android/deskclock/HandleApiCalls.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Activity
+import android.content.ContentUris
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Bundle
+import android.os.Parcelable
+import android.provider.AlarmClock
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+
+import com.android.deskclock.AlarmUtils.popAlarmSetToast
+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.ClockContract
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.timer.TimerFragment
+import com.android.deskclock.timer.TimerService
+import com.android.deskclock.uidata.UiDataModel
+
+import java.util.Calendar
+import java.util.Date
+
+/**
+ * This activity is never visible. It processes all public intents defined by [AlarmClock]
+ * that apply to alarms and timers. Its definition in AndroidManifest.xml requires callers to hold
+ * the com.android.alarm.permission.SET_ALARM permission to complete the requested action.
+ */
+// TODO(b/165664115) Replace deprecated AsyncTask calls
+class HandleApiCalls : Activity() {
+    private lateinit var mAppContext: Context
+
+    override fun onCreate(icicle: Bundle?) {
+        super.onCreate(icicle)
+
+        mAppContext = applicationContext
+
+        try {
+            val intent = intent
+            val action = intent?.action ?: return
+            LOGGER.i("onCreate: $intent")
+
+            when (action) {
+                AlarmClock.ACTION_SET_ALARM -> handleSetAlarm(intent)
+                AlarmClock.ACTION_SHOW_ALARMS -> handleShowAlarms()
+                AlarmClock.ACTION_SET_TIMER -> handleSetTimer(intent)
+                AlarmClock.ACTION_SHOW_TIMERS -> handleShowTimers(intent)
+                AlarmClock.ACTION_DISMISS_ALARM -> handleDismissAlarm(intent)
+                AlarmClock.ACTION_SNOOZE_ALARM -> handleSnoozeAlarm(intent)
+                AlarmClock.ACTION_DISMISS_TIMER -> handleDismissTimer(intent)
+            }
+        } catch (e: Exception) {
+            LOGGER.wtf(e)
+        } finally {
+            finish()
+        }
+    }
+
+    private fun handleDismissAlarm(intent: Intent) {
+        // Change to the alarms tab.
+        UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+        // Open DeskClock which is now positioned on the alarms tab.
+        startActivity(Intent(mAppContext, DeskClock::class.java))
+
+        DismissAlarmAsync(mAppContext, intent, this).execute()
+    }
+
+    private class DismissAlarmAsync(
+        private val mContext: Context,
+        private val mIntent: Intent,
+        private val mActivity: Activity
+    ) : AsyncTask<Void?, Void?, Void?>() {
+        override fun doInBackground(vararg parameters: Void?): Void? {
+            val cr = mContext.contentResolver
+            val alarms = getEnabledAlarms(mContext)
+            if (alarms.isEmpty()) {
+                val reason = mContext.getString(R.string.no_scheduled_alarms)
+                Controller.getController().notifyVoiceFailure(mActivity, reason)
+                LOGGER.i("No scheduled alarms")
+                return null
+            }
+
+            // remove Alarms in MISSED, DISMISSED, and PREDISMISSED states
+            val i: MutableIterator<Alarm> = alarms.toMutableList().listIterator()
+            while (i.hasNext()) {
+                val instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(cr, i.next().id)
+                if (instance == null ||
+                        instance.mAlarmState > ClockContract.InstancesColumns.FIRED_STATE) {
+                    i.remove()
+                }
+            }
+
+            val searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE)
+            if (searchMode == null && alarms.size > 1) {
+                // shows the UI where user picks which alarm they want to DISMISS
+                val pickSelectionIntent = Intent(mContext,
+                        AlarmSelectionActivity::class.java)
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                        .putExtra(AlarmSelectionActivity.EXTRA_ACTION,
+                                AlarmSelectionActivity.ACTION_DISMISS)
+                        .putExtra(AlarmSelectionActivity.EXTRA_ALARMS,
+                                alarms.toTypedArray<Parcelable>())
+                mContext.startActivity(pickSelectionIntent)
+                val voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss)
+                Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage)
+                return null
+            }
+
+            // fetch the alarms that are specified by the intent
+            val fmaa = FetchMatchingAlarmsAction(mContext, alarms, mIntent, mActivity)
+            fmaa.run()
+            val matchingAlarms: List<Alarm> = fmaa.matchingAlarms
+
+            // If there are multiple matching alarms and it wasn't expected
+            // disambiguate what the user meant
+            if (AlarmClock.ALARM_SEARCH_MODE_ALL != searchMode && matchingAlarms.size > 1) {
+                val pickSelectionIntent = Intent(mContext, AlarmSelectionActivity::class.java)
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                        .putExtra(AlarmSelectionActivity.EXTRA_ACTION,
+                                AlarmSelectionActivity.ACTION_DISMISS)
+                        .putExtra(AlarmSelectionActivity.EXTRA_ALARMS,
+                                matchingAlarms.toTypedArray<Parcelable>())
+                mContext.startActivity(pickSelectionIntent)
+                val voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss)
+                Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage)
+                return null
+            }
+
+            // Apply the action to the matching alarms
+            for (alarm in matchingAlarms) {
+                dismissAlarm(alarm, mActivity)
+                LOGGER.i("Alarm dismissed: $alarm")
+            }
+            return null
+        }
+
+        companion object {
+            private fun getEnabledAlarms(context: Context): List<Alarm> {
+                val selection = String.format("%s=?", AlarmsColumns.ENABLED)
+                val args = arrayOf("1")
+                return Alarm.getAlarms(context.contentResolver, selection, *args)
+            }
+        }
+    }
+
+    private fun handleSnoozeAlarm(intent: Intent) {
+        SnoozeAlarmAsync(intent, this).execute()
+    }
+
+    private class SnoozeAlarmAsync(
+        private val mIntent: Intent,
+        private val mActivity: Activity
+    ) : AsyncTask<Void?, Void?, Void?>() {
+        private val mContext: Context = mActivity.applicationContext
+
+        override fun doInBackground(vararg parameters: Void?): Void? {
+            val cr = mContext.contentResolver
+            val alarmInstances = AlarmInstance.getInstancesByState(
+                    cr, ClockContract.InstancesColumns.FIRED_STATE)
+            if (alarmInstances.isEmpty()) {
+                val reason = mContext.getString(R.string.no_firing_alarms)
+                Controller.getController().notifyVoiceFailure(mActivity, reason)
+                LOGGER.i("No firing alarms")
+                return null
+            }
+
+            for (firingAlarmInstance in alarmInstances) {
+                snoozeAlarm(firingAlarmInstance, mContext, mActivity)
+            }
+            return null
+        }
+    }
+
+    /**
+     * Processes the SET_ALARM intent
+     * @param intent Intent passed to the app
+     */
+    private fun handleSetAlarm(intent: Intent) {
+        // Validate the hour, if one was given.
+        var hour = -1
+        if (intent.hasExtra(AlarmClock.EXTRA_HOUR)) {
+            hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, hour)
+            if (hour < 0 || hour > 23) {
+                val mins = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0)
+                val voiceMessage = getString(R.string.invalid_time, hour, mins, " ")
+                Controller.getController().notifyVoiceFailure(this, voiceMessage)
+                LOGGER.i("Illegal hour: $hour")
+                return
+            }
+        }
+
+        // Validate the minute, if one was given.
+        val minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0)
+        if (minutes < 0 || minutes > 59) {
+            val voiceMessage = getString(R.string.invalid_time, hour, minutes, " ")
+            Controller.getController().notifyVoiceFailure(this, voiceMessage)
+            LOGGER.i("Illegal minute: $minutes")
+            return
+        }
+
+        val skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false)
+        val cr = contentResolver
+
+        // If time information was not provided an existing alarm cannot be located and a new one
+        // cannot be created so show the UI for creating the alarm from scratch per spec.
+        if (hour == -1) {
+            // Change to the alarms tab.
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+            // Intent has no time or an invalid time, open the alarm creation UI.
+            val createAlarm = Alarm.createIntent(this, DeskClock::class.java, Alarm.INVALID_ID)
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    .putExtra(AlarmClockFragment.ALARM_CREATE_NEW_INTENT_EXTRA, true)
+
+            // Open DeskClock which is now positioned on the alarms tab.
+            startActivity(createAlarm)
+            val voiceMessage = getString(R.string.invalid_time, hour, minutes, " ")
+            Controller.getController().notifyVoiceFailure(this, voiceMessage)
+            LOGGER.i("Missing alarm time; opening UI")
+            return
+        }
+
+        val selection = StringBuilder()
+        val argsList: MutableList<String> = ArrayList()
+        setSelectionFromIntent(intent, hour, minutes, selection, argsList)
+
+        // Try to locate an existing alarm using the intent data.
+        val args = argsList.toTypedArray()
+        val alarms = Alarm.getAlarms(cr, selection.toString(), *args)
+
+        val alarm: Alarm
+        if (alarms.isNotEmpty()) {
+            // Enable the first matching alarm.
+            alarm = alarms[0]
+            alarm.enabled = true
+            Alarm.updateAlarm(cr, alarm)
+
+            // Delete all old instances.
+            AlarmStateManager.deleteAllInstances(this, alarm.id)
+
+            Events.sendAlarmEvent(R.string.action_update, R.string.label_intent)
+            LOGGER.i("Updated alarm: $alarm")
+        } else {
+            // No existing alarm could be located; create one using the intent data.
+            alarm = Alarm()
+            updateAlarmFromIntent(alarm, intent)
+            alarm.deleteAfterUse = !alarm.daysOfWeek.isRepeating && skipUi
+
+            // Save the new alarm.
+            Alarm.addAlarm(cr, alarm)
+
+            Events.sendAlarmEvent(R.string.action_create, R.string.label_intent)
+            LOGGER.i("Created new alarm: $alarm")
+        }
+
+        // Schedule the next instance.
+        val now: Calendar = DataModel.dataModel.calendar
+        val alarmInstance = alarm.createInstanceAfter(now)
+        setupInstance(alarmInstance, skipUi)
+
+        val time = DateFormat.getTimeFormat(this).format(alarmInstance.alarmTime.time)
+        Controller.getController().notifyVoiceSuccess(this, getString(R.string.alarm_is_set, time))
+    }
+
+    private fun handleDismissTimer(intent: Intent) {
+        val dataUri = intent.data
+        if (dataUri != null) {
+            val selectedTimer = getSelectedTimer(dataUri)
+            if (selectedTimer != null) {
+                DataModel.dataModel.resetOrDeleteTimer(selectedTimer, R.string.label_intent)
+                Controller.getController().notifyVoiceSuccess(this,
+                        resources.getQuantityString(R.plurals.expired_timers_dismissed, 1))
+                LOGGER.i("Timer dismissed: $selectedTimer")
+            } else {
+                Controller.getController().notifyVoiceFailure(this,
+                        getString(R.string.invalid_timer))
+                LOGGER.e("Could not dismiss timer: invalid URI")
+            }
+        } else {
+            val expiredTimers: List<Timer> = DataModel.dataModel.expiredTimers
+            if (expiredTimers.isNotEmpty()) {
+                for (timer in expiredTimers) {
+                    DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_intent)
+                }
+                val numberOfTimers = expiredTimers.size
+                val timersDismissedMessage = resources.getQuantityString(
+                        R.plurals.expired_timers_dismissed, numberOfTimers, numberOfTimers)
+                Controller.getController().notifyVoiceSuccess(this, timersDismissedMessage)
+                LOGGER.i(timersDismissedMessage)
+            } else {
+                Controller.getController().notifyVoiceFailure(this,
+                        getString(R.string.no_expired_timers))
+                LOGGER.e("Could not dismiss timer: no expired timers")
+            }
+        }
+    }
+
+    private fun getSelectedTimer(dataUri: Uri): Timer? {
+        return try {
+            val timerId = ContentUris.parseId(dataUri).toInt()
+            DataModel.dataModel.getTimer(timerId)
+        } catch (e: NumberFormatException) {
+            null
+        }
+    }
+
+    private fun handleShowAlarms() {
+        Events.sendAlarmEvent(R.string.action_show, R.string.label_intent)
+
+        // Open DeskClock positioned on the alarms tab.
+        UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+        startActivity(Intent(this, DeskClock::class.java))
+    }
+
+    private fun handleShowTimers(intent: Intent) {
+        Events.sendTimerEvent(R.string.action_show, R.string.label_intent)
+
+        val showTimersIntent = Intent(this, DeskClock::class.java)
+
+        val timers: List<Timer> = DataModel.dataModel.timers
+        if (timers.isNotEmpty()) {
+            val newestTimer = timers[timers.size - 1]
+            showTimersIntent.putExtra(TimerService.EXTRA_TIMER_ID, newestTimer.id)
+        }
+
+        // Open DeskClock positioned on the timers tab.
+        UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+        startActivity(showTimersIntent)
+    }
+
+    private fun handleSetTimer(intent: Intent) {
+        // If no length is supplied, show the timer setup view.
+        if (!intent.hasExtra(AlarmClock.EXTRA_LENGTH)) {
+            // Change to the timers tab.
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+
+            // Open DeskClock which is now positioned on the timers tab and show the timer setup.
+            startActivity(TimerFragment.createTimerSetupIntent(this))
+            LOGGER.i("Showing timer setup")
+            return
+        }
+
+        // Verify that the timer length is between one second and one day.
+        val lengthMillis =
+                DateUtils.SECOND_IN_MILLIS * intent.getIntExtra(AlarmClock.EXTRA_LENGTH, 0)
+        if (lengthMillis < Timer.MIN_LENGTH) {
+            val voiceMessage = getString(R.string.invalid_timer_length)
+            Controller.getController().notifyVoiceFailure(this, voiceMessage)
+            LOGGER.i("Invalid timer length requested: $lengthMillis")
+            return
+        }
+
+        val label = getLabelFromIntent(intent, "")
+        val skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false)
+
+        // Attempt to reuse an existing timer that is Reset with the same length and label.
+        var timer: Timer? = null
+        for (t in DataModel.dataModel.timers) {
+            if (!t.isReset) {
+                continue
+            }
+            if (t.length != lengthMillis) {
+                continue
+            }
+            if (!TextUtils.equals(label, t.label)) {
+                continue
+            }
+
+            timer = t
+            break
+        }
+
+        // Create a new timer if one could not be reused.
+        if (timer == null) {
+            timer = DataModel.dataModel.addTimer(lengthMillis, label, skipUi)
+            Events.sendTimerEvent(R.string.action_create, R.string.label_intent)
+        }
+
+        // Start the selected timer.
+        DataModel.dataModel.startTimer(timer)
+        Events.sendTimerEvent(R.string.action_start, R.string.label_intent)
+        Controller.getController().notifyVoiceSuccess(this, getString(R.string.timer_created))
+
+        // If not instructed to skip the UI, display the running timer.
+        if (!skipUi) {
+            // Change to the timers tab.
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+
+            // Open DeskClock which is now positioned on the timers tab.
+            startActivity(Intent(this, DeskClock::class.java)
+                    .putExtra(TimerService.EXTRA_TIMER_ID, timer.id))
+        }
+    }
+
+    private fun setupInstance(instance: AlarmInstance, skipUi: Boolean) {
+        var variableInstance = instance
+        variableInstance = AlarmInstance.addInstance(this.contentResolver, variableInstance)
+        AlarmStateManager.registerInstance(this, variableInstance, true)
+        popAlarmSetToast(this, variableInstance.alarmTime.timeInMillis)
+        if (!skipUi) {
+            // Change to the alarms tab.
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+            // Open DeskClock which is now positioned on the alarms tab.
+            val showAlarm =
+                    Alarm.createIntent(this, DeskClock::class.java, variableInstance.mAlarmId!!)
+                    .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA,
+                            variableInstance.mAlarmId!!)
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            startActivity(showAlarm)
+        }
+    }
+
+    /**
+     * Assemble a database where clause to search for an alarm matching the given `hour` and
+     * `minutes` as well as all of the optional information within the `intent`
+     * including:
+     * <ul>
+     *     <li>alarm message</li>
+     *     <li>repeat days</li>
+     *     <li>vibration setting</li>
+     *     <li>ringtone uri</li>
+     * </ul>
+     *
+     * @param intent contains details of the alarm to be located
+     * @param hour the hour of the day of the alarm
+     * @param minutes the minute of the hour of the alarm
+     * @param selection an out parameter containing a SQL where clause
+     * @param args an out parameter containing the values to substitute into the `selection`
+     */
+    private fun setSelectionFromIntent(
+        intent: Intent,
+        hour: Int,
+        minutes: Int,
+        selection: StringBuilder,
+        args: MutableList<String>
+    ) {
+        selection.append(AlarmsColumns.HOUR).append("=?")
+        args.add(hour.toString())
+        selection.append(" AND ").append(AlarmsColumns.MINUTES).append("=?")
+        args.add(minutes.toString())
+        if (intent.hasExtra(AlarmClock.EXTRA_MESSAGE)) {
+            selection.append(" AND ").append(AlarmSettingColumns.LABEL).append("=?")
+            args.add(getLabelFromIntent(intent, ""))
+        }
+
+        // Days is treated differently than other fields because if days is not specified, it
+        // explicitly means "not recurring".
+        selection.append(" AND ").append(AlarmsColumns.DAYS_OF_WEEK).append("=?")
+        args.add(getDaysFromIntent(intent, Weekdays.NONE).bits.toString())
+        if (intent.hasExtra(AlarmClock.EXTRA_VIBRATE)) {
+            selection.append(" AND ").append(AlarmSettingColumns.VIBRATE).append("=?")
+            args.add(if (intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, false)) "1" else "0")
+        }
+        if (intent.hasExtra(AlarmClock.EXTRA_RINGTONE)) {
+            selection.append(" AND ").append(AlarmSettingColumns.RINGTONE).append("=?")
+
+            // If the intent explicitly specified a NULL ringtone, treat it as the default ringtone.
+            val defaultRingtone: Uri = DataModel.dataModel.defaultAlarmRingtoneUri
+            val ringtone = getAlertFromIntent(intent, defaultRingtone)
+            args.add(ringtone.toString())
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("HandleApiCalls")
+
+        fun dismissAlarm(alarm: Alarm, activity: Activity) {
+            val context = activity.applicationContext
+            val instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
+                    context.contentResolver, alarm.id)
+            if (instance == null) {
+                val reason = context.getString(R.string.no_alarm_scheduled_for_this_time)
+                Controller.getController().notifyVoiceFailure(activity, reason)
+                LOGGER.i("No alarm instance to dismiss")
+                return
+            }
+
+            dismissAlarmInstance(instance, activity)
+        }
+
+        private fun dismissAlarmInstance(instance: AlarmInstance, activity: Activity) {
+            Utils.enforceNotMainLooper()
+
+            val context = activity.applicationContext
+            val alarmTime: Date = instance.alarmTime.time
+            val time = DateFormat.getTimeFormat(context).format(alarmTime)
+
+            if (instance.mAlarmState == ClockContract.InstancesColumns.FIRED_STATE ||
+                    instance.mAlarmState == ClockContract.InstancesColumns.SNOOZE_STATE) {
+                // Always dismiss alarms that are fired or snoozed.
+                AlarmStateManager.deleteInstanceAndUpdateParent(context, instance)
+            } else if (Utils.isAlarmWithin24Hours(instance)) {
+                // Upcoming alarms are always predismissed.
+                AlarmStateManager.setPreDismissState(context, instance)
+            } else {
+                // Otherwise the alarm cannot be dismissed at this time.
+                val reason = context.getString(
+                        R.string.alarm_cant_be_dismissed_still_more_than_24_hours_away, time)
+                Controller.getController().notifyVoiceFailure(activity, reason)
+                LOGGER.i("Can't dismiss alarm more than 24 hours in advance")
+            }
+
+            // Log the successful dismissal.
+            val reason = context.getString(R.string.alarm_is_dismissed, time)
+            Controller.getController().notifyVoiceSuccess(activity, reason)
+            LOGGER.i("Alarm dismissed: $instance")
+            Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent)
+        }
+
+        fun snoozeAlarm(alarmInstance: AlarmInstance, context: Context, activity: Activity) {
+            Utils.enforceNotMainLooper()
+
+            val time = DateFormat.getTimeFormat(context).format(
+                    alarmInstance.alarmTime.time)
+            val reason = context.getString(R.string.alarm_is_snoozed, time)
+            AlarmStateManager.setSnoozeState(context, alarmInstance, true)
+
+            Controller.getController().notifyVoiceSuccess(activity, reason)
+            LOGGER.i("Alarm snoozed: $alarmInstance")
+            Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent)
+        }
+
+        /**
+         * @param alarm the alarm to be updated
+         * @param intent the intent containing new alarm field values to merge into the `alarm`
+         */
+        private fun updateAlarmFromIntent(alarm: Alarm, intent: Intent) {
+            alarm.enabled = true
+            alarm.hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, alarm.hour)
+            alarm.minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, alarm.minutes)
+            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)
+        }
+
+        private fun getLabelFromIntent(intent: Intent?, defaultLabel: String?): String {
+            val message = intent!!.extras!!.getString(AlarmClock.EXTRA_MESSAGE, defaultLabel)
+            return message ?: ""
+        }
+
+        private fun getDaysFromIntent(intent: Intent, defaultWeekdays: Weekdays): Weekdays {
+            if (!intent.hasExtra(AlarmClock.EXTRA_DAYS)) {
+                return defaultWeekdays
+            }
+
+            val days: List<Int>? = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS)
+            if (days != null) {
+                val daysArray = IntArray(days.size)
+                for (i in days.indices) {
+                    daysArray[i] = days[i]
+                }
+                return Weekdays.fromCalendarDays(*daysArray)
+            } else {
+                // API says to use an ArrayList<Integer> but we allow the user to use a int[] too.
+                val daysArray = intent.getIntArrayExtra(AlarmClock.EXTRA_DAYS)
+                if (daysArray != null) {
+                    return Weekdays.fromCalendarDays(*daysArray)
+                }
+            }
+            return defaultWeekdays
+        }
+
+        private fun getAlertFromIntent(intent: Intent, defaultUri: Uri): Uri {
+            val alert = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE)
+            if (alert == null) {
+                return defaultUri
+            } else if (AlarmClock.VALUE_RINGTONE_SILENT == alert || alert.isEmpty()) {
+                return AlarmSettingColumns.NO_RINGTONE_URI
+            }
+
+            return Uri.parse(alert)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/HandleShortcuts.java b/src/com/android/deskclock/HandleShortcuts.java
deleted file mode 100644
index 68bc42e..0000000
--- a/src/com/android/deskclock/HandleShortcuts.java
+++ /dev/null
@@ -1,70 +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.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-import com.android.deskclock.events.Events;
-import com.android.deskclock.stopwatch.StopwatchService;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
-
-public class HandleShortcuts extends Activity {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("HandleShortcuts");
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        final Intent intent = getIntent();
-
-        try {
-            final String action = intent.getAction();
-            switch (action) {
-                case StopwatchService.ACTION_PAUSE_STOPWATCH:
-                    Events.sendStopwatchEvent(R.string.action_pause, R.string.label_shortcut);
-
-                    // Open DeskClock positioned on the stopwatch tab.
-                    UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
-                    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);
-
-                    // Open DeskClock positioned on the stopwatch tab.
-                    UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
-                    startActivity(new Intent(this, DeskClock.class)
-                            .setAction(StopwatchService.ACTION_START_STOPWATCH));
-                    setResult(RESULT_OK);
-                    break;
-                default:
-                    throw new IllegalArgumentException("Unsupported action: " + action);
-            }
-        } catch (Exception e) {
-            LOGGER.e("Error handling intent: " + intent, e);
-            setResult(RESULT_CANCELED);
-        } finally {
-            finish();
-        }
-    }
-}
diff --git a/src/com/android/deskclock/HandleShortcuts.kt b/src/com/android/deskclock/HandleShortcuts.kt
new file mode 100644
index 0000000..11f2cee
--- /dev/null
+++ b/src/com/android/deskclock/HandleShortcuts.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Activity
+import android.content.Intent
+import android.os.Bundle
+
+import com.android.deskclock.events.Events
+import com.android.deskclock.stopwatch.StopwatchService
+import com.android.deskclock.uidata.UiDataModel
+
+class HandleShortcuts : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val intent = intent
+
+        try {
+            when (val action = intent.action) {
+                StopwatchService.ACTION_PAUSE_STOPWATCH -> {
+                    Events.sendStopwatchEvent(R.string.action_pause, R.string.label_shortcut)
+
+                    // Open DeskClock positioned on the stopwatch tab.
+                    UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.STOPWATCH
+                    startActivity(Intent(this, DeskClock::class.java)
+                            .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH))
+                    setResult(RESULT_OK)
+                }
+                StopwatchService.ACTION_START_STOPWATCH -> {
+                    Events.sendStopwatchEvent(R.string.action_start, R.string.label_shortcut)
+
+                    // Open DeskClock positioned on the stopwatch tab.
+                    UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.STOPWATCH
+                    startActivity(Intent(this, DeskClock::class.java)
+                            .setAction(StopwatchService.ACTION_START_STOPWATCH))
+                    setResult(RESULT_OK)
+                }
+                else -> throw IllegalArgumentException("Unsupported action: $action")
+            }
+        } catch (e: Exception) {
+            LOGGER.e("Error handling intent: $intent", e)
+            setResult(RESULT_CANCELED)
+        } finally {
+            finish()
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("HandleShortcuts")
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAdapter.java b/src/com/android/deskclock/ItemAdapter.java
deleted file mode 100644
index a417636..0000000
--- a/src/com/android/deskclock/ItemAdapter.java
+++ /dev/null
@@ -1,544 +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.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import android.util.SparseArray;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static androidx.recyclerview.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.
- */
-public class ItemAdapter<T extends ItemAdapter.ItemHolder>
-        extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
-
-    /**
-     * 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
-        public void onItemChanged(ItemHolder<?> itemHolder) {
-            if (mOnItemChangedListener != null) {
-                mOnItemChangedListener.onItemChanged(itemHolder);
-            }
-            final int position = mItemHolders.indexOf(itemHolder);
-            if (position != RecyclerView.NO_POSITION) {
-                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);
-            }
-        }
-    };
-
-    /**
-     * Invokes the {@link OnItemClickedListener} in {@link #mListenersByViewType} corresponding
-     * to {@link ItemViewHolder#getItemViewType()}
-     */
-    private final OnItemClickedListener mOnItemClickedListener = new OnItemClickedListener() {
-        @Override
-        public void onItemClicked(ItemViewHolder<?> viewHolder, int id) {
-            final OnItemClickedListener listener =
-                    mListenersByViewType.get(viewHolder.getItemViewType());
-            if (listener != null) {
-                listener.onItemClicked(viewHolder, id);
-            }
-        }
-    };
-
-    /**
-     * Invoked when any item changes.
-     */
-    private OnItemChangedListener mOnItemChangedListener;
-
-    /**
-     * Factories for creating new {@link ItemViewHolder} entities.
-     */
-    private final SparseArray<ItemViewHolder.Factory> mFactoriesByViewType = new SparseArray<>();
-
-    /**
-     * Listeners to invoke in {@link #mOnItemClickedListener}.
-     */
-    private final SparseArray<OnItemClickedListener> mListenersByViewType = new SparseArray<>();
-
-    /**
-     * List of current item holders represented by this adapter.
-     */
-    private List<T> mItemHolders;
-
-    /**
-     * Convenience for calling {@link #setHasStableIds(boolean)} with {@code true}.
-     *
-     * @return this object, allowing calls to methods in this class to be chained
-     */
-    public ItemAdapter setHasStableIds() {
-        setHasStableIds(true);
-        return this;
-    }
-
-    /**
-     * Sets the {@link ItemViewHolder.Factory} and {@link OnItemClickedListener} used to create
-     * new item view holders in {@link #onCreateViewHolder(ViewGroup, int)}.
-     *
-     * @param factory   the {@link ItemViewHolder.Factory} used to create new item view holders
-     * @param listener  the {@link OnItemClickedListener} to be invoked by
-     *                  {@link #mItemChangedNotifier}
-     * @param viewTypes the unique identifier for the view types to be created
-     * @return this object, allowing calls to methods in this class to be chained
-     */
-    public ItemAdapter withViewTypes(ItemViewHolder.Factory factory,
-            OnItemClickedListener listener, int... viewTypes) {
-        for (int viewType : viewTypes) {
-            mFactoriesByViewType.put(viewType, factory);
-            mListenersByViewType.put(viewType, listener);
-        }
-        return this;
-    }
-
-    /**
-     * @return the current list of item holders represented by this adapter
-     */
-    public final List<T> getItems() {
-        return mItemHolders;
-    }
-
-    /**
-     * Sets the list of item holders to serve as the dataset for this adapter and invokes
-     * {@link #notifyDataSetChanged()} to update the UI.
-     * <p/>
-     * If {@link #hasStableIds()} returns {@code true}, then the instance state will preserved
-     * between new and old holders that have matching {@link ItemHolder#itemId} values.
-     *
-     * @param itemHolders the new list of item holders
-     * @return this object, allowing calls to methods in this class to be chained
-     */
-    public ItemAdapter setItems(List<T> itemHolders) {
-        final List<T> oldItemHolders = mItemHolders;
-        if (oldItemHolders != itemHolders) {
-            if (oldItemHolders != null) {
-                // remove the item change listener from the old item holders
-                for (T oldItemHolder : oldItemHolders) {
-                    oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier);
-                }
-            }
-
-            if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
-                // transfer instance state from old to new item holders based on item id,
-                // we use a simple O(N^2) implementation since we assume the number of items is
-                // relatively small and generating a temporary map would be more expensive
-                final Bundle bundle = new Bundle();
-                for (ItemHolder newItemHolder : itemHolders) {
-                    for (ItemHolder oldItemHolder : oldItemHolders) {
-                        if (newItemHolder.itemId == oldItemHolder.itemId
-                                && newItemHolder != oldItemHolder) {
-                            // clear any existing state from the bundle
-                            bundle.clear();
-
-                            // transfer instance state from old to new item holder
-                            oldItemHolder.onSaveInstanceState(bundle);
-                            newItemHolder.onRestoreInstanceState(bundle);
-
-                            break;
-                        }
-                    }
-                }
-            }
-
-            if (itemHolders != null) {
-                // add the item change listener to the new item holders
-                for (ItemHolder newItemHolder : itemHolders) {
-                    newItemHolder.addOnItemChangedListener(mItemChangedNotifier);
-                }
-            }
-
-            // finally update the current list of item holders and inform the RV to update the UI
-            mItemHolders = itemHolders;
-            notifyDataSetChanged();
-        }
-
-        return this;
-    }
-
-    /**
-     * 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) {
-        mOnItemChangedListener = listener;
-    }
-
-    @Override
-    public int getItemCount() {
-        return mItemHolders == null ? 0 : mItemHolders.size();
-    }
-
-    @Override
-    public long getItemId(int position) {
-        return hasStableIds() ? mItemHolders.get(position).itemId : NO_ID;
-    }
-
-    public T findItemById(long id) {
-        for (T holder : mItemHolders) {
-            if (holder.itemId == id) {
-                return holder;
-            }
-        }
-        return null;
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        return mItemHolders.get(position).getItemViewType();
-    }
-
-    @Override
-    public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        final ItemViewHolder.Factory factory = mFactoriesByViewType.get(viewType);
-        if (factory != null) {
-            return factory.createViewHolder(parent, viewType);
-        }
-        throw new IllegalArgumentException("Unsupported view type: " + viewType);
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public void onBindViewHolder(ItemViewHolder viewHolder, int position) {
-        // suppress any unchecked warnings since it is up to the subclass to guarantee
-        // compatibility of their view holders with the item holder at the corresponding position
-        viewHolder.bindItemView(mItemHolders.get(position));
-        viewHolder.setOnItemClickedListener(mOnItemClickedListener);
-    }
-
-    @Override
-    public void onViewRecycled(ItemViewHolder viewHolder) {
-        viewHolder.setOnItemClickedListener(null);
-        viewHolder.recycleItemView();
-    }
-
-    /**
-     * Base class for wrapping an item for compatibility with an {@link ItemHolder}.
-     * <p/>
-     * An {@link ItemHolder} serves as bridge between the model and view layer; subclassers should
-     * implement properties that fall beyond the scope of their model layer but are necessary for
-     * the view layer. Properties that should be persisted across dataset changes can be
-     * preserved via the {@link #onSaveInstanceState(Bundle)} and
-     * {@link #onRestoreInstanceState(Bundle)} methods.
-     * <p/>
-     * Note: An {@link ItemHolder} can be used by multiple {@link ItemHolder} and any state changes
-     * should simultaneously be reflected in both UIs.  It is not thread-safe however and should
-     * only be used on a single thread at a given time.
-     *
-     * @param <T> the item type wrapped by the holder
-     */
-    public static abstract class ItemHolder<T> {
-
-        /**
-         * The item held by this holder.
-         */
-        public final T item;
-
-        /**
-         * Globally unique id corresponding to the item.
-         */
-        public final long itemId;
-
-        /**
-         * Listeners to be invoked by {@link #notifyItemChanged()}.
-         */
-        private final List<OnItemChangedListener> mOnItemChangedListeners = new ArrayList<>();
-
-        /**
-         * Designated constructor.
-         *
-         * @param item   the {@link T} item to be held by this holder
-         * @param itemId the globally unique id corresponding to the item
-         */
-        public ItemHolder(T item, long itemId) {
-            this.item = item;
-            this.itemId = itemId;
-        }
-
-        /**
-         * @return the unique identifier for the view that should be used to represent the item,
-         * e.g. the layout resource id.
-         */
-        public abstract int getItemViewType();
-
-        /**
-         * Adds the listener to the current list of registered listeners if it is not already
-         * registered.
-         *
-         * @param listener the listener to add
-         */
-        public final void addOnItemChangedListener(OnItemChangedListener listener) {
-            if (!mOnItemChangedListeners.contains(listener)) {
-                mOnItemChangedListeners.add(listener);
-            }
-        }
-
-        /**
-         * Removes the listener from the current list of registered listeners.
-         *
-         * @param listener the listener to remove
-         */
-        public final void removeOnItemChangedListener(OnItemChangedListener listener) {
-            mOnItemChangedListeners.remove(listener);
-        }
-
-        /**
-         * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder)} for all listeners added
-         * via {@link #addOnItemChangedListener(OnItemChangedListener)}.
-         */
-        public final void notifyItemChanged() {
-            for (OnItemChangedListener listener : mOnItemChangedListeners) {
-                listener.onItemChanged(this);
-            }
-        }
-
-        /**
-         * 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/>
-         * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
-         * reused for other items in the {@link ItemHolder}.
-         *
-         * @param bundle the {@link Bundle} in which to place saved state
-         */
-        public void onSaveInstanceState(Bundle bundle) {
-            // for subclassers
-        }
-
-        /**
-         * Called to restore any per-instance state which was previously saved in
-         * {@link #onSaveInstanceState(Bundle)} for an item with a matching {@link #itemId}.
-         * <p/>
-         * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
-         * reused for other items in the {@link ItemHolder}.
-         *
-         * @param bundle the {@link Bundle} in which to retrieve saved state
-         */
-        public void onRestoreInstanceState(Bundle bundle) {
-            // for subclassers
-        }
-    }
-
-    /**
-     * Base class for a reusable {@link RecyclerView.ViewHolder} compatible with an
-     * {@link ItemViewHolder}. Provides an interface for binding to an {@link ItemHolder} and later
-     * being recycled.
-     */
-    public static class ItemViewHolder<T extends ItemHolder> extends RecyclerView.ViewHolder {
-
-        /**
-         * The current {@link ItemHolder} bound to this holder.
-         */
-        private T mItemHolder;
-
-        /**
-         * The current {@link OnItemClickedListener} associated with this holder.
-         */
-        private OnItemClickedListener mOnItemClickedListener;
-
-        /**
-         * Designated constructor.
-         *
-         * @param itemView the item {@link View} to associate with this holder
-         */
-        public ItemViewHolder(View itemView) {
-            super(itemView);
-        }
-
-        /**
-         * @return the current {@link ItemHolder} bound to this holder, or {@code null} if unbound
-         */
-        public final T getItemHolder() {
-            return mItemHolder;
-        }
-
-        /**
-         * Binds the holder's {@link #itemView} to a particular item.
-         *
-         * @param itemHolder the {@link ItemHolder} to bind
-         */
-        public final void bindItemView(T itemHolder) {
-            mItemHolder = itemHolder;
-            onBindItemView(itemHolder);
-        }
-
-        /**
-         * Called when a new item is bound to the holder. Subclassers should override to bind any
-         * relevant data to their {@link #itemView} in this method.
-         *
-         * @param itemHolder the {@link ItemHolder} to bind
-         */
-        protected void onBindItemView(T itemHolder) {
-            // for subclassers
-        }
-
-        /**
-         * Recycles the current item view, unbinding the current item holder and state.
-         */
-        public final void recycleItemView() {
-            mItemHolder = null;
-            mOnItemClickedListener = null;
-
-            onRecycleItemView();
-        }
-
-        /**
-         * Called when the current item view is recycled. Subclassers should override to release
-         * any bound item state and prepare their {@link #itemView} for reuse.
-         */
-        protected void onRecycleItemView() {
-            // for subclassers
-        }
-
-        /**
-         * Sets the current {@link OnItemClickedListener} to be invoked via
-         * {@link #notifyItemClicked}.
-         *
-         * @param listener the new {@link OnItemClickedListener}, or {@code null} to clear
-         */
-        public final void setOnItemClickedListener(OnItemClickedListener listener) {
-            mOnItemClickedListener = listener;
-        }
-
-        /**
-         * Called by subclasses to invoke the current {@link OnItemClickedListener} for a
-         * particular click event so it can be handled at a higher level.
-         *
-         * @param id the unique identifier for the click action that has occurred
-         */
-        public final void notifyItemClicked(int id) {
-            if (mOnItemClickedListener != null) {
-                mOnItemClickedListener.onItemClicked(this, id);
-            }
-        }
-
-        /**
-         * Factory interface used by {@link ItemAdapter} for creating new {@link ItemViewHolder}.
-         */
-        public interface Factory {
-            /**
-             * Used by {@link ItemAdapter#createViewHolder(ViewGroup, int)} to make new
-             * {@link ItemViewHolder} for a given view type.
-             *
-             * @param parent   the {@code ViewGroup} that the {@link ItemViewHolder#itemView} will
-             *                 be attached
-             * @param viewType the unique id of the item view to create
-             * @return a new initialized {@link ItemViewHolder}
-             */
-            public ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType);
-        }
-    }
-
-    /**
-     * Callback interface for when an item changes and should be re-bound.
-     */
-    public interface OnItemChangedListener {
-        /**
-         * Invoked by {@link ItemHolder#notifyItemChanged()}.
-         *
-         * @param itemHolder the item holder that has changed
-         */
-        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);
-    }
-
-    /**
-     * Callback interface for handling when an item is clicked.
-     */
-    public interface OnItemClickedListener {
-        /**
-         * Invoked by {@link ItemViewHolder#notifyItemClicked(int)}
-         *
-         * @param viewHolder the {@link ItemViewHolder} containing the view that was clicked
-         * @param id         the unique identifier for the click action that has occurred
-         */
-        void onItemClicked(ItemViewHolder<?> viewHolder, int id);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAdapter.kt b/src/com/android/deskclock/ItemAdapter.kt
new file mode 100644
index 0000000..8d9bd4c
--- /dev/null
+++ b/src/com/android/deskclock/ItemAdapter.kt
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.Bundle
+import android.util.SparseArray
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+
+import kotlin.math.min
+
+/**
+ * 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.
+ */
+class ItemAdapter<T : ItemHolder<*>> : RecyclerView.Adapter<ItemViewHolder<T>>() {
+    /**
+     * Finds the position of the changed item holder and invokes [.notifyItemChanged] or
+     * [.notifyItemChanged] if payloads are present (in order to do in-place
+     * change animations).
+     */
+    private val mItemChangedNotifier: OnItemChangedListener = object : OnItemChangedListener {
+        override fun onItemChanged(itemHolder: ItemHolder<*>) {
+            mOnItemChangedListener?.onItemChanged(itemHolder)
+            val position = items!!.indexOf(itemHolder)
+            if (position != RecyclerView.NO_POSITION) {
+                notifyItemChanged(position)
+            }
+        }
+
+        override fun onItemChanged(itemHolder: ItemHolder<*>, payload: Any) {
+            mOnItemChangedListener?.onItemChanged(itemHolder, payload)
+            val position = items!!.indexOf(itemHolder)
+            if (position != RecyclerView.NO_POSITION) {
+                notifyItemChanged(position, payload)
+            }
+        }
+    }
+
+    /**
+     * Invokes the [OnItemClickedListener] in [.mListenersByViewType] corresponding
+     * to [ItemViewHolder.getItemViewType]
+     */
+    private val mOnItemClickedListener: OnItemClickedListener = object : OnItemClickedListener {
+        override fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int) {
+            val listener = mListenersByViewType[viewHolder.getItemViewType()]
+            listener?.onItemClicked(viewHolder, id)
+        }
+    }
+
+    /**
+     * Invoked when any item changes.
+     */
+    private var mOnItemChangedListener: OnItemChangedListener? = null
+
+    /**
+     * Factories for creating new [ItemViewHolder] entities.
+     */
+    private val mFactoriesByViewType: SparseArray<ItemViewHolder.Factory> = SparseArray()
+
+    /**
+     * Listeners to invoke in [.mOnItemClickedListener].
+     */
+    private val mListenersByViewType: SparseArray<OnItemClickedListener?> = SparseArray()
+
+    /**
+     * List of current item holders represented by this adapter.
+     */
+    @JvmField var items: MutableList<T>? = null
+
+    /**
+     * Convenience for calling [.setHasStableIds] with `true`.
+     *
+     * @return this object, allowing calls to methods in this class to be chained
+     */
+    fun setHasStableIds(): ItemAdapter<T> {
+        setHasStableIds(true)
+        return this
+    }
+
+    /**
+     * Sets the [ItemViewHolder.Factory] and [OnItemClickedListener] used to create
+     * new item view holders in [.onCreateViewHolder].
+     *
+     * @param factory the [ItemViewHolder.Factory] used to create new item view holders
+     * @param listener the [OnItemClickedListener] to be invoked by [.mItemChangedNotifier]
+     * @param viewTypes the unique identifier for the view types to be created
+     * @return this object, allowing calls to methods in this class to be chained
+     */
+    fun withViewTypes(
+        factory: ItemViewHolder.Factory,
+        listener: OnItemClickedListener?,
+        vararg viewTypes: Int
+    ): ItemAdapter<T> {
+        for (viewType in viewTypes) {
+            mFactoriesByViewType.put(viewType, factory)
+            mListenersByViewType.put(viewType, listener)
+        }
+        return this
+    }
+
+    /**
+     * Sets the list of item holders to serve as the dataset for this adapter and invokes
+     * [.notifyDataSetChanged] to update the UI.
+     *
+     * If [.hasStableIds] returns `true`, then the instance state will preserved
+     * between new and old holders that have matching [itemId] values.
+     *
+     * @param itemHolders the new list of item holders
+     * @return this object, allowing calls to methods in this class to be chained
+     */
+    fun setItems(itemHolders: List<T>?): ItemAdapter<T> {
+        val oldItemHolders = items
+        if (oldItemHolders !== itemHolders) {
+            if (oldItemHolders != null) {
+                // remove the item change listener from the old item holders
+                for (oldItemHolder in oldItemHolders) {
+                    oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
+                }
+            }
+
+            if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
+                // transfer instance state from old to new item holders based on item id,
+                // we use a simple O(N^2) implementation since we assume the number of items is
+                // relatively small and generating a temporary map would be more expensive
+                val bundle = Bundle()
+                for (newItemHolder in itemHolders) {
+                    for (oldItemHolder in oldItemHolders) {
+                        if (newItemHolder.itemId == oldItemHolder.itemId &&
+                                newItemHolder !== oldItemHolder) {
+                            // clear any existing state from the bundle
+                            bundle.clear()
+
+                            // transfer instance state from old to new item holder
+                            oldItemHolder.onSaveInstanceState(bundle)
+                            newItemHolder.onRestoreInstanceState(bundle)
+                            break
+                        }
+                    }
+                }
+            }
+
+            if (itemHolders != null) {
+                // add the item change listener to the new item holders
+                for (newItemHolder in itemHolders) {
+                    newItemHolder.addOnItemChangedListener(mItemChangedNotifier)
+                }
+            }
+
+            // finally update the current list of item holders and inform the RV to update the UI
+            items = itemHolders?.toMutableList()
+            notifyDataSetChanged()
+        }
+
+        return this
+    }
+
+    /**
+     * Inserts the specified item holder at the specified position. Invokes
+     * [.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
+     */
+    fun addItem(position: Int, itemHolder: T): ItemAdapter<T> {
+        var variablePosition = position
+        itemHolder.addOnItemChangedListener(mItemChangedNotifier)
+        variablePosition = min(variablePosition, items!!.size)
+        items!!.add(variablePosition, itemHolder)
+        notifyItemInserted(variablePosition)
+        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
+     * [.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
+     */
+    fun removeItem(itemHolder: T): ItemAdapter<T> {
+        var variableItemHolder = itemHolder
+        val index = items!!.indexOf(variableItemHolder)
+        if (index >= 0) {
+            variableItemHolder = items!!.removeAt(index)
+            variableItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
+            notifyItemRemoved(index)
+        }
+        return this
+    }
+
+    /**
+     * Sets the listener to be invoked whenever any item changes.
+     */
+    fun setOnItemChangedListener(listener: OnItemChangedListener) {
+        mOnItemChangedListener = listener
+    }
+
+    override fun getItemCount(): Int = items?.size ?: 0
+
+    override fun getItemId(position: Int): Long {
+        return if (hasStableIds()) items!![position].itemId else NO_ID
+    }
+
+    fun findItemById(id: Long): T? {
+        for (holder in items!!) {
+            if (holder.itemId == id) {
+                return holder
+            }
+        }
+        return null
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        return items!![position].getItemViewType()
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<T> {
+        val factory = mFactoriesByViewType[viewType]
+        if (factory != null) {
+            return factory.createViewHolder(parent, viewType) as ItemViewHolder<T>
+        }
+        throw IllegalArgumentException("Unsupported view type: $viewType")
+    }
+
+    override fun onBindViewHolder(viewHolder: ItemViewHolder<T>, position: Int) {
+        // suppress any unchecked warnings since it is up to the subclass to guarantee
+        // compatibility of their view holders with the item holder at the corresponding position
+        viewHolder.bindItemView(items!![position])
+        viewHolder.setOnItemClickedListener(mOnItemClickedListener)
+    }
+
+    override fun onViewRecycled(viewHolder: ItemViewHolder<T>) {
+        viewHolder.setOnItemClickedListener(null)
+        viewHolder.recycleItemView()
+    }
+
+    /**
+     * Base class for wrapping an item for compatibility with an [ItemHolder].
+     *
+     * An [ItemHolder] serves as bridge between the model and view layer; subclassers should
+     * implement properties that fall beyond the scope of their model layer but are necessary for
+     * the view layer. Properties that should be persisted across dataset changes can be
+     * preserved via the [.onSaveInstanceState] and
+     * [.onRestoreInstanceState] methods.
+     *
+     * Note: An [ItemHolder] can be used by multiple [ItemHolder] and any state changes
+     * should simultaneously be reflected in both UIs.  It is not thread-safe however and should
+     * only be used on a single thread at a given time.
+     *
+     * @param <T> the item type wrapped by the holder
+    </T> */
+    abstract class ItemHolder<T>(
+        /** The item held by this holder. */
+        val item: T,
+        /** Globally unique id corresponding to the item. */
+        val itemId: Long
+    ) {
+        /** Listeners to be invoked by [.notifyItemChanged]. */
+        private val mOnItemChangedListeners: MutableList<OnItemChangedListener> = ArrayList()
+
+        /**
+         * @return the unique identifier for the view that should be used to represent the item,
+         * e.g. the layout resource id.
+         */
+        abstract fun getItemViewType(): Int
+
+        /**
+         * Adds the listener to the current list of registered listeners if it is not already
+         * registered.
+         *
+         * @param listener the listener to add
+         */
+        fun addOnItemChangedListener(listener: OnItemChangedListener) {
+            if (!mOnItemChangedListeners.contains(listener)) {
+                mOnItemChangedListeners.add(listener)
+            }
+        }
+
+        /**
+         * Removes the listener from the current list of registered listeners.
+         *
+         * @param listener the listener to remove
+         */
+        fun removeOnItemChangedListener(listener: OnItemChangedListener) {
+            mOnItemChangedListeners.remove(listener)
+        }
+
+        /**
+         * Invokes [OnItemChangedListener.onItemChanged] for all listeners added
+         * via [.addOnItemChangedListener].
+         */
+        fun notifyItemChanged() {
+            for (listener in mOnItemChangedListeners) {
+                listener.onItemChanged(this)
+            }
+        }
+
+        /**
+         * Invokes [OnItemChangedListener.onItemChanged] for all
+         * listeners added via [.addOnItemChangedListener].
+         */
+        fun notifyItemChanged(payload: Any) {
+            for (listener in 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 [.onRestoreInstanceState].
+         *
+         * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
+         * reused for other items in the [ItemHolder].
+         *
+         * @param bundle the [Bundle] in which to place saved state
+         */
+        open fun onSaveInstanceState(bundle: Bundle) {
+            // for subclassers
+        }
+
+        /**
+         * Called to restore any per-instance state which was previously saved in
+         * [.onSaveInstanceState] for an item with a matching [.itemId].
+         *
+         * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
+         * reused for other items in the [ItemHolder].
+         *
+         * @param bundle the [Bundle] in which to retrieve saved state
+         */
+        open fun onRestoreInstanceState(bundle: Bundle) {
+            // for subclassers
+        }
+    }
+
+    /**
+     * Base class for a reusable [RecyclerView.ViewHolder] compatible with an
+     * [ItemViewHolder]. Provides an interface for binding to an [ItemHolder] and later
+     * being recycled.
+     */
+    open class ItemViewHolder<T : ItemHolder<*>>(itemView: View)
+        : RecyclerView.ViewHolder(itemView) {
+        /**
+         * The current [ItemHolder] bound to this holder, or `null` if unbound.
+         */
+        var itemHolder: T? = null
+            private set
+
+        /**
+         * The current [OnItemClickedListener] associated with this holder.
+         */
+        private var mOnItemClickedListener: OnItemClickedListener? = null
+
+        /**
+         * Binds the holder's [.itemView] to a particular item.
+         *
+         * @param itemHolder the [ItemHolder] to bind
+         */
+        fun bindItemView(itemHolder: T) {
+            this.itemHolder = itemHolder
+            onBindItemView(itemHolder)
+        }
+
+        /**
+         * Called when a new item is bound to the holder. Subclassers should override to bind any
+         * relevant data to their [.itemView] in this method.
+         *
+         * @param itemHolder the [ItemHolder] to bind
+         */
+        protected open fun onBindItemView(itemHolder: T) {
+            // for subclassers
+        }
+
+        /**
+         * Recycles the current item view, unbinding the current item holder and state.
+         */
+        fun recycleItemView() {
+            itemHolder = null
+            mOnItemClickedListener = null
+
+            onRecycleItemView()
+        }
+
+        /**
+         * Called when the current item view is recycled. Subclassers should override to release
+         * any bound item state and prepare their [.itemView] for reuse.
+         */
+        protected fun onRecycleItemView() {
+            // for subclassers
+        }
+
+        /**
+         * Sets the current [OnItemClickedListener] to be invoked via
+         * [.notifyItemClicked].
+         *
+         * @param listener the new [OnItemClickedListener], or `null` to clear
+         */
+        fun setOnItemClickedListener(listener: OnItemClickedListener?) {
+            mOnItemClickedListener = listener
+        }
+
+        /**
+         * Called by subclasses to invoke the current [OnItemClickedListener] for a
+         * particular click event so it can be handled at a higher level.
+         *
+         * @param id the unique identifier for the click action that has occurred
+         */
+        fun notifyItemClicked(id: Int) {
+            mOnItemClickedListener?.onItemClicked(this, id)
+        }
+
+        /**
+         * Factory interface used by [ItemAdapter] for creating new [ItemViewHolder].
+         */
+        interface Factory {
+            /**
+             * Used by [ItemAdapter.createViewHolder] to make new
+             * [ItemViewHolder] for a given view type.
+             *
+             * @param parent the `ViewGroup` that the [ItemViewHolder.itemView] will be attached
+             * @param viewType the unique id of the item view to create
+             * @return a new initialized [ItemViewHolder]
+             */
+            fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*>
+        }
+    }
+
+    /**
+     * Callback interface for when an item changes and should be re-bound.
+     */
+    interface OnItemChangedListener {
+        /**
+         * Invoked by [ItemHolder.notifyItemChanged].
+         *
+         * @param itemHolder the item holder that has changed
+         */
+        fun onItemChanged(itemHolder: ItemHolder<*>)
+
+        /**
+         * Invoked by [ItemHolder.notifyItemChanged].
+         *
+         * @param itemHolder the item holder that has changed
+         * @param payload the payload object
+         */
+        fun onItemChanged(itemHolder: ItemHolder<*>, payload: Any)
+    }
+
+    /**
+     * Callback interface for handling when an item is clicked.
+     */
+    interface OnItemClickedListener {
+        /**
+         * Invoked by [ItemViewHolder.notifyItemClicked]
+         *
+         * @param viewHolder the [ItemViewHolder] containing the view that was clicked
+         * @param id the unique identifier for the click action that has occurred
+         */
+        fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAnimator.java b/src/com/android/deskclock/ItemAnimator.java
deleted file mode 100644
index 756d4e9..0000000
--- a/src/com/android/deskclock/ItemAnimator.java
+++ /dev/null
@@ -1,371 +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.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import androidx.annotation.NonNull;
-import androidx.collection.ArrayMap;
-import androidx.recyclerview.widget.RecyclerView.State;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-import androidx.recyclerview.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/ItemAnimator.kt b/src/com/android/deskclock/ItemAnimator.kt
new file mode 100644
index 0000000..727204f
--- /dev/null
+++ b/src/com/android/deskclock/ItemAnimator.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.View
+import androidx.collection.ArrayMap
+import androidx.recyclerview.widget.RecyclerView.State
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.recyclerview.widget.RecyclerView.ItemAnimator
+import androidx.recyclerview.widget.SimpleItemAnimator
+
+class ItemAnimator : SimpleItemAnimator() {
+    private val mAddAnimatorsList: MutableList<Animator> = ArrayList()
+    private val mRemoveAnimatorsList: MutableList<Animator> = ArrayList()
+    private val mChangeAnimatorsList: MutableList<Animator> = ArrayList()
+    private val mMoveAnimatorsList: MutableList<Animator> = ArrayList()
+
+    private val mAnimators: MutableMap<ViewHolder, Animator> = ArrayMap()
+
+    override fun animateRemove(holder: ViewHolder): Boolean {
+        endAnimation(holder)
+
+        val prevAlpha: Float = holder.itemView.getAlpha()
+
+        val removeAnimator: Animator? = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 0f)
+        removeAnimator!!.duration = getRemoveDuration()
+        removeAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationStart(animator: Animator) {
+                dispatchRemoveStarting(holder)
+            }
+
+            override fun onAnimationEnd(animator: Animator) {
+                animator.removeAllListeners()
+                mAnimators.remove(holder)
+                holder.itemView.setAlpha(prevAlpha)
+                dispatchRemoveFinished(holder)
+            }
+        })
+        mRemoveAnimatorsList.add(removeAnimator)
+        mAnimators[holder] = removeAnimator
+        return true
+    }
+
+    override fun animateAdd(holder: ViewHolder): Boolean {
+        endAnimation(holder)
+
+        val prevAlpha: Float = holder.itemView.getAlpha()
+        holder.itemView.setAlpha(0f)
+
+        val addAnimator: Animator = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 1f)
+                .setDuration(getAddDuration())
+        addAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationStart(animator: Animator) {
+                dispatchAddStarting(holder)
+            }
+
+            override fun onAnimationEnd(animator: Animator) {
+                animator.removeAllListeners()
+                mAnimators.remove(holder)
+                holder.itemView.setAlpha(prevAlpha)
+                dispatchAddFinished(holder)
+            }
+        })
+        mAddAnimatorsList.add(addAnimator)
+        mAnimators[holder] = addAnimator
+        return true
+    }
+
+    override fun animateMove(
+        holder: ViewHolder,
+        fromX: Int,
+        fromY: Int,
+        toX: Int,
+        toY: Int
+    ): Boolean {
+        endAnimation(holder)
+
+        val deltaX = toX - fromX
+        val deltaY = toY - fromY
+        val moveDuration: Long = getMoveDuration()
+
+        if (deltaX == 0 && deltaY == 0) {
+            dispatchMoveFinished(holder)
+            return false
+        }
+
+        val view: View = holder.itemView
+        val prevTranslationX = view.translationX
+        val prevTranslationY = view.translationY
+        view.translationX = -deltaX.toFloat()
+        view.translationY = -deltaY.toFloat()
+
+        val moveAnimator: ObjectAnimator?
+        if (deltaX != 0 && deltaY != 0) {
+            val moveX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0f)
+            val moveY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f)
+            moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX, moveY)
+        } else if (deltaX != 0) {
+            val moveX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0f)
+            moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX)
+        } else {
+            val moveY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f)
+            moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveY)
+        }
+
+        moveAnimator?.duration = moveDuration
+        moveAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+        moveAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationStart(animator: Animator) {
+                dispatchMoveStarting(holder)
+            }
+
+            override fun onAnimationEnd(animator: Animator) {
+                animator?.removeAllListeners()
+                mAnimators.remove(holder)
+                view.translationX = prevTranslationX
+                view.translationY = prevTranslationY
+                dispatchMoveFinished(holder)
+            }
+        })
+        mMoveAnimatorsList.add(moveAnimator)
+        mAnimators[holder] = moveAnimator
+
+        return true
+    }
+
+    override fun animateChange(
+        oldHolder: ViewHolder,
+        newHolder: ViewHolder,
+        preInfo: ItemHolderInfo,
+        postInfo: ItemHolderInfo
+    ): Boolean {
+        endAnimation(oldHolder)
+        endAnimation(newHolder)
+
+        val changeDuration: Long = getChangeDuration()
+        val payloads = if (preInfo is PayloadItemHolderInfo) preInfo.payloads else null
+
+        if (oldHolder === newHolder) {
+            val animator = (newHolder as OnAnimateChangeListener)
+                    .onAnimateChange(payloads, preInfo.left, preInfo.top, preInfo.right,
+                            preInfo.bottom, changeDuration)
+            if (animator == null) {
+                dispatchChangeFinished(newHolder, false)
+                return false
+            }
+            animator.addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    dispatchChangeStarting(newHolder, false)
+                }
+
+                override fun onAnimationEnd(animator: Animator) {
+                    animator.removeAllListeners()
+                    mAnimators.remove(newHolder)
+                    dispatchChangeFinished(newHolder, false)
+                }
+            })
+            mChangeAnimatorsList.add(animator)
+            mAnimators[newHolder] = animator
+            return true
+        } else if (oldHolder !is OnAnimateChangeListener ||
+                newHolder !is OnAnimateChangeListener) {
+            // Both holders must implement OnAnimateChangeListener in order to animate.
+            dispatchChangeFinished(oldHolder, true)
+            dispatchChangeFinished(newHolder, true)
+            return false
+        }
+
+        val oldChangeAnimator = (oldHolder as OnAnimateChangeListener)
+                .onAnimateChange(oldHolder, newHolder, changeDuration)
+        if (oldChangeAnimator != null) {
+            oldChangeAnimator.addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    dispatchChangeStarting(oldHolder, true)
+                }
+
+                override fun onAnimationEnd(animator: Animator) {
+                    animator.removeAllListeners()
+                    mAnimators.remove(oldHolder)
+                    dispatchChangeFinished(oldHolder, true)
+                }
+            })
+            mAnimators[oldHolder] = oldChangeAnimator
+            mChangeAnimatorsList.add(oldChangeAnimator)
+        } else {
+            dispatchChangeFinished(oldHolder, true)
+        }
+
+        val newChangeAnimator = (newHolder as OnAnimateChangeListener)
+                .onAnimateChange(oldHolder, newHolder, changeDuration)
+        if (newChangeAnimator != null) {
+            newChangeAnimator.addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animator: Animator) {
+                    dispatchChangeStarting(newHolder, false)
+                }
+
+                override fun onAnimationEnd(animator: Animator) {
+                    animator.removeAllListeners()
+                    mAnimators.remove(newHolder)
+                    dispatchChangeFinished(newHolder, false)
+                }
+            })
+            mAnimators[newHolder] = newChangeAnimator
+            mChangeAnimatorsList.add(newChangeAnimator)
+        } else {
+            dispatchChangeFinished(newHolder, false)
+        }
+
+        return true
+    }
+
+    override fun animateChange(
+        oldHolder: ViewHolder,
+        newHolder: ViewHolder,
+        fromLeft: Int,
+        fromTop: Int,
+        toLeft: Int,
+        toTop: Int
+    ): Boolean {
+        /* Unused */
+        throw IllegalStateException("This method should not be used")
+    }
+
+    override fun runPendingAnimations() {
+        val removeAnimatorSet = AnimatorSet()
+        removeAnimatorSet.playTogether(mRemoveAnimatorsList)
+        mRemoveAnimatorsList.clear()
+
+        val addAnimatorSet = AnimatorSet()
+        addAnimatorSet.playTogether(mAddAnimatorsList)
+        mAddAnimatorsList.clear()
+
+        val changeAnimatorSet = AnimatorSet()
+        changeAnimatorSet.playTogether(mChangeAnimatorsList)
+        mChangeAnimatorsList.clear()
+
+        val moveAnimatorSet = AnimatorSet()
+        moveAnimatorSet.playTogether(mMoveAnimatorsList)
+        mMoveAnimatorsList.clear()
+
+        val pendingAnimatorSet = AnimatorSet()
+        pendingAnimatorSet.addListener(object : AnimatorListenerAdapter() {
+            override fun 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 fun endAnimation(holder: ViewHolder) {
+        val animator = mAnimators[holder]
+
+        mAnimators.remove(holder)
+        mAddAnimatorsList.remove(animator)
+        mRemoveAnimatorsList.remove(animator)
+        mChangeAnimatorsList.remove(animator)
+        mMoveAnimatorsList.remove(animator)
+
+        animator?.end()
+        dispatchFinishedWhenDone()
+    }
+
+    override fun endAnimations() {
+        val animatorList: MutableList<Animator?> = ArrayList(mAnimators.values)
+        for (animator in animatorList) {
+            animator?.end()
+        }
+        dispatchFinishedWhenDone()
+    }
+
+    override fun isRunning(): Boolean = mAnimators.isNotEmpty()
+
+    private fun dispatchFinishedWhenDone() {
+        if (!isRunning()) {
+            dispatchAnimationsFinished()
+        }
+    }
+
+    override fun recordPreLayoutInformation(
+        state: State,
+        viewHolder: ViewHolder,
+        @AdapterChanges changeFlags: Int,
+        payloads: MutableList<Any>
+    ): ItemAnimator.ItemHolderInfo {
+        val itemHolderInfo: ItemHolderInfo =
+                super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
+        if (itemHolderInfo is PayloadItemHolderInfo) {
+            itemHolderInfo.payloads = payloads
+        }
+        return itemHolderInfo
+    }
+
+    override fun obtainHolderInfo(): ItemAnimator.ItemHolderInfo {
+        return PayloadItemHolderInfo()
+    }
+
+    override fun canReuseUpdatedViewHolder(
+        viewHolder: ViewHolder,
+        payloads: MutableList<Any?>
+    ): Boolean {
+        val defaultReusePolicy: Boolean = super.canReuseUpdatedViewHolder(viewHolder, payloads)
+        // Whenever we have a payload, this is an in-place animation.
+        return payloads.isNotEmpty() || defaultReusePolicy
+    }
+
+    private class PayloadItemHolderInfo : ItemHolderInfo() {
+        private val mPayloads: MutableList<Any> = ArrayList()
+
+        var payloads: MutableList<Any>
+            get() = mPayloads
+            set(payloads) {
+                mPayloads.clear()
+                mPayloads.addAll(payloads)
+            }
+    }
+
+    interface OnAnimateChangeListener {
+        fun onAnimateChange(oldHolder: ViewHolder, newHolder: ViewHolder, duration: Long): Animator?
+
+        fun onAnimateChange(
+            payloads: List<Any>?,
+            fromLeft: Int,
+            fromTop: Int,
+            fromRight: Int,
+            fromBottom: Int,
+            duration: Long
+        ): Animator?
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/LabelDialogFragment.java b/src/com/android/deskclock/LabelDialogFragment.java
deleted file mode 100644
index 0fa0eab..0000000
--- a/src/com/android/deskclock/LabelDialogFragment.java
+++ /dev/null
@@ -1,243 +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;
-
-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 androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.AppCompatEditText;
-import android.text.Editable;
-import android.text.InputType;
-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;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.provider.Alarm;
-
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
-
-/**
- * DialogFragment to edit label.
- */
-public class LabelDialogFragment extends DialogFragment {
-
-    /**
-     * 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;
-    private int mTimerId;
-    private String mTag;
-
-    public static LabelDialogFragment newInstance(Alarm alarm, String label, String tag) {
-        final Bundle args = new Bundle();
-        args.putString(ARG_LABEL, label);
-        args.putParcelable(ARG_ALARM, alarm);
-        args.putString(ARG_TAG, tag);
-
-        final LabelDialogFragment frag = new LabelDialogFragment();
-        frag.setArguments(args);
-        return frag;
-    }
-
-    public static LabelDialogFragment newInstance(Timer timer) {
-        final Bundle args = new Bundle();
-        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(ARG_LABEL, mLabelBox.getText().toString());
-        }
-    }
-
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        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);
-
-        String label = args.getString(ARG_LABEL);
-        if (savedInstanceState != null) {
-            label = savedInstanceState.getString(ARG_LABEL, label);
-        }
-
-        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());
-        mLabelBox.setSingleLine();
-        mLabelBox.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
-        mLabelBox.setText(label);
-        mLabelBox.selectAll();
-
-        // 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);
-
-        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);
-    }
-
-    /**
-     * Sets the new label into the timer or alarm.
-     */
-    private void setLabel() {
-        String label = mLabelBox.getText().toString();
-        if (label.trim().isEmpty()) {
-            // Don't allow user to input label with only whitespace.
-            label = "";
-        }
-
-        if (mAlarm != null) {
-            ((AlarmLabelDialogHandler) getActivity()).onDialogLabelSet(mAlarm, label, mTag);
-        } else if (mTimerId >= 0) {
-            final Timer timer = DataModel.getDataModel().getTimer(mTimerId);
-            if (timer != null) {
-                DataModel.getDataModel().setTimerLabel(timer, label);
-            }
-        }
-    }
-
-    public interface AlarmLabelDialogHandler {
-        void onDialogLabelSet(Alarm alarm, String label, String tag);
-    }
-
-    /**
-     * Alters the UI to indicate when input is valid or invalid.
-     */
-    private class TextChangeListener implements TextWatcher {
-        @Override
-        public void onTextChanged(CharSequence s, int start, int before, int count) {
-            mLabelBox.setActivated(!TextUtils.isEmpty(s));
-        }
-
-        @Override
-        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-        }
-
-        @Override
-        public void afterTextChanged(Editable editable) {
-        }
-    }
-
-    /**
-     * Handles completing the label edit from the IME keyboard.
-     */
-    private class ImeDoneListener implements TextView.OnEditorActionListener {
-        @Override
-        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-            if (actionId == EditorInfo.IME_ACTION_DONE) {
-                setLabel();
-                dismissAllowingStateLoss();
-                return true;
-            }
-            return false;
-        }
-    }
-
-    /**
-     * Handles completing the label edit from the Ok button of the dialog.
-     */
-    private class OkListener implements DialogInterface.OnClickListener {
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            setLabel();
-            dismiss();
-        }
-    }
-}
diff --git a/src/com/android/deskclock/LabelDialogFragment.kt b/src/com/android/deskclock/LabelDialogFragment.kt
new file mode 100644
index 0000000..77f0b7b
--- /dev/null
+++ b/src/com/android/deskclock/LabelDialogFragment.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.res.ColorStateList
+import android.os.Bundle
+import android.text.Editable
+import android.text.InputType
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.view.KeyEvent
+import android.view.Window
+import android.view.WindowManager
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import android.widget.TextView.OnEditorActionListener
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.AppCompatEditText
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.provider.Alarm
+
+/**
+ * DialogFragment to edit label.
+ */
+class LabelDialogFragment : DialogFragment() {
+    private var mLabelBox: AppCompatEditText? = null
+    private var mAlarm: Alarm? = null
+    private var mTimerId = 0
+    private var mTag: String? = null
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        // As long as the label box exists, save its state.
+        mLabelBox?. let {
+            outState.putString(ARG_LABEL, it.getText().toString())
+        }
+    }
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val args = arguments ?: Bundle.EMPTY
+        mAlarm = args.getParcelable(ARG_ALARM)
+        mTimerId = args.getInt(ARG_TIMER_ID, -1)
+        mTag = args.getString(ARG_TAG)
+
+        var label = args.getString(ARG_LABEL)
+        savedInstanceState?.let {
+            label = it.getString(ARG_LABEL, label)
+        }
+
+        val dialog: AlertDialog = AlertDialog.Builder(requireActivity())
+                .setPositiveButton(android.R.string.ok, OkListener())
+                .setNegativeButton(android.R.string.cancel, null)
+                .setMessage(R.string.label)
+                .create()
+        val context: Context = dialog.context
+
+        val colorControlActivated = ThemeUtils.resolveColor(context, R.attr.colorControlActivated)
+        val colorControlNormal = ThemeUtils.resolveColor(context, R.attr.colorControlNormal)
+
+        mLabelBox = AppCompatEditText(context)
+        mLabelBox?.setSupportBackgroundTintList(ColorStateList(
+                arrayOf(intArrayOf(android.R.attr.state_activated), intArrayOf()),
+                intArrayOf(colorControlActivated, colorControlNormal)))
+        mLabelBox?.setOnEditorActionListener(ImeDoneListener())
+        mLabelBox?.addTextChangedListener(TextChangeListener())
+        mLabelBox?.setSingleLine()
+        mLabelBox?.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
+        mLabelBox?.setText(label)
+        mLabelBox?.selectAll()
+
+        // The line at the bottom of EditText is part of its background therefore the padding
+        // must be added to its container.
+        val padding = context.resources
+                .getDimensionPixelSize(R.dimen.label_edittext_padding)
+        dialog.setView(mLabelBox, padding, 0, padding, 0)
+
+        val alertDialogWindow: Window? = dialog.window
+        alertDialogWindow?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
+        return dialog
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+
+        // Stop callbacks from the IME since there is no view to process them.
+        mLabelBox?.setOnEditorActionListener(null)
+    }
+
+    /**
+     * Sets the new label into the timer or alarm.
+     */
+    private fun setLabel() {
+        var label: String = mLabelBox!!.getText().toString()
+        if (label.trim { it <= ' ' }.isEmpty()) {
+            // Don't allow user to input label with only whitespace.
+            label = ""
+        }
+
+        if (mAlarm != null) {
+            (activity as AlarmLabelDialogHandler).onDialogLabelSet(mAlarm!!, label, mTag!!)
+        } else if (mTimerId >= 0) {
+            val timer: Timer? = DataModel.dataModel.getTimer(mTimerId)
+            if (timer != null) {
+                DataModel.dataModel.setTimerLabel(timer, label)
+            }
+        }
+    }
+
+    interface AlarmLabelDialogHandler {
+        fun onDialogLabelSet(alarm: Alarm, label: String, tag: String)
+    }
+
+    /**
+     * Alters the UI to indicate when input is valid or invalid.
+     */
+    private inner class TextChangeListener : TextWatcher {
+        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+            mLabelBox?.setActivated(!TextUtils.isEmpty(s))
+        }
+
+        override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+        }
+
+        override fun afterTextChanged(editable: Editable) {
+        }
+    }
+
+    /**
+     * Handles completing the label edit from the IME keyboard.
+     */
+    private inner class ImeDoneListener : OnEditorActionListener {
+        override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {
+            if (actionId == EditorInfo.IME_ACTION_DONE) {
+                setLabel()
+                dismissAllowingStateLoss()
+                return true
+            }
+            return false
+        }
+    }
+
+    /**
+     * Handles completing the label edit from the Ok button of the dialog.
+     */
+    private inner class OkListener : DialogInterface.OnClickListener {
+        override fun onClick(dialog: DialogInterface, which: Int) {
+            setLabel()
+            dismiss()
+        }
+    }
+
+    companion object {
+        /**
+         * The tag that identifies instances of LabelDialogFragment in the fragment manager.
+         */
+        private const val TAG = "label_dialog"
+
+        private const val ARG_LABEL = "arg_label"
+        private const val ARG_ALARM = "arg_alarm"
+        private const val ARG_TIMER_ID = "arg_timer_id"
+        private const val ARG_TAG = "arg_tag"
+
+        fun newInstance(alarm: Alarm, label: String?, tag: String?): LabelDialogFragment {
+            val args = Bundle()
+            args.putString(ARG_LABEL, label)
+            args.putParcelable(ARG_ALARM, alarm)
+            args.putString(ARG_TAG, tag)
+
+            val frag = LabelDialogFragment()
+            frag.arguments = args
+            return frag
+        }
+
+        @JvmStatic
+        fun newInstance(timer: Timer): LabelDialogFragment {
+            val args = Bundle()
+            args.putString(ARG_LABEL, timer.label)
+            args.putInt(ARG_TIMER_ID, timer.id)
+
+            val frag = LabelDialogFragment()
+            frag.arguments = args
+            return frag
+        }
+
+        /**
+         * Replaces any existing LabelDialogFragment with the given `fragment`.
+         */
+        @JvmStatic
+        fun show(manager: FragmentManager?, fragment: LabelDialogFragment) {
+            if (manager == null || manager.isDestroyed) {
+                return
+            }
+
+            // Finish any outstanding fragment work.
+            manager.executePendingTransactions()
+
+            val tx = manager.beginTransaction()
+
+            // Remove existing instance of LabelDialogFragment if necessary.
+            val existing = manager.findFragmentByTag(TAG)
+            existing?.let {
+                tx.remove(it)
+            }
+            tx.addToBackStack(null)
+
+            fragment.show(tx, TAG)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/LogUtils.java b/src/com/android/deskclock/LogUtils.java
deleted file mode 100644
index c679ac2..0000000
--- a/src/com/android/deskclock/LogUtils.java
+++ /dev/null
@@ -1,137 +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.os.Build;
-import android.util.Log;
-
-public class LogUtils {
-
-    /**
-     * Default logger used for generic logging, i.eTAG. when a specific log tag isn't specified.
-     */
-    private final static Logger DEFAULT_LOGGER = new Logger("AlarmClock");
-
-    public static void v(String message, Object... args) {
-        DEFAULT_LOGGER.v(message, args);
-    }
-
-    public static void d(String message, Object... args) {
-        DEFAULT_LOGGER.d(message, args);
-    }
-
-    public static void i(String message, Object... args) {
-        DEFAULT_LOGGER.i(message, args);
-    }
-
-    public static void w(String message, Object... args) {
-        DEFAULT_LOGGER.w(message, args);
-    }
-
-    public static void e(String message, Object... args) {
-        DEFAULT_LOGGER.e(message, args);
-    }
-
-    public static void e(String message, Throwable e) {
-        DEFAULT_LOGGER.e(message, e);
-    }
-
-    public static void wtf(String message, Object... args) {
-        DEFAULT_LOGGER.wtf(message, args);
-    }
-
-    public static void wtf(Throwable e) {
-        DEFAULT_LOGGER.wtf(e);
-    }
-
-    public final static class Logger {
-
-        /**
-         * Log everything for debug builds or if running on a dev device.
-         */
-        public final static boolean DEBUG = BuildConfig.DEBUG
-                || "eng".equals(Build.TYPE)
-                || "userdebug".equals(Build.TYPE);
-
-        public final String logTag;
-
-        public Logger(String logTag) {
-            this.logTag = logTag;
-        }
-
-        public boolean isVerboseLoggable() { return DEBUG || Log.isLoggable(logTag, Log.VERBOSE); }
-        public boolean isDebugLoggable() { return DEBUG || Log.isLoggable(logTag, Log.DEBUG); }
-        public boolean isInfoLoggable() { return DEBUG || Log.isLoggable(logTag, Log.INFO); }
-        public boolean isWarnLoggable() { return DEBUG || Log.isLoggable(logTag, Log.WARN); }
-        public boolean isErrorLoggable() { return DEBUG || Log.isLoggable(logTag, Log.ERROR); }
-        public boolean isWtfLoggable() { return DEBUG || Log.isLoggable(logTag, Log.ASSERT); }
-
-        public void v(String message, Object... args) {
-            if (isVerboseLoggable()) {
-                Log.v(logTag, args == null || args.length == 0
-                        ? message : String.format(message, args));
-            }
-        }
-
-        public void d(String message, Object... args) {
-            if (isDebugLoggable()) {
-                Log.d(logTag, args == null || args.length == 0 ? message
-                        : String.format(message, args));
-            }
-        }
-
-        public void i(String message, Object... args) {
-            if (isInfoLoggable()) {
-                Log.i(logTag, args == null || args.length == 0 ? message
-                        : String.format(message, args));
-            }
-        }
-
-        public void w(String message, Object... args) {
-            if (isWarnLoggable()) {
-                Log.w(logTag, args == null || args.length == 0 ? message
-                        : String.format(message, args));
-            }
-        }
-
-        public void e(String message, Object... args) {
-            if (isErrorLoggable()) {
-                Log.e(logTag, args == null || args.length == 0 ? message
-                        : String.format(message, args));
-            }
-        }
-
-        public void e(String message, Throwable e) {
-            if (isErrorLoggable()) {
-                Log.e(logTag, message, e);
-            }
-        }
-
-        public void wtf(String message, Object... args) {
-            if (isWtfLoggable()) {
-                Log.wtf(logTag, args == null || args.length == 0 ? message
-                        : String.format(message, args));
-            }
-        }
-
-        public void wtf(Throwable e) {
-            if (isWtfLoggable()) {
-                Log.wtf(logTag, e);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/LogUtils.kt b/src/com/android/deskclock/LogUtils.kt
new file mode 100644
index 0000000..0713559
--- /dev/null
+++ b/src/com/android/deskclock/LogUtils.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.Build
+import android.util.Log
+
+object LogUtils {
+    /** Default logger used for generic logging, i.e TAG. when a specific log tag isn't specified.*/
+    private val DEFAULT_LOGGER = Logger("AlarmClock")
+
+    @JvmStatic
+    fun v(message: String, vararg args: Any?) {
+        DEFAULT_LOGGER.v(message, *args)
+    }
+
+    @JvmStatic
+    fun d(message: String, vararg args: Any?) {
+        DEFAULT_LOGGER.d(message, *args)
+    }
+
+    @JvmStatic
+    fun i(message: String, vararg args: Any?) {
+        DEFAULT_LOGGER.i(message, *args)
+    }
+
+    @JvmStatic
+    fun w(message: String, vararg args: Any?) {
+        DEFAULT_LOGGER.w(message, *args)
+    }
+
+    fun e(message: String, vararg args: Any?) {
+        DEFAULT_LOGGER.e(message, *args)
+    }
+
+    @JvmStatic
+    fun e(message: String, e: Throwable) {
+        DEFAULT_LOGGER.e(message, e)
+    }
+
+    fun wtf(message: String, vararg args: Any?) {
+        DEFAULT_LOGGER.wtf(message, *args)
+    }
+
+    @JvmStatic
+    fun wtf(e: Throwable) {
+        DEFAULT_LOGGER.wtf(e)
+    }
+
+    class Logger(val logTag: String) {
+        val isVerboseLoggable: Boolean
+            get() = DEBUG || Log.isLoggable(logTag, Log.VERBOSE)
+
+        val isDebugLoggable: Boolean
+            get() = DEBUG || Log.isLoggable(logTag, Log.DEBUG)
+
+        val isInfoLoggable: Boolean
+            get() = DEBUG || Log.isLoggable(logTag, Log.INFO)
+
+        val isWarnLoggable: Boolean
+            get() = DEBUG || Log.isLoggable(logTag, Log.WARN)
+
+        val isErrorLoggable: Boolean
+            get() = DEBUG || Log.isLoggable(logTag, Log.ERROR)
+
+        val isWtfLoggable: Boolean
+            get() = DEBUG || Log.isLoggable(logTag, Log.ASSERT)
+
+        fun v(message: String, vararg args: Any?) {
+            if (isVerboseLoggable) {
+                Log.v(logTag, if (args.isEmpty() || args[0] == null) {
+                    message
+                } else {
+                    String.format(message, *args)
+                })
+            }
+        }
+
+        fun d(message: String, vararg args: Any?) {
+            if (isDebugLoggable) {
+                Log.d(logTag, if (args.isEmpty() || args[0] == null) {
+                    message
+                } else {
+                    String.format(message, *args)
+                })
+            }
+        }
+
+        fun i(message: String, vararg args: Any?) {
+            if (isInfoLoggable) {
+                Log.i(logTag, if (args.isEmpty() || args[0] == null) {
+                    message
+                } else {
+                    String.format(message, *args)
+                })
+            }
+        }
+
+        fun w(message: String, vararg args: Any?) {
+            if (isWarnLoggable) {
+                Log.w(logTag, if (args.isEmpty() || args[0] == null) {
+                    message
+                } else {
+                    String.format(message, *args)
+                })
+            }
+        }
+
+        fun e(message: String, vararg args: Any?) {
+            if (isErrorLoggable) {
+                Log.e(logTag, if (args.isEmpty() || args[0] == null) {
+                    message
+                } else {
+                    String.format(message, *args)
+                })
+            }
+        }
+
+        fun e(message: String, e: Throwable) {
+            if (isErrorLoggable) {
+                Log.e(logTag, message, e)
+            }
+        }
+
+        fun wtf(message: String, vararg args: Any?) {
+            if (isWtfLoggable) {
+                Log.wtf(logTag, if (args.isEmpty() || args[0] == null) {
+                    message
+                } else {
+                    String.format(message, *args)
+                })
+            }
+        }
+
+        fun wtf(e: Throwable) {
+            if (isWtfLoggable) {
+                Log.wtf(logTag, e)
+            }
+        }
+
+        companion object {
+            /** Log everything for debug builds or if running on a dev device. */
+            val DEBUG = (BuildConfig.DEBUG || "eng" == Build.TYPE || "userdebug" == Build.TYPE)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/MoveScreensaverRunnable.java b/src/com/android/deskclock/MoveScreensaverRunnable.java
deleted file mode 100644
index 73d2527..0000000
--- a/src/com/android/deskclock/MoveScreensaverRunnable.java
+++ /dev/null
@@ -1,157 +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.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.view.View;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.AnimatorUtils.getAlphaAnimator;
-import static com.android.deskclock.AnimatorUtils.getScaleAnimator;
-
-/**
- * This runnable chooses a random initial position for {@link #mSaverView} within
- * {@link #mContentView} if {@link #mSaverView} is transparent. It also schedules itself to run
- * each minute, at which time {@link #mSaverView} is faded out, set to a new random location, and
- * faded in.
- */
-public final class MoveScreensaverRunnable implements Runnable {
-
-    /** The duration over which the fade in/out animations occur. */
-    private static final long FADE_TIME = 3000L;
-
-    /** Accelerate the hide animation. */
-    private final Interpolator mAcceleration = new AccelerateInterpolator();
-
-    /** Decelerate the show animation. */
-    private final Interpolator mDeceleration = new DecelerateInterpolator();
-
-    /** The container that houses {@link #mSaverView}. */
-    private final View mContentView;
-
-    /** The display within the {@link #mContentView} that is randomly positioned. */
-    private final View mSaverView;
-
-    /** Tracks the currently executing animation if any; used to gracefully stop the animation. */
-    private Animator mActiveAnimator;
-
-    /**
-     * @param contentView contains the {@code saverView}
-     * @param saverView a child view of {@code contentView} that periodically moves around
-     */
-    public MoveScreensaverRunnable(View contentView, View saverView) {
-        mContentView = contentView;
-        mSaverView = saverView;
-    }
-
-    /**
-     * Start or restart the random movement of the saver view within the content view.
-     */
-    public void start() {
-        // Stop any existing animations or callbacks.
-        stop();
-
-        // Reset the alpha to 0 so saver view will be randomly positioned within the new bounds.
-        mSaverView.setAlpha(0);
-
-        // Execute the position updater runnable to choose the first random position of saver view.
-        run();
-
-        // Schedule callbacks every minute to adjust the position of mSaverView.
-        UiDataModel.getUiDataModel().addMinuteCallback(this, -FADE_TIME);
-    }
-
-    /**
-     * Stop the random movement of the saver view within the content view.
-     */
-    public void stop() {
-        UiDataModel.getUiDataModel().removePeriodicCallback(this);
-
-        // End any animation currently running.
-        if (mActiveAnimator != null) {
-            mActiveAnimator.end();
-            mActiveAnimator = null;
-        }
-    }
-
-    @Override
-    public void run() {
-        Utils.enforceMainLooper();
-
-        final boolean selectInitialPosition = mSaverView.getAlpha() == 0f;
-        if (selectInitialPosition) {
-            // When selecting an initial position for the saver view the width and height of
-            // mContentView are untrustworthy if this was caused by a configuration change. To
-            // combat this, we position the mSaverView randomly within the smallest box that is
-            // guaranteed to work.
-            final int smallestDim = Math.min(mContentView.getWidth(), mContentView.getHeight());
-            final float newX = getRandomPoint(smallestDim - mSaverView.getWidth());
-            final float newY = getRandomPoint(smallestDim - mSaverView.getHeight());
-
-            mSaverView.setX(newX);
-            mSaverView.setY(newY);
-            mActiveAnimator = getAlphaAnimator(mSaverView, 0f, 1f);
-            mActiveAnimator.setDuration(FADE_TIME);
-            mActiveAnimator.setInterpolator(mDeceleration);
-            mActiveAnimator.start();
-        } else {
-            // Select a new random position anywhere in mContentView that will fit mSaverView.
-            final float newX = getRandomPoint(mContentView.getWidth() - mSaverView.getWidth());
-            final float newY = getRandomPoint(mContentView.getHeight() - mSaverView.getHeight());
-
-            // Fade out and shrink the saver view.
-            final AnimatorSet hide = new AnimatorSet();
-            hide.setDuration(FADE_TIME);
-            hide.setInterpolator(mAcceleration);
-            hide.play(getAlphaAnimator(mSaverView, 1f, 0f))
-                    .with(getScaleAnimator(mSaverView, 1f, 0.85f));
-
-            // Fade in and grow the saver view after altering its position.
-            final AnimatorSet show = new AnimatorSet();
-            show.setDuration(FADE_TIME);
-            show.setInterpolator(mDeceleration);
-            show.play(getAlphaAnimator(mSaverView, 0f, 1f))
-                    .with(getScaleAnimator(mSaverView, 0.85f, 1f));
-            show.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationStart(Animator animation) {
-                    mSaverView.setX(newX);
-                    mSaverView.setY(newY);
-                }
-            });
-
-            // Execute hide followed by show.
-            final AnimatorSet all = new AnimatorSet();
-            all.play(show).after(hide);
-            mActiveAnimator = all;
-            mActiveAnimator.start();
-        }
-    }
-
-    /**
-     * @return a random integer between 0 and the {@code maximum} exclusive.
-     */
-    private static float getRandomPoint(float maximum) {
-        return (int) (Math.random() * maximum);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/MoveScreensaverRunnable.kt b/src/com/android/deskclock/MoveScreensaverRunnable.kt
new file mode 100644
index 0000000..ece7c7e
--- /dev/null
+++ b/src/com/android/deskclock/MoveScreensaverRunnable.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.View
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+
+import com.android.deskclock.uidata.UiDataModel
+
+import kotlin.math.min
+
+/**
+ * This runnable chooses a random initial position for [.mSaverView] within
+ * [.mContentView] if [.mSaverView] is transparent. It also schedules itself to run
+ * each minute, at which time [.mSaverView] is faded out, set to a new random location, and
+ * faded in.
+ */
+class MoveScreensaverRunnable(
+    /** The container that houses [.mSaverView].  */
+    private val mContentView: View,
+    /** The display within the [.mContentView] that is randomly positioned.  */
+    private val mSaverView: View
+) : Runnable {
+    /** Accelerate the hide animation.  */
+    private val mAcceleration: Interpolator = AccelerateInterpolator()
+
+    /** Decelerate the show animation.  */
+    private val mDeceleration: Interpolator = DecelerateInterpolator()
+
+    /** Tracks the currently executing animation if any; used to gracefully stop the animation.  */
+    private var mActiveAnimator: Animator? = null
+
+    /** Start or restart the random movement of the saver view within the content view. */
+    fun start() {
+        // Stop any existing animations or callbacks.
+        stop()
+
+        // Reset the alpha to 0 so saver view will be randomly positioned within the new bounds.
+        mSaverView.alpha = 0f
+
+        // Execute the position updater runnable to choose the first random position of saver view.
+        run()
+
+        // Schedule callbacks every minute to adjust the position of mSaverView.
+        UiDataModel.uiDataModel.addMinuteCallback(this, -FADE_TIME)
+    }
+
+    /** Stop the random movement of the saver view within the content view. */
+    fun stop() {
+        UiDataModel.uiDataModel.removePeriodicCallback(this)
+
+        // End any animation currently running.
+        if (mActiveAnimator != null) {
+            mActiveAnimator?.end()
+            mActiveAnimator = null
+        }
+    }
+
+    override fun run() {
+        Utils.enforceMainLooper()
+
+        val selectInitialPosition = mSaverView.alpha == 0f
+        if (selectInitialPosition) {
+            // When selecting an initial position for the saver view the width and height of
+            // mContentView are untrustworthy if this was caused by a configuration change. To
+            // combat this, we position the mSaverView randomly within the smallest box that is
+            // guaranteed to work.
+            val smallestDim = min(mContentView.width, mContentView.height)
+            val newX = getRandomPoint(smallestDim - mSaverView.width.toFloat())
+            val newY = getRandomPoint(smallestDim - mSaverView.height.toFloat())
+
+            mSaverView.x = newX
+            mSaverView.y = newY
+            mActiveAnimator = AnimatorUtils.getAlphaAnimator(mSaverView, 0f, 1f)
+            mActiveAnimator?.duration = FADE_TIME
+            mActiveAnimator?.interpolator = mDeceleration
+            mActiveAnimator?.start()
+        } else {
+            // Select a new random position anywhere in mContentView that will fit mSaverView.
+            val newX = getRandomPoint(mContentView.width - mSaverView.width.toFloat())
+            val newY = getRandomPoint(mContentView.height - mSaverView.height.toFloat())
+
+            // Fade out and shrink the saver view.
+            val hide = AnimatorSet()
+            hide.duration = FADE_TIME
+            hide.interpolator = mAcceleration
+            hide.play(AnimatorUtils.getAlphaAnimator(mSaverView, 1f, 0f))
+                    .with(AnimatorUtils.getScaleAnimator(mSaverView, 1f, 0.85f))
+
+            // Fade in and grow the saver view after altering its position.
+            val show = AnimatorSet()
+            show.duration = FADE_TIME
+            show.interpolator = mDeceleration
+            show.play(AnimatorUtils.getAlphaAnimator(mSaverView, 0f, 1f))
+                    .with(AnimatorUtils.getScaleAnimator(mSaverView, 0.85f, 1f))
+            show.addListener(object : AnimatorListenerAdapter() {
+                override fun onAnimationStart(animation: Animator) {
+                    mSaverView.x = newX
+                    mSaverView.y = newY
+                }
+            })
+
+            // Execute hide followed by show.
+            val all = AnimatorSet()
+            all.play(show).after(hide)
+            mActiveAnimator = all
+            mActiveAnimator?.start()
+        }
+    }
+
+    companion object {
+        /** The duration over which the fade in/out animations occur.  */
+        private const val FADE_TIME = 3000L
+
+        /**
+         * @return a random integer between 0 and the `maximum` exclusive.
+         */
+        private fun getRandomPoint(maximum: Float): Float {
+            return (Math.random() * maximum).toFloat()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/NotificationUtils.kt b/src/com/android/deskclock/NotificationUtils.kt
new file mode 100644
index 0000000..0cb2d55
--- /dev/null
+++ b/src/com/android/deskclock/NotificationUtils.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2020 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.NotificationChannel
+import android.content.Context
+import android.util.ArraySet
+import android.util.Log
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
+import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
+
+object NotificationUtils {
+    private val TAG = NotificationUtils::class.java.simpleName
+
+    /**
+     * Notification channel containing all missed alarm notifications.
+     */
+    const val ALARM_MISSED_NOTIFICATION_CHANNEL_ID = "alarmMissedNotification"
+
+    /**
+     * Notification channel containing all upcoming alarm notifications.
+     */
+    const val ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID = "alarmUpcomingNotification"
+
+    /**
+     * Notification channel containing all snooze notifications.
+     */
+    const val ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID = "alarmSnoozingNotification"
+
+    /**
+     * Notification channel containing all firing alarm and timer notifications.
+     */
+    const val FIRING_NOTIFICATION_CHANNEL_ID = "firingAlarmsAndTimersNotification"
+
+    /**
+     * Notification channel containing all TimerModel notifications.
+     */
+    const val TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "timerNotification"
+
+    /**
+     * Notification channel containing all stopwatch notifications.
+     */
+    const val STOPWATCH_NOTIFICATION_CHANNEL_ID = "stopwatchNotification"
+
+    /**
+     * Values used to bitmask certain channel defaults
+     */
+    private const val PLAY_SOUND = 0x01
+    private const val ENABLE_LIGHTS = 0x02
+    private const val ENABLE_VIBRATION = 0x04
+
+    private val CHANNEL_PROPS: MutableMap<String, IntArray> = HashMap()
+
+    init {
+        CHANNEL_PROPS[ALARM_MISSED_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+                R.string.alarm_missed_channel,
+                IMPORTANCE_HIGH
+        )
+        CHANNEL_PROPS[ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+                R.string.alarm_snooze_channel,
+                IMPORTANCE_LOW
+        )
+        CHANNEL_PROPS[ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+                R.string.alarm_upcoming_channel,
+                IMPORTANCE_LOW
+        )
+        CHANNEL_PROPS[FIRING_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+                R.string.firing_alarms_timers_channel,
+                IMPORTANCE_HIGH,
+                ENABLE_LIGHTS
+        )
+        CHANNEL_PROPS[STOPWATCH_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+                R.string.stopwatch_channel,
+                IMPORTANCE_LOW
+        )
+        CHANNEL_PROPS[TIMER_MODEL_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+                R.string.timer_channel,
+                IMPORTANCE_LOW
+        )
+    }
+
+    @JvmStatic
+    fun createChannel(context: Context, id: String) {
+        if (!Utils.isOOrLater) {
+            return
+        }
+
+        if (!CHANNEL_PROPS.containsKey(id)) {
+            Log.e(TAG, "Invalid channel requested: $id")
+            return
+        }
+
+        val properties = CHANNEL_PROPS[id]!!
+        val nameId = properties[0]
+        val importance = properties[1]
+        val channel = NotificationChannel(id, context.getString(nameId), importance)
+        if (properties.size >= 3) {
+            val bits = properties[2]
+            channel.enableLights(bits and ENABLE_LIGHTS != 0)
+            channel.enableVibration(bits and ENABLE_VIBRATION != 0)
+            if (bits and PLAY_SOUND == 0) {
+                channel.setSound(null, null)
+            }
+        }
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        nm.createNotificationChannel(channel)
+    }
+
+    private fun deleteChannel(nm: NotificationManagerCompat, channelId: String) {
+        val channel: NotificationChannel? = nm.getNotificationChannel(channelId)
+        if (channel != null) {
+            nm.deleteNotificationChannel(channelId)
+        }
+    }
+
+    private fun getAllExistingChannelIds(nm: NotificationManagerCompat): Set<String> {
+        val result: MutableSet<String> = ArraySet()
+        for (channel in nm.getNotificationChannels()) {
+            result.add(channel.id)
+        }
+        return result
+    }
+
+    @JvmStatic
+    fun updateNotificationChannels(context: Context) {
+        if (!Utils.isOOrLater) {
+            return
+        }
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+
+        // These channels got a new behavior so we need to recreate them with new ids
+        deleteChannel(nm, "alarmLowPriorityNotification")
+        deleteChannel(nm, "alarmHighPriorityNotification")
+        deleteChannel(nm, "StopwatchNotification")
+        deleteChannel(nm, "alarmNotification")
+        deleteChannel(nm, "TimerModelNotification")
+        deleteChannel(nm, "alarmSnoozeNotification")
+
+        // We recreate all existing channels so any language change or our name changes propagate
+        // to the actual channels
+        val existingChannelIds = getAllExistingChannelIds(nm)
+        for (id in existingChannelIds) {
+            createChannel(context, id)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Predicate.java b/src/com/android/deskclock/Predicate.java
deleted file mode 100644
index 9d9cebc..0000000
--- a/src/com/android/deskclock/Predicate.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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/Predicate.kt b/src/com/android/deskclock/Predicate.kt
new file mode 100644
index 0000000..758b57b
--- /dev/null
+++ b/src/com/android/deskclock/Predicate.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 `RegexPredicate` might implement
+ * `Predicate<String>`, and return true for any String that matches its
+ * given regular expression.
+ *
+ * Implementors of Predicate which may cause side effects upon evaluation are
+ * strongly encouraged to state this fact clearly in their API documentation.
+ */
+interface Predicate<T> {
+    fun apply(t: T): Boolean
+
+    companion object {
+        /**
+         * An implementation of the predicate interface that always returns true.
+         */
+        @JvmField
+        val TRUE: Predicate<*> = object : Predicate<Any> {
+            override fun apply(t: Any): Boolean = true
+        }
+
+        /**
+         * An implementation of the predicate interface that always returns false.
+         */
+        @JvmField
+        val FALSE: Predicate<*> = object : Predicate<Any> {
+            override fun apply(t: Any): Boolean = false
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/RingtonePreviewKlaxon.java b/src/com/android/deskclock/RingtonePreviewKlaxon.java
deleted file mode 100644
index eea5fe3..0000000
--- a/src/com/android/deskclock/RingtonePreviewKlaxon.java
+++ /dev/null
@@ -1,47 +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.content.Context;
-import android.net.Uri;
-
-public final class RingtonePreviewKlaxon {
-
-    private static AsyncRingtonePlayer sAsyncRingtonePlayer;
-
-    private RingtonePreviewKlaxon() {
-    }
-
-    public static void stop(Context context) {
-        LogUtils.i("RingtonePreviewKlaxon.stop()");
-        getAsyncRingtonePlayer(context).stop();
-    }
-
-    public static void start(Context context, Uri uri) {
-        stop(context);
-        LogUtils.i("RingtonePreviewKlaxon.start()");
-        getAsyncRingtonePlayer(context).play(uri, 0);
-    }
-
-    private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
-        if (sAsyncRingtonePlayer == null) {
-            sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
-        }
-
-        return sAsyncRingtonePlayer;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/RingtonePreviewKlaxon.kt b/src/com/android/deskclock/RingtonePreviewKlaxon.kt
new file mode 100644
index 0000000..a7d406c
--- /dev/null
+++ b/src/com/android/deskclock/RingtonePreviewKlaxon.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.net.Uri
+
+import com.android.deskclock.LogUtils.i
+
+object RingtonePreviewKlaxon {
+    private var sAsyncRingtonePlayer: AsyncRingtonePlayer? = null
+
+    @JvmStatic
+    fun stop(context: Context) {
+        i("RingtonePreviewKlaxon.stop()")
+        getAsyncRingtonePlayer(context).stop()
+    }
+
+    @JvmStatic
+    fun start(context: Context, uri: Uri) {
+        stop(context)
+        i("RingtonePreviewKlaxon.start()")
+        getAsyncRingtonePlayer(context).play(uri, 0)
+    }
+
+    @Synchronized
+    private fun getAsyncRingtonePlayer(context: Context): AsyncRingtonePlayer {
+        if (sAsyncRingtonePlayer == null) {
+            sAsyncRingtonePlayer = AsyncRingtonePlayer(context.applicationContext)
+        }
+        return sAsyncRingtonePlayer!!
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Screensaver.java b/src/com/android/deskclock/Screensaver.java
deleted file mode 100644
index 427885e..0000000
--- a/src/com/android/deskclock/Screensaver.java
+++ /dev/null
@@ -1,212 +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.app.AlarmManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-import android.service.dreams.DreamService;
-import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
-import android.widget.TextClock;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.uidata.UiDataModel;
-
-public final class Screensaver extends DreamService {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Screensaver");
-
-    private final OnPreDrawListener mStartPositionUpdater = new StartPositionUpdater();
-    private MoveScreensaverRunnable mPositionUpdater;
-
-    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.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() {
-        @Override
-        public void run() {
-            Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
-        }
-    };
-
-    /**
-     * Receiver to alarm clock changes.
-     */
-    private final BroadcastReceiver mAlarmChangedReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            Utils.refreshAlarm(Screensaver.this, mContentView);
-        }
-    };
-
-    @Override
-    public void onCreate() {
-        LOGGER.v("Screensaver created");
-
-        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);
-    }
-
-    @Override
-    public void onAttachedToWindow() {
-        LOGGER.v("Screensaver attached to window");
-        super.onAttachedToWindow();
-
-        setContentView(R.layout.desk_clock_saver);
-
-        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.setClockIconTypeface(mContentView);
-        Utils.setTimeFormat(mDigitalClock, false);
-        mAnalogClock.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);
-
-        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.
-        if (Utils.isLOrLater()) {
-            registerReceiver(mAlarmChangedReceiver,
-                    new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED));
-        }
-
-        if (mSettingsContentObserver != null) {
-            @SuppressWarnings("deprecation")
-            final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
-            getContentResolver().registerContentObserver(uri, false, mSettingsContentObserver);
-        }
-
-        Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
-        Utils.refreshAlarm(this, mContentView);
-
-        startPositionUpdater();
-        UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
-    }
-
-    @Override
-    public void onDetachedFromWindow() {
-        LOGGER.v("Screensaver detached from window");
-        super.onDetachedFromWindow();
-
-        if (mSettingsContentObserver != null) {
-            getContentResolver().unregisterContentObserver(mSettingsContentObserver);
-        }
-
-        UiDataModel.getUiDataModel().removePeriodicCallback(mMidnightUpdater);
-        stopPositionUpdater();
-
-        // Tear down handlers for time reference changes and date updates.
-        if (Utils.isLOrLater()) {
-            unregisterReceiver(mAlarmChangedReceiver);
-        }
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        LOGGER.v("Screensaver configuration changed");
-        super.onConfigurationChanged(newConfig);
-
-        startPositionUpdater();
-    }
-
-    private void setClockStyle() {
-        Utils.setScreensaverClockStyle(mDigitalClock, mAnalogClock);
-        final boolean dimNightMode = DataModel.getDataModel().getScreensaverNightModeOn();
-        Utils.dimClockView(dimNightMode, mMainClockView);
-        setScreenBright(!dimNightMode);
-    }
-
-    /**
-     * The {@link #mContentView} will be drawn shortly. When that draw occurs, the position updater
-     * callback will also be executed to choose a random position for the time display as well as
-     * schedule future callbacks to move the time display each minute.
-     */
-    private void startPositionUpdater() {
-        if (mContentView != null) {
-            mContentView.getViewTreeObserver().addOnPreDrawListener(mStartPositionUpdater);
-        }
-    }
-
-    /**
-     * This activity is no longer in the foreground; position callbacks should be removed.
-     */
-    private void stopPositionUpdater() {
-        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 #mMainClockView} and schedule future callbacks to update
-         * its position.
-         *
-         * @return {@code true} to continue with the drawing pass
-         */
-        @Override
-        public boolean onPreDraw() {
-            if (mContentView.getViewTreeObserver().isAlive()) {
-                // (Re)start the periodic position updater.
-                mPositionUpdater.start();
-
-                // This listener must now be removed to avoid starting the position updater again.
-                mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
-            }
-            return true;
-        }
-    }
-}
diff --git a/src/com/android/deskclock/Screensaver.kt b/src/com/android/deskclock/Screensaver.kt
new file mode 100644
index 0000000..3c64131
--- /dev/null
+++ b/src/com/android/deskclock/Screensaver.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AlarmManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Configuration
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.service.dreams.DreamService
+import android.view.View
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.widget.TextClock
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.uidata.UiDataModel
+
+class Screensaver : DreamService() {
+    private val mStartPositionUpdater: OnPreDrawListener = StartPositionUpdater()
+    private var mPositionUpdater: MoveScreensaverRunnable? = null
+
+    private var mDateFormat: String? = null
+    private var mDateFormatForAccessibility: String? = null
+
+    private var mContentView: View? = null
+    private var mMainClockView: View? = null
+    private var mDigitalClock: TextClock? = null
+    private var mAnalogClock: AnalogClock? = null
+
+    /* Register ContentObserver to see alarm changes for pre-L */
+    private val mSettingsContentObserver: ContentObserver? = if (Utils.isLOrLater) {
+        null
+    } else {
+        object : ContentObserver(Handler(Looper.myLooper()!!)) {
+            override fun onChange(selfChange: Boolean) {
+                Utils.refreshAlarm(this@Screensaver, mContentView)
+            }
+        }
+    }
+
+    // Runs every midnight or when the time changes and refreshes the date.
+    private val mMidnightUpdater = Runnable {
+        Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+    }
+
+    /**
+     * Receiver to alarm clock changes.
+     */
+    private val mAlarmChangedReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            Utils.refreshAlarm(this@Screensaver, mContentView)
+        }
+    }
+
+    override fun onCreate() {
+        LOGGER.v("Screensaver created")
+
+        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)
+    }
+
+    override fun onAttachedToWindow() {
+        LOGGER.v("Screensaver attached to window")
+        super.onAttachedToWindow()
+
+        setContentView(R.layout.desk_clock_saver)
+
+        mContentView = findViewById(R.id.saver_container)
+        mMainClockView = mContentView?.findViewById(R.id.main_clock)
+        mDigitalClock = mMainClockView?.findViewById<View>(R.id.digital_clock) as TextClock
+        mAnalogClock = mMainClockView?.findViewById<View>(R.id.analog_clock) as AnalogClock
+
+        setClockStyle()
+        Utils.setClockIconTypeface(mContentView)
+        Utils.setTimeFormat(mDigitalClock, false)
+        mAnalogClock?.enableSeconds(false)
+
+        mContentView?.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
+                or View.SYSTEM_UI_FLAG_IMMERSIVE
+                or View.SYSTEM_UI_FLAG_FULLSCREEN
+                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+        mPositionUpdater = MoveScreensaverRunnable(mContentView!!, mMainClockView!!)
+
+        // We want the screen saver to exit upon user interaction.
+        isInteractive = false
+        isFullscreen = true
+
+        // Setup handlers for time reference changes and date updates.
+        if (Utils.isLOrLater) {
+            registerReceiver(mAlarmChangedReceiver,
+                    IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED))
+        }
+
+        mSettingsContentObserver?.let {
+            val uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED)
+            contentResolver.registerContentObserver(uri, false, it)
+        }
+
+        Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+        Utils.refreshAlarm(this, mContentView)
+
+        startPositionUpdater()
+        UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
+    }
+
+    override fun onDetachedFromWindow() {
+        LOGGER.v("Screensaver detached from window")
+        super.onDetachedFromWindow()
+
+        mSettingsContentObserver?.let {
+            contentResolver.unregisterContentObserver(it)
+        }
+
+        UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
+        stopPositionUpdater()
+
+        // Tear down handlers for time reference changes and date updates.
+        if (Utils.isLOrLater) {
+            unregisterReceiver(mAlarmChangedReceiver)
+        }
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        LOGGER.v("Screensaver configuration changed")
+        super.onConfigurationChanged(newConfig)
+
+        startPositionUpdater()
+    }
+
+    private fun setClockStyle() {
+        Utils.setScreensaverClockStyle(mDigitalClock!!, mAnalogClock!!)
+        val dimNightMode: Boolean = DataModel.dataModel.screensaverNightModeOn
+        Utils.dimClockView(dimNightMode, mMainClockView!!)
+        isScreenBright = !dimNightMode
+    }
+
+    /**
+     * The [.mContentView] will be drawn shortly. When that draw occurs, the position updater
+     * callback will also be executed to choose a random position for the time display as well as
+     * schedule future callbacks to move the time display each minute.
+     */
+    private fun startPositionUpdater() {
+        mContentView?.viewTreeObserver?.addOnPreDrawListener(mStartPositionUpdater)
+    }
+
+    /**
+     * This activity is no longer in the foreground; position callbacks should be removed.
+     */
+    private fun stopPositionUpdater() {
+        mContentView?.viewTreeObserver?.removeOnPreDrawListener(mStartPositionUpdater)
+        mPositionUpdater?.stop()
+    }
+
+    private inner class StartPositionUpdater : OnPreDrawListener {
+        /**
+         * This callback occurs after initial layout has completed. It is an appropriate place to
+         * select a random position for [.mMainClockView] and schedule future callbacks to update
+         * its position.
+         *
+         * @return `true` to continue with the drawing pass
+         */
+        override fun onPreDraw(): Boolean {
+            if (mContentView!!.viewTreeObserver.isAlive) {
+                // (Re)start the periodic position updater.
+                mPositionUpdater?.start()
+
+                // This listener must now be removed to avoid starting the position updater again.
+                mContentView?.viewTreeObserver?.removeOnPreDrawListener(mStartPositionUpdater)
+            }
+            return true
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("Screensaver")
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ScreensaverActivity.java b/src/com/android/deskclock/ScreensaverActivity.java
deleted file mode 100644
index 656cfc7..0000000
--- a/src/com/android/deskclock/ScreensaverActivity.java
+++ /dev/null
@@ -1,258 +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;
-
-import android.app.AlarmManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.provider.Settings;
-import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.TextClock;
-
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static android.content.Intent.ACTION_BATTERY_CHANGED;
-import static android.os.BatteryManager.EXTRA_PLUGGED;
-
-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 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 final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            LOGGER.v("ScreensaverActivity onReceive, action: " + intent.getAction());
-
-            switch (intent.getAction()) {
-                case Intent.ACTION_POWER_CONNECTED:
-                    updateWakeLock(true);
-                    break;
-                case Intent.ACTION_POWER_DISCONNECTED:
-                    updateWakeLock(false);
-                    break;
-                case Intent.ACTION_USER_PRESENT:
-                    finish();
-                    break;
-                case AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED:
-                    Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
-                    break;
-            }
-        }
-    };
-
-    /* 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(ScreensaverActivity.this, mContentView);
-            }
-        }
-        : null;
-
-    // Runs every midnight or when the time changes and refreshes the date.
-    private final Runnable mMidnightUpdater = new Runnable() {
-        @Override
-        public void run() {
-            Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
-        }
-    };
-
-    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);
-        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());
-
-        mPositionUpdater = new MoveScreensaverRunnable(mContentView, mMainClockView);
-
-        final Intent intent = getIntent();
-        if (intent != null) {
-            final int eventLabel = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, 0);
-            Events.sendScreensaverEvent(R.string.action_show, eventLabel);
-        }
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        final IntentFilter filter = new IntentFilter();
-        filter.addAction(Intent.ACTION_POWER_CONNECTED);
-        filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
-        filter.addAction(Intent.ACTION_USER_PRESENT);
-        if (Utils.isLOrLater()) {
-            filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
-        }
-        registerReceiver(mIntentReceiver, filter);
-
-        if (mSettingsContentObserver != null) {
-            @SuppressWarnings("deprecation")
-            final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
-            getContentResolver().registerContentObserver(uri, false, mSettingsContentObserver);
-        }
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
-        Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
-
-        startPositionUpdater();
-        UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
-
-        final Intent intent = registerReceiver(null, new IntentFilter(ACTION_BATTERY_CHANGED));
-        final boolean pluggedIn = intent != null && intent.getIntExtra(EXTRA_PLUGGED, 0) != 0;
-        updateWakeLock(pluggedIn);
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-        UiDataModel.getUiDataModel().removePeriodicCallback(mMidnightUpdater);
-        stopPositionUpdater();
-    }
-
-    @Override
-    public void onStop() {
-        if (mSettingsContentObserver != null) {
-            getContentResolver().unregisterContentObserver(mSettingsContentObserver);
-        }
-        unregisterReceiver(mIntentReceiver);
-        super.onStop();
-    }
-
-    @Override
-    public void onUserInteraction() {
-        // We want the screen saver to exit upon user interaction.
-        finish();
-    }
-
-    /**
-     * @param pluggedIn {@code true} iff the device is currently plugged in to a charger
-     */
-    private void updateWakeLock(boolean pluggedIn) {
-        final Window win = getWindow();
-        final WindowManager.LayoutParams winParams = win.getAttributes();
-        winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
-        if (pluggedIn) {
-            winParams.flags |= WINDOW_FLAGS;
-        } else {
-            winParams.flags &= (~WINDOW_FLAGS);
-        }
-        win.setAttributes(winParams);
-    }
-
-    /**
-     * The {@link #mContentView} will be drawn shortly. When that draw occurs, the position updater
-     * callback will also be executed to choose a random position for the time display as well as
-     * schedule future callbacks to move the time display each minute.
-     */
-    private void startPositionUpdater() {
-        mContentView.getViewTreeObserver().addOnPreDrawListener(mStartPositionUpdater);
-    }
-
-    /**
-     * This activity is no longer in the foreground; position callbacks should be removed.
-     */
-    private void stopPositionUpdater() {
-        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 #mMainClockView} and schedule future callbacks to update
-         * its position.
-         *
-         * @return {@code true} to continue with the drawing pass
-         */
-        @Override
-        public boolean onPreDraw() {
-            if (mContentView.getViewTreeObserver().isAlive()) {
-                // Start the periodic position updater.
-                mPositionUpdater.start();
-
-                // This listener must now be removed to avoid starting the position updater again.
-                mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
-            }
-            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/ScreensaverActivity.kt b/src/com/android/deskclock/ScreensaverActivity.kt
new file mode 100644
index 0000000..2f0f47f
--- /dev/null
+++ b/src/com/android/deskclock/ScreensaverActivity.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AlarmManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.os.BatteryManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.view.View
+import android.view.View.OnSystemUiVisibilityChangeListener
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.Window
+import android.view.WindowManager
+import android.widget.TextClock
+
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+
+class ScreensaverActivity : BaseActivity() {
+    private val mStartPositionUpdater: OnPreDrawListener = StartPositionUpdater()
+
+    private val mIntentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            LOGGER.v("ScreensaverActivity onReceive, action: " + intent.action)
+
+            when (intent.action) {
+                Intent.ACTION_POWER_CONNECTED -> updateWakeLock(true)
+                Intent.ACTION_POWER_DISCONNECTED -> updateWakeLock(false)
+                Intent.ACTION_USER_PRESENT -> finish()
+                AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED -> {
+                    Utils.refreshAlarm(this@ScreensaverActivity, mContentView)
+                }
+            }
+        }
+    }
+
+    /* Register ContentObserver to see alarm changes for pre-L */
+    private val mSettingsContentObserver: ContentObserver? = if (Utils.isPreL) {
+        object : ContentObserver(Handler(Looper.myLooper()!!)) {
+            override fun onChange(selfChange: Boolean) {
+                Utils.refreshAlarm(this@ScreensaverActivity, mContentView)
+            }
+        }
+    } else {
+        null
+    }
+
+    // Runs every midnight or when the time changes and refreshes the date.
+    private val mMidnightUpdater = Runnable {
+        Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+    }
+
+    private lateinit var mDateFormat: String
+    private lateinit var mDateFormatForAccessibility: String
+
+    private lateinit var mContentView: View
+    private lateinit var mMainClockView: View
+
+    private lateinit var mPositionUpdater: MoveScreensaverRunnable
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        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)
+        mContentView = findViewById(R.id.saver_container)
+        mMainClockView = mContentView.findViewById(R.id.main_clock)
+
+        val digitalClock = mMainClockView.findViewById<View>(R.id.digital_clock)
+        val analogClock = mMainClockView.findViewById<View>(R.id.analog_clock) as AnalogClock
+
+        Utils.setClockIconTypeface(mMainClockView)
+        Utils.setTimeFormat(digitalClock as TextClock, false)
+        Utils.setClockStyle(digitalClock, analogClock)
+        Utils.dimClockView(true, mMainClockView)
+        analogClock.enableSeconds(false)
+
+        mContentView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
+                or View.SYSTEM_UI_FLAG_IMMERSIVE
+                or View.SYSTEM_UI_FLAG_FULLSCREEN
+                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+        mContentView.setOnSystemUiVisibilityChangeListener(InteractionListener())
+
+        mPositionUpdater = MoveScreensaverRunnable(mContentView, mMainClockView)
+
+        getIntent()?.let {
+            val eventLabel = it.getIntExtra(Events.EXTRA_EVENT_LABEL, 0)
+            Events.sendScreensaverEvent(R.string.action_show, eventLabel)
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        val filter = IntentFilter()
+        filter.addAction(Intent.ACTION_POWER_CONNECTED)
+        filter.addAction(Intent.ACTION_POWER_DISCONNECTED)
+        filter.addAction(Intent.ACTION_USER_PRESENT)
+        if (Utils.isLOrLater) {
+            filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)
+        }
+        registerReceiver(mIntentReceiver, filter)
+
+        mSettingsContentObserver?.let {
+            val uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED)
+            getContentResolver().registerContentObserver(uri, false, it)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+        Utils.refreshAlarm(this, mContentView)
+
+        startPositionUpdater()
+        UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
+
+        val intent: Intent? = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
+        val pluggedIn = intent != null && intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
+        updateWakeLock(pluggedIn)
+    }
+
+    override fun onPause() {
+        super.onPause()
+        UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
+        stopPositionUpdater()
+    }
+
+    override fun onStop() {
+        mSettingsContentObserver?.let {
+            getContentResolver().unregisterContentObserver(it)
+        }
+        unregisterReceiver(mIntentReceiver)
+        super.onStop()
+    }
+
+    override fun onUserInteraction() {
+        // We want the screen saver to exit upon user interaction.
+        finish()
+    }
+
+    /**
+     * @param pluggedIn `true` iff the device is currently plugged in to a charger
+     */
+    private fun updateWakeLock(pluggedIn: Boolean) {
+        val win: Window = getWindow()
+        val winParams = win.attributes
+        winParams.flags = winParams.flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
+        if (pluggedIn) {
+            winParams.flags = winParams.flags or WINDOW_FLAGS
+        } else {
+            winParams.flags = winParams.flags and WINDOW_FLAGS.inv()
+        }
+        win.attributes = winParams
+    }
+
+    /**
+     * The [.mContentView] will be drawn shortly. When that draw occurs, the position updater
+     * callback will also be executed to choose a random position for the time display as well as
+     * schedule future callbacks to move the time display each minute.
+     */
+    private fun startPositionUpdater() {
+        mContentView.viewTreeObserver.addOnPreDrawListener(mStartPositionUpdater)
+    }
+
+    /**
+     * This activity is no longer in the foreground; position callbacks should be removed.
+     */
+    private fun stopPositionUpdater() {
+        mContentView.viewTreeObserver.removeOnPreDrawListener(mStartPositionUpdater)
+        mPositionUpdater.stop()
+    }
+
+    private inner class StartPositionUpdater : OnPreDrawListener {
+        /**
+         * This callback occurs after initial layout has completed. It is an appropriate place to
+         * select a random position for [.mMainClockView] and schedule future callbacks to update
+         * its position.
+         *
+         * @return `true` to continue with the drawing pass
+         */
+        override fun onPreDraw(): Boolean {
+            if (mContentView.viewTreeObserver.isAlive) {
+                // Start the periodic position updater.
+                mPositionUpdater.start()
+
+                // This listener must now be removed to avoid starting the position updater again.
+                mContentView.viewTreeObserver.removeOnPreDrawListener(mStartPositionUpdater)
+            }
+            return true
+        }
+    }
+
+    private inner class InteractionListener : OnSystemUiVisibilityChangeListener {
+        override fun onSystemUiVisibilityChange(visibility: Int) {
+            // When the user interacts with the screen, the navigation bar reappears
+            if (visibility and View.SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0) {
+                // We want the screen saver to exit upon user interaction.
+                finish()
+            }
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("ScreensaverActivity")
+
+        /** These flags keep the screen on if the device is plugged in.  */
+        private const val WINDOW_FLAGS = (WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+                or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/StopwatchTextController.java b/src/com/android/deskclock/StopwatchTextController.java
deleted file mode 100644
index 4b94dbd..0000000
--- a/src/com/android/deskclock/StopwatchTextController.java
+++ /dev/null
@@ -1,71 +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.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/StopwatchTextController.kt b/src/com/android/deskclock/StopwatchTextController.kt
new file mode 100644
index 0000000..a2a2c8e
--- /dev/null
+++ b/src/com/android/deskclock/StopwatchTextController.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.format.DateUtils
+import android.widget.TextView
+
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * A controller which will format a provided time in millis to display as a stopwatch.
+ */
+class StopwatchTextController(
+    private val mMainTextView: TextView,
+    private val mHundredthsTextView: TextView
+) {
+    private var mLastTime = Long.MIN_VALUE
+
+    fun setTimeString(accumulatedTime: Long) {
+        // 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
+        }
+
+        val hours = (accumulatedTime / DateUtils.HOUR_IN_MILLIS).toInt()
+        var remainder = (accumulatedTime % DateUtils.HOUR_IN_MILLIS).toInt()
+
+        val minutes = (remainder / DateUtils.MINUTE_IN_MILLIS).toInt()
+        remainder = (remainder % DateUtils.MINUTE_IN_MILLIS).toInt()
+
+        val seconds = (remainder / DateUtils.SECOND_IN_MILLIS).toInt()
+        remainder = (remainder % DateUtils.SECOND_IN_MILLIS).toInt()
+
+        mHundredthsTextView.text = UiDataModel.uiDataModel.getFormattedNumber(remainder / 10, 2)
+
+        // Avoid unnecessary computations and garbage creation if seconds have not changed since
+        // last layout pass.
+        if (mLastTime / DateUtils.SECOND_IN_MILLIS !=
+                accumulatedTime / DateUtils.SECOND_IN_MILLIS) {
+            val context = mMainTextView.context
+            val time = Utils.getTimeString(context, hours, minutes, seconds)
+            mMainTextView.text = time
+        }
+        mLastTime = accumulatedTime
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ThemeUtils.java b/src/com/android/deskclock/ThemeUtils.java
deleted file mode 100644
index a6d3156..0000000
--- a/src/com/android/deskclock/ThemeUtils.java
+++ /dev/null
@@ -1,100 +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.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import androidx.annotation.AttrRes;
-import androidx.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/ThemeUtils.kt b/src/com/android/deskclock/ThemeUtils.kt
new file mode 100644
index 0000000..1fdae54
--- /dev/null
+++ b/src/com/android/deskclock/ThemeUtils.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.TypedArray
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
+
+object ThemeUtils {
+    /** Temporary array used internally to resolve attributes. */
+    private val TEMP_ATTR = IntArray(1)
+
+    /**
+     * Convenience method for retrieving a themed color value.
+     *
+     * @param context the [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
+    fun resolveColor(context: Context, @AttrRes attr: Int): Int =
+            resolveColor(context, attr, stateSet = null)
+
+    /**
+     * Convenience method for retrieving a themed color value.
+     *
+     * @param context the [Context] to resolve the theme attribute against
+     * @param attr the attribute corresponding to the color to resolve
+     * @param stateSet an array of [android.view.View] states
+     * @return the color value of the resolved attribute
+     */
+    @JvmStatic
+    @ColorInt
+    fun resolveColor(context: Context, @AttrRes attr: Int, @AttrRes stateSet: IntArray?): Int {
+        var a: TypedArray
+        synchronized(TEMP_ATTR) {
+            TEMP_ATTR[0] = attr
+            a = context.obtainStyledAttributes(TEMP_ATTR)
+        }
+
+        try {
+            if (stateSet == null) {
+                return a.getColor(0, Color.RED)
+            }
+            val colorStateList = a.getColorStateList(0)
+            return colorStateList?.getColorForState(stateSet, Color.RED) ?: Color.RED
+        } finally {
+            a.recycle()
+        }
+    }
+
+    /**
+     * Convenience method for retrieving a themed drawable.
+     *
+     * @param context the [Context] to resolve the theme attribute against
+     * @param attr the attribute corresponding to the drawable to resolve
+     * @return the drawable of the resolved attribute
+     */
+    @JvmStatic
+    fun resolveDrawable(context: Context, @AttrRes attr: Int): Drawable? {
+        var a: TypedArray
+        synchronized(TEMP_ATTR) {
+            TEMP_ATTR[0] = attr
+            a = context.obtainStyledAttributes(TEMP_ATTR)
+        }
+
+        return try {
+            a.getDrawable(0)
+        } finally {
+            a.recycle()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/TimerCircleFrameLayout.java b/src/com/android/deskclock/TimerCircleFrameLayout.java
deleted file mode 100644
index e0cf320..0000000
--- a/src/com/android/deskclock/TimerCircleFrameLayout.java
+++ /dev/null
@@ -1,74 +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.content.Context;
-import android.util.AttributeSet;
-import android.widget.FrameLayout;
-
-/**
- * A container that frames a timer circle of some sort. The circle is allowed to grow naturally
- * according to its layout constraints up to the {@link R.dimen#max_timer_circle_size largest}
- * allowable size.
- */
-public class TimerCircleFrameLayout extends FrameLayout {
-
-    public TimerCircleFrameLayout(Context context) {
-        super(context);
-    }
-
-    public TimerCircleFrameLayout(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public TimerCircleFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    /**
-     * Note: this method assumes the parent container will specify {@link MeasureSpec#EXACTLY exact}
-     * width and height values.
-     *
-     * @param widthMeasureSpec horizontal space requirements as imposed by the parent
-     * @param heightMeasureSpec vertical space requirements as imposed by the parent
-     */
-    @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) - paddingLeft - paddingRight;
-        final int height = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom;
-        final int smallestDimension = Math.min(width, height);
-
-        // Fetch the absolute maximum circle size allowed.
-        final int maxSize = getResources().getDimensionPixelSize(R.dimen.max_timer_circle_size);
-        final int size = Math.min(smallestDimension, maxSize);
-
-        // Set the size of this container.
-        widthMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingLeft + paddingRight,
-                MeasureSpec.EXACTLY);
-        heightMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingTop + paddingBottom,
-                MeasureSpec.EXACTLY);
-
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-}
diff --git a/src/com/android/deskclock/TimerCircleFrameLayout.kt b/src/com/android/deskclock/TimerCircleFrameLayout.kt
new file mode 100644
index 0000000..b77c738
--- /dev/null
+++ b/src/com/android/deskclock/TimerCircleFrameLayout.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util.AttributeSet
+import android.widget.FrameLayout
+
+import kotlin.math.min
+
+/**
+ * A container that frames a timer circle of some sort. The circle is allowed to grow naturally
+ * according to its layout constraints up to the [largest][R.dimen.max_timer_circle_size]
+ * allowable size.
+ */
+class TimerCircleFrameLayout : FrameLayout {
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+    constructor(
+        context: Context,
+        attrs: AttributeSet?,
+        defStyleAttr: Int
+    ) : super(context, attrs, defStyleAttr)
+
+    /**
+     * Note: this method assumes the parent container will specify [exact][MeasureSpec.EXACTLY]
+     * width and height values.
+     *
+     * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+     * @param heightMeasureSpec vertical space requirements as imposed by the parent
+     */
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        var variableWidthMeasureSpec = widthMeasureSpec
+        var variableHeightMeasureSpec = heightMeasureSpec
+        val paddingLeft = paddingLeft
+        val paddingRight = paddingRight
+
+        val paddingTop = paddingTop
+        val paddingBottom = paddingBottom
+
+        // Fetch the exact sizes imposed by the parent container.
+        val width = MeasureSpec.getSize(variableWidthMeasureSpec) - paddingLeft - paddingRight
+        val height = MeasureSpec.getSize(variableHeightMeasureSpec) - paddingTop - paddingBottom
+        val smallestDimension = min(width, height)
+
+        // Fetch the absolute maximum circle size allowed.
+        val maxSize = resources.getDimensionPixelSize(R.dimen.max_timer_circle_size)
+        val size = min(smallestDimension, maxSize)
+
+        // Set the size of this container.
+        variableWidthMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingLeft + paddingRight,
+                MeasureSpec.EXACTLY)
+        variableHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingTop + paddingBottom,
+                MeasureSpec.EXACTLY)
+
+        super.onMeasure(variableWidthMeasureSpec, variableHeightMeasureSpec)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/TimerTextController.java b/src/com/android/deskclock/TimerTextController.java
deleted file mode 100644
index 1744a7b..0000000
--- a/src/com/android/deskclock/TimerTextController.java
+++ /dev/null
@@ -1,72 +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.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/TimerTextController.kt b/src/com/android/deskclock/TimerTextController.kt
new file mode 100644
index 0000000..77610e6
--- /dev/null
+++ b/src/com/android/deskclock/TimerTextController.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.format.DateUtils
+import android.widget.TextView
+
+/**
+ * A controller which will format a provided time in millis to display as a timer.
+ */
+class TimerTextController(private val mTextView: TextView) {
+    fun setTimeString(remainingTime: Long) {
+        var variableRemainingTime = remainingTime
+        var isNegative = false
+        if (variableRemainingTime < 0) {
+            variableRemainingTime = -variableRemainingTime
+            isNegative = true
+        }
+
+        var hours = (variableRemainingTime / DateUtils.HOUR_IN_MILLIS).toInt()
+        var remainder = (variableRemainingTime % DateUtils.HOUR_IN_MILLIS).toInt()
+
+        var minutes = (remainder / DateUtils.MINUTE_IN_MILLIS).toInt()
+        remainder = (remainder % DateUtils.MINUTE_IN_MILLIS).toInt()
+
+        var seconds = (remainder / DateUtils.SECOND_IN_MILLIS).toInt()
+        remainder = (remainder % DateUtils.SECOND_IN_MILLIS).toInt()
+
+        // Round up to the next second
+        if (!isNegative && remainder != 0) {
+            seconds++
+            if (seconds == 60) {
+                seconds = 0
+                minutes++
+                if (minutes == 60) {
+                    minutes = 0
+                    hours++
+                }
+            }
+        }
+
+        var time = Utils.getTimeString(mTextView.context, hours, minutes, seconds)
+        if (isNegative && !(hours == 0 && minutes == 0 && seconds == 0)) {
+            time = "\u2212" + time
+        }
+
+        mTextView.text = time
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Utils.java b/src/com/android/deskclock/Utils.java
deleted file mode 100644
index 0c9daf6..0000000
--- a/src/com/android/deskclock/Utils.java
+++ /dev/null
@@ -1,628 +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.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.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.Typeface;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Looper;
-import android.provider.Settings;
-import androidx.annotation.AnyRes;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.StringRes;
-import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
-import androidx.core.os.BuildCompat;
-import androidx.core.view.AccessibilityDelegateCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.text.style.RelativeSizeSpan;
-import android.text.style.StyleSpan;
-import android.text.style.TypefaceSpan;
-import android.util.ArraySet;
-import android.view.View;
-import android.widget.TextClock;
-import android.widget.TextView;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.text.NumberFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY;
-import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.graphics.Bitmap.Config.ARGB_8888;
-
-public class Utils {
-
-    /**
-     * {@link Uri} signifying the "silent" ringtone.
-     */
-    public static final Uri RINGTONE_SILENT = Uri.EMPTY;
-
-    public static void enforceMainLooper() {
-        if (Looper.getMainLooper() != Looper.myLooper()) {
-            throw new IllegalAccessError("May only call from main thread.");
-        }
-    }
-
-    public static void enforceNotMainLooper() {
-        if (Looper.getMainLooper() == Looper.myLooper()) {
-            throw new IllegalAccessError("May not call from main thread.");
-        }
-    }
-
-    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}
-     */
-    public static boolean isPreL() {
-        return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
-    }
-
-    /**
-     * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or
-     * {@link Build.VERSION_CODES#LOLLIPOP_MR1}
-     */
-    public static boolean isLOrLMR1() {
-        final int sdkInt = Build.VERSION.SDK_INT;
-        return sdkInt == Build.VERSION_CODES.LOLLIPOP || sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1;
-    }
-
-    /**
-     * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or later
-     */
-    public static boolean isLOrLater() {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
-    }
-
-    /**
-     * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP_MR1} or later
-     */
-    public static boolean isLMR1OrLater() {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
-    }
-
-    /**
-     * @return {@code true} if the device is {@link Build.VERSION_CODES#M} or later
-     */
-    public static boolean isMOrLater() {
-        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
-    }
-
-    /**
-     * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later
-     */
-    public static boolean isNOrLater() {
-        return BuildCompat.isAtLeastN();
-    }
-
-    /**
-     * @return {@code true} if the device is {@link Build.VERSION_CODES#N_MR1} or later
-     */
-    public static boolean isNMR1OrLater() {
-        return BuildCompat.isAtLeastNMR1();
-    }
-
-    /**
-     * @param resourceId identifies an application resource
-     * @return the Uri by which the application resource is accessed
-     */
-    public static Uri getResourceUri(Context context, @AnyRes int resourceId) {
-        return new Uri.Builder()
-                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
-                .authority(context.getPackageName())
-                .path(String.valueOf(resourceId))
-                .build();
-    }
-
-    /**
-     * @param view the scrollable view to test
-     * @return {@code true} iff the {@code view} content is currently scrolled to the top
-     */
-    public static boolean isScrolledToTop(View view) {
-        return !view.canScrollVertically(-1);
-    }
-
-    /**
-     * Calculate the amount by which the radius of a CircleTimerView should be offset by any
-     * of the extra painted objects.
-     */
-    public static float calculateRadiusOffset(
-            float strokeSize, float dotStrokeSize, float markerStrokeSize) {
-        return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize));
-    }
-
-    /**
-     * 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 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);
-    }
-
-    /**
-     * 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) {
-        final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle();
-        switch (clockStyle) {
-            case ANALOG:
-                digitalClock.setVisibility(View.GONE);
-                analogClock.setVisibility(View.VISIBLE);
-                return analogClock;
-            case DIGITAL:
-                digitalClock.setVisibility(View.VISIBLE);
-                analogClock.setVisibility(View.GONE);
-                return digitalClock;
-        }
-
-        throw new IllegalStateException("unexpected clock style: " + clockStyle);
-    }
-
-    /**
-     * For screensavers to set whether the digital or analog clock should be displayed.
-     * Returns the view to be displayed.
-     */
-    public static View setScreensaverClockStyle(View digitalClock, View analogClock) {
-        final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getScreensaverClockStyle();
-        switch (clockStyle) {
-            case ANALOG:
-                digitalClock.setVisibility(View.GONE);
-                analogClock.setVisibility(View.VISIBLE);
-                return analogClock;
-            case DIGITAL:
-                digitalClock.setVisibility(View.VISIBLE);
-                analogClock.setVisibility(View.GONE);
-                return digitalClock;
-        }
-
-        throw new IllegalStateException("unexpected clock style: " + clockStyle);
-    }
-
-    /**
-     * For screensavers to dim the lights if necessary.
-     */
-    public static void dimClockView(boolean dim, View clockView) {
-        Paint paint = new Paint();
-        paint.setColor(Color.WHITE);
-        paint.setColorFilter(new PorterDuffColorFilter(
-                (dim ? 0x40FFFFFF : 0xC0FFFFFF),
-                PorterDuff.Mode.MULTIPLY));
-        clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
-    }
-
-    /**
-     * 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
-     * @return a PendingIntent that will start a service
-     */
-    public static PendingIntent pendingServiceIntent(Context context, Intent intent) {
-        return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT);
-    }
-
-    /**
-     * 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
-     * @return a PendingIntent that will start an activity
-     */
-    public static PendingIntent pendingActivityIntent(Context context, Intent intent) {
-        return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT);
-    }
-
-    /**
-     * @return The next alarm from {@link AlarmManager}
-     */
-    public static String getNextAlarm(Context context) {
-        return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context);
-    }
-
-    @SuppressWarnings("deprecation")
-    @TargetApi(Build.VERSION_CODES.KITKAT)
-    private static String getNextAlarmPreL(Context context) {
-        final ContentResolver cr = context.getContentResolver();
-        return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED);
-    }
-
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    private static String getNextAlarmLOrLater(Context context) {
-        final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
-        final AlarmClockInfo info = getNextAlarmClock(am);
-        if (info != null) {
-            final long triggerTime = info.getTriggerTime();
-            final Calendar alarmTime = Calendar.getInstance();
-            alarmTime.setTimeInMillis(triggerTime);
-            return AlarmUtils.getFormattedTime(context, alarmTime);
-        }
-
-        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();
-        return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS;
-    }
-
-    /**
-     * Clock views can call this to refresh their alarm to the next upcoming value.
-     */
-    public static void refreshAlarm(Context context, View clock) {
-        final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon);
-        final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm);
-        if (nextAlarmView == null) {
-            return;
-        }
-
-        final String alarm = getNextAlarm(context);
-        if (!TextUtils.isEmpty(alarm)) {
-            final String description = context.getString(R.string.next_alarm_description, alarm);
-            nextAlarmView.setText(alarm);
-            nextAlarmView.setContentDescription(description);
-            nextAlarmView.setVisibility(View.VISIBLE);
-            nextAlarmIconView.setVisibility(View.VISIBLE);
-            nextAlarmIconView.setContentDescription(description);
-        } 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.
-     **/
-    public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) {
-        final TextView dateDisplay = (TextView) clock.findViewById(R.id.date);
-        if (dateDisplay == null) {
-            return;
-        }
-
-        final Locale l = Locale.getDefault();
-        final String datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton);
-        final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton);
-
-        final Date now = new Date();
-        dateDisplay.setText(new SimpleDateFormat(datePattern, l).format(now));
-        dateDisplay.setVisibility(View.VISIBLE);
-        dateDisplay.setContentDescription(new SimpleDateFormat(descriptionPattern, l).format(now));
-    }
-
-    /***
-     * 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 includeSeconds whether or not to include seconds in the clock's time
-     */
-    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 */, includeSeconds));
-            // Get the best format for 24 hours mode according to the locale
-            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
-     * @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, boolean includeSeconds) {
-        String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
-                includeSeconds ? "hmsa" : "hma");
-        if (amPmRatio <= 0) {
-            pattern = pattern.replaceAll("a", "").trim();
-        }
-
-        // Replace spaces with "Hair Space"
-        pattern = pattern.replaceAll(" ", "\u200A");
-        // Build a spannable so that the am/pm will be formatted
-        int amPmPos = pattern.indexOf('a');
-        if (amPmPos == -1) {
-            return pattern;
-        }
-
-        final Spannable sp = new SpannableString(pattern);
-        sp.setSpan(new RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1,
-                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-        sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1,
-                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-        sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1,
-                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-
-        return sp;
-    }
-
-    public static CharSequence get24ModeFormat(boolean includeSeconds) {
-        return DateFormat.getBestDateTimePattern(Locale.getDefault(),
-                includeSeconds ? "Hms" : "Hm");
-    }
-
-    /**
-     * Returns string denoting the timezone hour offset (e.g. GMT -8:00)
-     *
-     * @param useShortForm Whether to return a short form of the header that rounds to the
-     *                     nearest hour and excludes the "GMT" prefix
-     */
-    public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) {
-        final int gmtOffset = timezone.getRawOffset();
-        final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS;
-        final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) /
-                DateUtils.MINUTE_IN_MILLIS;
-
-        if (useShortForm) {
-            return String.format(Locale.ENGLISH, "%+d", hour);
-        } else {
-            return String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min);
-        }
-    }
-
-    /**
-     * Given a point in time, return the subsequent moment any of the time zones changes days.
-     * 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 zones a collection of time zones
-     * @return the nearest point in the future at which any of the time zones changes days
-     */
-    public static Date getNextDay(Date time, Collection<TimeZone> zones) {
-        Calendar next = null;
-        for (TimeZone tz : zones) {
-            final Calendar c = Calendar.getInstance(tz);
-            c.setTime(time);
-
-            // Advance to the next day.
-            c.add(Calendar.DAY_OF_YEAR, 1);
-
-            // Reset the time to midnight.
-            c.set(Calendar.HOUR_OF_DAY, 0);
-            c.set(Calendar.MINUTE, 0);
-            c.set(Calendar.SECOND, 0);
-            c.set(Calendar.MILLISECOND, 0);
-
-            if (next == null || c.compareTo(next) < 0) {
-                next = c;
-            }
-        }
-
-        return next == null ? null : next.getTime();
-    }
-
-    public static String getNumberFormattedQuantityString(Context context, int id, int quantity) {
-        final String localizedQuantity = NumberFormat.getInstance().format(quantity);
-        return context.getResources().getQuantityString(id, quantity, localizedQuantity);
-    }
-
-    /**
-     * @return {@code true} iff the widget is being hosted in a container where tapping is allowed
-     */
-    public static boolean isWidgetClickable(AppWidgetManager widgetManager, int widgetId) {
-        final Bundle wo = widgetManager.getAppWidgetOptions(widgetId);
-        return wo != null
-                && wo.getInt(OPTION_APPWIDGET_HOST_CATEGORY, -1) != WIDGET_CATEGORY_KEYGUARD;
-    }
-
-    /**
-     * @return a vector-drawable inflated from the given {@code resId}
-     */
-    public static VectorDrawableCompat getVectorDrawable(Context context, @DrawableRes int resId) {
-        return VectorDrawableCompat.create(context.getResources(), resId, context.getTheme());
-    }
-
-    /**
-     * This method assumes the given {@code view} has already been layed out.
-     *
-     * @return a Bitmap containing an image of the {@code view} at its current size
-     */
-    public static Bitmap createBitmap(View view) {
-        final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), ARGB_8888);
-        final Canvas canvas = new Canvas(bitmap);
-        view.draw(canvas);
-        return bitmap;
-    }
-
-    /**
-     * {@link ArraySet} is @hide prior to {@link Build.VERSION_CODES#M}.
-     */
-    @SuppressLint("NewApi")
-    public static <E> ArraySet<E> newArraySet(Collection<E> collection) {
-        final ArraySet<E> arraySet = new ArraySet<>(collection.size());
-        arraySet.addAll(collection);
-        return arraySet;
-    }
-
-    /**
-     * @param context from which to query the current device configuration
-     * @return {@code true} if the device is currently in portrait or reverse portrait orientation
-     */
-    public static boolean isPortrait(Context context) {
-        return context.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
-    }
-
-    /**
-     * @param context from which to query the current device configuration
-     * @return {@code true} if the device is currently in landscape or reverse landscape orientation
-     */
-    public static boolean isLandscape(Context context) {
-        return context.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
-    }
-
-    public static long now() {
-        return DataModel.getDataModel().elapsedRealtime();
-    }
-
-    public static long wallClock() {
-        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/Utils.kt b/src/com/android/deskclock/Utils.kt
new file mode 100644
index 0000000..166803a
--- /dev/null
+++ b/src/com/android/deskclock/Utils.kt
@@ -0,0 +1,621 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.TargetApi
+import android.app.AlarmManager
+import android.app.AlarmManager.AlarmClockInfo
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Typeface
+import android.net.Uri
+import android.os.Build
+import android.os.Looper
+import android.provider.Settings
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+import android.text.style.RelativeSizeSpan
+import android.text.style.StyleSpan
+import android.text.style.TypefaceSpan
+import android.util.ArraySet
+import android.view.View
+import android.widget.TextClock
+import android.widget.TextView
+import androidx.annotation.AnyRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.core.os.BuildCompat
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
+import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.uidata.UiDataModel
+
+import java.text.NumberFormat
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+import kotlin.math.abs
+import kotlin.math.max
+
+object Utils {
+    /**
+     * [Uri] signifying the "silent" ringtone.
+     */
+    @JvmField
+    val RINGTONE_SILENT = Uri.EMPTY
+
+    fun enforceMainLooper() {
+        if (Looper.getMainLooper() != Looper.myLooper()) {
+            throw IllegalAccessError("May only call from main thread.")
+        }
+    }
+
+    fun enforceNotMainLooper() {
+        if (Looper.getMainLooper() == Looper.myLooper()) {
+            throw IllegalAccessError("May not call from main thread.")
+        }
+    }
+
+    fun indexOf(array: Array<out Any>, item: Any): Int {
+        for (i in array.indices) {
+            if (array[i] == item) {
+                return i
+            }
+        }
+        return -1
+    }
+
+    /**
+     * @return `true` if the device is prior to [Build.VERSION_CODES.LOLLIPOP]
+     */
+    val isPreL: Boolean
+        get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
+
+    /**
+     * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP] or
+     * [Build.VERSION_CODES.LOLLIPOP_MR1]
+     */
+    val isLOrLMR1: Boolean
+        get() {
+            val sdkInt = Build.VERSION.SDK_INT
+            return sdkInt == Build.VERSION_CODES.LOLLIPOP ||
+                    sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1
+        }
+
+    /**
+     * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP] or later
+     */
+    val isLOrLater: Boolean
+        get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+
+    /**
+     * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP_MR1] or later
+     */
+    val isLMR1OrLater: Boolean
+        get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1
+
+    /**
+     * @return `true` if the device is [Build.VERSION_CODES.M] or later
+     */
+    val isMOrLater: Boolean
+        get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+
+    /**
+     * @return `true` if the device is [Build.VERSION_CODES.N] or later
+     */
+    val isNOrLater: Boolean
+        get() = BuildCompat.isAtLeastN()
+
+    /**
+     * @return `true` if the device is [Build.VERSION_CODES.N_MR1] or later
+     */
+    val isNMR1OrLater: Boolean
+        get() = BuildCompat.isAtLeastNMR1()
+
+    /**
+     * @return `true` if the device is [Build.VERSION_CODES.O] or later
+     */
+    val isOOrLater: Boolean
+        get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+
+    /**
+     * @return {@code true} if the device is {@link Build.VERSION_CODES#P} or later
+     */
+    val isPOrLater: Boolean
+        get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
+
+    /**
+     * @param resourceId identifies an application resource
+     * @return the Uri by which the application resource is accessed
+     */
+    fun getResourceUri(context: Context, @AnyRes resourceId: Int): Uri {
+        return Uri.Builder()
+                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+                .authority(context.packageName)
+                .path(resourceId.toString())
+                .build()
+    }
+
+    /**
+     * @param view the scrollable view to test
+     * @return `true` iff the `view` content is currently scrolled to the top
+     */
+    fun isScrolledToTop(view: View): Boolean {
+        return !view.canScrollVertically(-1)
+    }
+
+    /**
+     * Calculate the amount by which the radius of a CircleTimerView should be offset by any
+     * of the extra painted objects.
+     */
+    fun calculateRadiusOffset(
+        strokeSize: Float,
+        dotStrokeSize: Float,
+        markerStrokeSize: Float
+    ): Float {
+        return max(strokeSize, max(dotStrokeSize, markerStrokeSize))
+    }
+
+    /**
+     * 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.
+     */
+    fun setClockSecondsEnabled(digitalClock: TextClock, analogClock: AnalogClock) {
+        val displaySeconds: Boolean = DataModel.dataModel.displayClockSeconds
+        when (DataModel.dataModel.clockStyle) {
+            DataModel.ClockStyle.ANALOG -> {
+                setTimeFormat(digitalClock, false)
+                analogClock.enableSeconds(displaySeconds)
+            }
+            DataModel.ClockStyle.DIGITAL -> {
+                analogClock.enableSeconds(false)
+                setTimeFormat(digitalClock, displaySeconds)
+            }
+        }
+    }
+
+    /**
+     * Set whether the digital or analog clock should be displayed in the application.
+     * Returns the view to be displayed.
+     */
+    fun setClockStyle(digitalClock: View, analogClock: View): View {
+        return when (DataModel.dataModel.clockStyle) {
+            DataModel.ClockStyle.ANALOG -> {
+                digitalClock.visibility = View.GONE
+                analogClock.visibility = View.VISIBLE
+                analogClock
+            }
+            DataModel.ClockStyle.DIGITAL -> {
+                digitalClock.visibility = View.VISIBLE
+                analogClock.visibility = View.GONE
+                digitalClock
+            }
+        }
+    }
+
+    /**
+     * For screensavers to set whether the digital or analog clock should be displayed.
+     * Returns the view to be displayed.
+     */
+    fun setScreensaverClockStyle(digitalClock: View, analogClock: View): View {
+        return when (DataModel.dataModel.screensaverClockStyle) {
+            DataModel.ClockStyle.ANALOG -> {
+                digitalClock.visibility = View.GONE
+                analogClock.visibility = View.VISIBLE
+                analogClock
+            }
+            DataModel.ClockStyle.DIGITAL -> {
+                digitalClock.visibility = View.VISIBLE
+                analogClock.visibility = View.GONE
+                digitalClock
+            }
+        }
+    }
+
+    /**
+     * For screensavers to dim the lights if necessary.
+     */
+    fun dimClockView(dim: Boolean, clockView: View) {
+        val paint = Paint()
+        paint.color = Color.WHITE
+        paint.colorFilter = PorterDuffColorFilter(
+                if (dim) 0x40FFFFFF else -0x3f000001,
+                PorterDuff.Mode.MULTIPLY)
+        clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint)
+    }
+
+    /**
+     * Update and return the PendingIntent corresponding to the given `intent`.
+     *
+     * @param context the Context in which the PendingIntent should start the service
+     * @param intent an Intent describing the service to be started
+     * @return a PendingIntent that will start a service
+     */
+    fun pendingServiceIntent(context: Context, intent: Intent): PendingIntent {
+        return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+    }
+
+    /**
+     * Update and return the PendingIntent corresponding to the given `intent`.
+     *
+     * @param context the Context in which the PendingIntent should start the activity
+     * @param intent an Intent describing the activity to be started
+     * @return a PendingIntent that will start an activity
+     */
+    fun pendingActivityIntent(context: Context, intent: Intent): PendingIntent {
+        return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+    }
+
+    /**
+     * @return The next alarm from [AlarmManager]
+     */
+    fun getNextAlarm(context: Context): String? {
+        return if (isPreL) getNextAlarmPreL(context) else getNextAlarmLOrLater(context)
+    }
+
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private fun getNextAlarmPreL(context: Context): String {
+        val cr = context.contentResolver
+        return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED)
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun getNextAlarmLOrLater(context: Context): String? {
+        val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+        val info = getNextAlarmClock(am)
+        if (info != null) {
+            val triggerTime = info.triggerTime
+            val alarmTime = Calendar.getInstance()
+            alarmTime.timeInMillis = triggerTime
+            return AlarmUtils.getFormattedTime(context, alarmTime)
+        }
+
+        return null
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun getNextAlarmClock(am: AlarmManager): AlarmClockInfo? {
+        return am.nextAlarmClock
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    fun updateNextAlarm(am: AlarmManager, info: AlarmClockInfo?, op: PendingIntent?) {
+        am.setAlarmClock(info, op)
+    }
+
+    fun isAlarmWithin24Hours(alarmInstance: AlarmInstance): Boolean {
+        val nextAlarmTime: Calendar = alarmInstance.alarmTime
+        val nextAlarmTimeMillis = nextAlarmTime.timeInMillis
+        return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS
+    }
+
+    /**
+     * Clock views can call this to refresh their alarm to the next upcoming value.
+     */
+    fun refreshAlarm(context: Context, clock: View?) {
+        val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView
+        val nextAlarmView = clock.findViewById<View>(R.id.nextAlarm) as TextView? ?: return
+
+        val alarm = getNextAlarm(context)
+        if (!TextUtils.isEmpty(alarm)) {
+            val description = context.getString(R.string.next_alarm_description, alarm)
+            nextAlarmView.text = alarm
+            nextAlarmView.contentDescription = description
+            nextAlarmView.visibility = View.VISIBLE
+            nextAlarmIconView.visibility = View.VISIBLE
+            nextAlarmIconView.contentDescription = description
+        } else {
+            nextAlarmView.visibility = View.GONE
+            nextAlarmIconView.visibility = View.GONE
+        }
+    }
+
+    fun setClockIconTypeface(clock: View?) {
+        val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView?
+        nextAlarmIconView?.typeface = UiDataModel.uiDataModel.alarmIconTypeface
+    }
+
+    /**
+     * Clock views can call this to refresh their date.
+     */
+    fun updateDate(dateSkeleton: String?, descriptionSkeleton: String?, clock: View?) {
+        val dateDisplay = clock?.findViewById<View>(R.id.date) as TextView? ?: return
+
+        val l = Locale.getDefault()
+        val datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton)
+        val descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton)
+
+        val now = Date()
+        dateDisplay.text = SimpleDateFormat(datePattern, l).format(now)
+        dateDisplay.visibility = View.VISIBLE
+        dateDisplay.contentDescription = SimpleDateFormat(descriptionPattern, l).format(now)
+    }
+
+    /***
+     * 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 includeSeconds whether or not to include seconds in the clock's time
+     */
+    fun setTimeFormat(clock: TextClock?, includeSeconds: Boolean) {
+        // Get the best format for 12 hours mode according to the locale
+        clock?.format12Hour = get12ModeFormat(amPmRatio = 0.4f, includeSeconds = includeSeconds)
+        // Get the best format for 24 hours mode according to the locale
+        clock?.format24Hour = 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
+     * @param includeSeconds whether or not to include seconds in the time string
+     * @return format string for 12 hours mode time, not including seconds
+     */
+    fun get12ModeFormat(amPmRatio: Float, includeSeconds: Boolean): CharSequence {
+        var pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
+                if (includeSeconds) "hmsa" else "hma")
+        if (amPmRatio <= 0) {
+            pattern = pattern.replace("a".toRegex(), "").trim { it <= ' ' }
+        }
+
+        // Replace spaces with "Hair Space"
+        pattern = pattern.replace(" ".toRegex(), "\u200A")
+        // Build a spannable so that the am/pm will be formatted
+        val amPmPos = pattern.indexOf('a')
+        if (amPmPos == -1) {
+            return pattern
+        }
+
+        val sp: Spannable = SpannableString(pattern)
+        sp.setSpan(RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+        sp.setSpan(StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+        sp.setSpan(TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1,
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+
+        return sp
+    }
+
+    fun get24ModeFormat(includeSeconds: Boolean): CharSequence {
+        return DateFormat.getBestDateTimePattern(Locale.getDefault(),
+                if (includeSeconds) "Hms" else "Hm")
+    }
+
+    /**
+     * Returns string denoting the timezone hour offset (e.g. GMT -8:00)
+     *
+     * @param useShortForm Whether to return a short form of the header that rounds to the
+     * nearest hour and excludes the "GMT" prefix
+     */
+    fun getGMTHourOffset(timezone: TimeZone, useShortForm: Boolean): String {
+        val gmtOffset = timezone.rawOffset
+        val hour = gmtOffset / DateUtils.HOUR_IN_MILLIS
+        val min = abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS / DateUtils.MINUTE_IN_MILLIS
+
+        return if (useShortForm) {
+            String.format(Locale.ENGLISH, "%+d", hour)
+        } else {
+            String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min)
+        }
+    }
+
+    /**
+     * Given a point in time, return the subsequent moment any of the time zones changes days.
+     * 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 zones a collection of time zones
+     * @return the nearest point in the future at which any of the time zones changes days
+     */
+    fun getNextDay(time: Date, zones: Collection<TimeZone>): Date {
+        var next: Calendar? = null
+        for (tz in zones) {
+            val c = Calendar.getInstance(tz)
+            c.time = time
+
+            // Advance to the next day.
+            c.add(Calendar.DAY_OF_YEAR, 1)
+
+            // Reset the time to midnight.
+            c[Calendar.HOUR_OF_DAY] = 0
+            c[Calendar.MINUTE] = 0
+            c[Calendar.SECOND] = 0
+            c[Calendar.MILLISECOND] = 0
+
+            if (next == null || c < next) {
+                next = c
+            }
+        }
+
+        return next!!.time
+    }
+
+    fun getNumberFormattedQuantityString(context: Context, id: Int, quantity: Int): String {
+        val localizedQuantity = NumberFormat.getInstance().format(quantity.toLong())
+        return context.resources.getQuantityString(id, quantity, localizedQuantity)
+    }
+
+    /**
+     * @return `true` iff the widget is being hosted in a container where tapping is allowed
+     */
+    fun isWidgetClickable(widgetManager: AppWidgetManager, widgetId: Int): Boolean {
+        val wo = widgetManager.getAppWidgetOptions(widgetId)
+        return (wo != null &&
+                wo.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1)
+                != AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
+    }
+
+    /**
+     * @return a vector-drawable inflated from the given `resId`
+     */
+    fun getVectorDrawable(context: Context, @DrawableRes resId: Int): VectorDrawableCompat? {
+        return VectorDrawableCompat.create(context.resources, resId, context.theme)
+    }
+
+    /**
+     * This method assumes the given `view` has already been layed out.
+     *
+     * @return a Bitmap containing an image of the `view` at its current size
+     */
+    fun createBitmap(view: View): Bitmap {
+        val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(bitmap)
+        view.draw(canvas)
+        return bitmap
+    }
+
+    /**
+     * [ArraySet] is @hide prior to [Build.VERSION_CODES.M].
+     */
+    @SuppressLint("NewApi")
+    fun <E> newArraySet(collection: Collection<E>): ArraySet<E> {
+        val arraySet = ArraySet<E>(collection.size)
+        arraySet.addAll(collection)
+        return arraySet
+    }
+
+    /**
+     * @param context from which to query the current device configuration
+     * @return `true` if the device is currently in portrait or reverse portrait orientation
+     */
+    fun isPortrait(context: Context): Boolean {
+        return context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
+    }
+
+    /**
+     * @param context from which to query the current device configuration
+     * @return `true` if the device is currently in landscape or reverse landscape orientation
+     */
+    fun isLandscape(context: Context): Boolean {
+        return context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+    }
+
+    fun now(): Long = DataModel.dataModel.elapsedRealtime()
+
+    fun wallClock(): Long = DataModel.dataModel.currentTimeMillis()
+
+    /**
+     * @param context to obtain strings.
+     * @param displayMinutes whether or not minutes should be included
+     * @param isAhead `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
+     */
+    fun createHoursDifferentString(
+        context: Context,
+        displayMinutes: Boolean,
+        isAhead: Boolean,
+        hoursDifferent: Int,
+        minutesDifferent: Int
+    ): String {
+        val timeString: String
+        timeString = if (displayMinutes && hoursDifferent != 0) {
+            // Both minutes and hours
+            val hoursShortQuantityString = getNumberFormattedQuantityString(context,
+                    R.plurals.hours_short, abs(hoursDifferent))
+            val minsShortQuantityString = getNumberFormattedQuantityString(context,
+                    R.plurals.minutes_short, abs(minutesDifferent))
+            @StringRes val stringType = if (isAhead) {
+                R.string.world_hours_minutes_ahead
+            } else {
+                R.string.world_hours_minutes_behind
+            }
+            context.getString(stringType, hoursShortQuantityString,
+                    minsShortQuantityString)
+        } else {
+            // Minutes alone or hours alone
+            val hoursQuantityString = getNumberFormattedQuantityString(
+                    context, R.plurals.hours, abs(hoursDifferent))
+            val minutesQuantityString = getNumberFormattedQuantityString(
+                    context, R.plurals.minutes, abs(minutesDifferent))
+            @StringRes val stringType = if (isAhead) {
+                R.string.world_time_ahead
+            } else {
+                R.string.world_time_behind
+            }
+            context.getString(stringType, if (displayMinutes) {
+                minutesQuantityString
+            } else {
+                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
+     */
+    fun getTimeString(context: Context, hours: Int, minutes: Int, seconds: Int): String {
+        if (hours != 0) {
+            return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds)
+        }
+        return if (minutes != 0) {
+            context.getString(R.string.minutes_seconds, minutes, seconds)
+        } else {
+            context.getString(R.string.seconds, seconds)
+        }
+    }
+
+    class ClickAccessibilityDelegate @JvmOverloads constructor(
+        /** The label for talkback to apply to the view  */
+        private val mLabel: String,
+        /** Whether or not to always make the view visible to talkback  */
+        private val mIsAlwaysAccessibilityVisible: Boolean = false
+    ) : AccessibilityDelegateCompat() {
+
+        override fun onInitializeAccessibilityNodeInfo(
+            host: View,
+            info: AccessibilityNodeInfoCompat
+        ) {
+            super.onInitializeAccessibilityNodeInfo(host, info)
+            if (mIsAlwaysAccessibilityVisible) {
+                info.setVisibleToUser(true)
+            }
+            info.addAction(AccessibilityActionCompat(
+                    AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel))
+        }
+    }
+}
diff --git a/src/com/android/deskclock/VerticalViewPager.java b/src/com/android/deskclock/VerticalViewPager.java
deleted file mode 100644
index da630ca..0000000
--- a/src/com/android/deskclock/VerticalViewPager.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import androidx.viewpager.widget.ViewPager;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-
-public class VerticalViewPager extends ViewPager {
-
-    public VerticalViewPager(Context context) {
-        this(context, null);
-    }
-
-    public VerticalViewPager(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        init();
-    }
-
-    /**
-     * @return {@code false} since a vertical view pager can never be scrolled horizontally
-     */
-    @Override
-    public boolean canScrollHorizontally(int direction) {
-        return false;
-    }
-
-    /**
-     * @return {@code true} iff a normal view pager would support horizontal scrolling at this time
-     */
-    @Override
-    public boolean canScrollVertically(int direction) {
-        return super.canScrollHorizontally(direction);
-    }
-
-    private void init() {
-        // Make page transit vertical
-        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) {
-        final boolean toHandle = super.onTouchEvent(flipXY(ev));
-        // Return MotionEvent to normal
-        flipXY(ev);
-        return toHandle;
-    }
-
-    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 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/VerticalViewPager.kt b/src/com/android/deskclock/VerticalViewPager.kt
new file mode 100644
index 0000000..d399726
--- /dev/null
+++ b/src/com/android/deskclock/VerticalViewPager.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import androidx.viewpager.widget.ViewPager
+
+class VerticalViewPager @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : ViewPager(context, attrs) {
+    init {
+        init()
+    }
+
+    /**
+     * @return `false` since a vertical view pager can never be scrolled horizontally
+     */
+    override fun canScrollHorizontally(direction: Int): Boolean = false
+
+    /**
+     * @return `true` iff a normal view pager would support horizontal scrolling at this time
+     */
+    override fun canScrollVertically(direction: Int): Boolean {
+        return super.canScrollHorizontally(direction)
+    }
+
+    private fun init() {
+        // Make page transit vertical
+        setPageTransformer(true, VerticalPageTransformer())
+        // Get rid of the overscroll drawing that happens on the left and right (the ripple)
+        setOverScrollMode(View.OVER_SCROLL_NEVER)
+    }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+        val toIntercept: Boolean = super.onInterceptTouchEvent(flipXY(ev))
+        // Return MotionEvent to normal
+        flipXY(ev)
+        return toIntercept
+    }
+
+    override fun onTouchEvent(ev: MotionEvent): Boolean {
+        val toHandle: Boolean = super.onTouchEvent(flipXY(ev))
+        // Return MotionEvent to normal
+        flipXY(ev)
+        return toHandle
+    }
+
+    private fun flipXY(ev: MotionEvent): MotionEvent {
+        val width: Float = width.toFloat()
+        val height: Float = height.toFloat()
+
+        val x = ev.y / height * width
+        val y = ev.x / width * height
+
+        ev.setLocation(x, y)
+
+        return ev
+    }
+
+    private class VerticalPageTransformer : ViewPager.PageTransformer {
+        override fun transformPage(view: View, position: Float) {
+            val pageWidth = view.width
+            val pageHeight = view.height
+            when {
+                position < -1 -> {
+                    // This page is way off-screen to the left.
+                    view.alpha = 0f
+                }
+                position <= 1 -> {
+                    view.alpha = 1f
+                    // Counteract the default slide transition
+                    view.translationX = pageWidth * -position
+                    // set Y position to swipe in from top
+                    val yPosition = position * pageHeight
+                    view.translationY = yPosition
+                }
+                else -> {
+                    // This page is way off-screen to the right.
+                    view.alpha = 0f
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemController.java b/src/com/android/deskclock/actionbarmenu/MenuItemController.java
deleted file mode 100644
index 542a035..0000000
--- a/src/com/android/deskclock/actionbarmenu/MenuItemController.java
+++ /dev/null
@@ -1,51 +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.actionbarmenu;
-
-import android.view.Menu;
-import android.view.MenuItem;
-
-/**
- * Interface for handling a single menu item in action bar.
- */
-public interface MenuItemController {
-
-    /**
-     * Returns the menu item resource id that the controller manages.
-     */
-    int getId();
-
-    /**
-     * Create the menu item.
-     */
-    void onCreateOptionsItem(Menu menu);
-
-    /**
-     * Called immediately before the {@link MenuItem} is shown.
-     *
-     * @param item the {@link MenuItem} created by the controller
-     */
-    void onPrepareOptionsItem(MenuItem item);
-
-    /**
-     * Attempts to handle the click action.
-     *
-     * @param item the {@link MenuItem} that was selected
-     * @return {@code true} if the action is handled by this controller
-     */
-    boolean onOptionsItemSelected(MenuItem item);
-}
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemController.kt b/src/com/android/deskclock/actionbarmenu/MenuItemController.kt
new file mode 100644
index 0000000..12cdcce
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemController.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.actionbarmenu
+
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * Interface for handling a single menu item in action bar.
+ */
+interface MenuItemController {
+    /**
+     * Returns the menu item resource id that the controller manages.
+     */
+    val id: Int
+
+    /**
+     * Create the menu item.
+     */
+    fun onCreateOptionsItem(menu: Menu)
+
+    /**
+     * Called immediately before the [MenuItem] is shown.
+     *
+     * @param item the [MenuItem] created by the controller
+     */
+    fun onPrepareOptionsItem(item: MenuItem)
+
+    /**
+     * Attempts to handle the click action.
+     *
+     * @param item the [MenuItem] that was selected
+     * @return `true` if the action is handled by this controller
+     */
+    fun onOptionsItemSelected(item: MenuItem): Boolean
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java
deleted file mode 100644
index 3598f16..0000000
--- a/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.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.actionbarmenu;
-
-import android.app.Activity;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Factory that builds optional {@link MenuItemController} instances.
- */
-public final class MenuItemControllerFactory {
-
-    private static final MenuItemControllerFactory INSTANCE = new MenuItemControllerFactory();
-
-    public static MenuItemControllerFactory getInstance() {
-        return INSTANCE;
-    }
-
-    private final List<MenuItemProvider> mMenuItemProviders;
-
-    private MenuItemControllerFactory() {
-        mMenuItemProviders = new ArrayList<>();
-    }
-
-    public MenuItemControllerFactory addMenuItemProvider(MenuItemProvider provider) {
-        mMenuItemProviders.add(provider);
-        return this;
-    }
-
-    public MenuItemController[] buildMenuItemControllers(Activity activity) {
-        final int providerSize = mMenuItemProviders.size();
-        final MenuItemController[] controllers = new MenuItemController[providerSize];
-        for (int i = 0; i < providerSize; i++) {
-            controllers[i] = mMenuItemProviders.get(i).provide(activity);
-        }
-        return controllers;
-    }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.kt b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.kt
new file mode 100644
index 0000000..97ee000
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.actionbarmenu
+
+import android.app.Activity
+
+import kotlin.collections.ArrayList
+
+/**
+ * Factory that builds optional [MenuItemController] instances.
+ */
+object MenuItemControllerFactory {
+    private val mMenuItemProviders: MutableList<MenuItemProvider> = ArrayList()
+
+    fun buildMenuItemControllers(activity: Activity?): Array<MenuItemController?> {
+        val providerSize = mMenuItemProviders.size
+        val controllers = arrayOfNulls<MenuItemController>(providerSize)
+        for (i in 0 until providerSize) {
+            controllers[i] = mMenuItemProviders[i].provide(activity)
+        }
+        return controllers
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt
similarity index 63%
rename from src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
rename to src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt
index c3e460d..33d7878 100644
--- a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,17 +14,16 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.actionbarmenu;
+package com.android.deskclock.actionbarmenu
 
-import android.app.Activity;
+import android.app.Activity
 
 /**
- * Provider for a {@link MenuItemController} instances.
+ * Provider for a [MenuItemController] instances.
  */
-public interface MenuItemProvider {
-
+interface MenuItemProvider {
     /**
-     * provides a {@link MenuItemController} that handles menu item.
+     * provides a [MenuItemController] that handles menu item.
      */
-    MenuItemController provide(Activity activity);
-}
+    fun provide(activity: Activity?): MenuItemController?
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java
deleted file mode 100644
index b1622da..0000000
--- a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java
+++ /dev/null
@@ -1,54 +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.actionbarmenu;
-
-import android.app.Activity;
-import android.view.Menu;
-import android.view.MenuItem;
-
-/**
- * {@link MenuItemController} for handling navigation up button in actionbar. It is a special
- * menu item because it's not inflated through menu.xml, and has its own predefined id.
- */
-public final class NavUpMenuItemController implements MenuItemController {
-
-    private final Activity mActivity;
-
-    public NavUpMenuItemController(Activity activity) {
-        mActivity = activity;
-    }
-
-    @Override
-    public int getId() {
-        return android.R.id.home;
-    }
-
-    @Override
-    public void onCreateOptionsItem(Menu menu) {
-        // "Home" option is automatically created by the Toolbar.
-    }
-
-    @Override
-    public void onPrepareOptionsItem(MenuItem item) {
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        mActivity.finish();
-        return true;
-    }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt
new file mode 100644
index 0000000..3640dd4
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.actionbarmenu
+
+import android.app.Activity
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * [MenuItemController] for handling navigation up button in actionbar. It is a special
+ * menu item because it's not inflated through menu.xml, and has its own predefined id.
+ */
+class NavUpMenuItemController(private val activity: Activity) : MenuItemController {
+
+    override val id: Int = android.R.id.home
+
+    override fun onCreateOptionsItem(menu: Menu) {
+        // "Home" option is automatically created by the Toolbar.
+    }
+
+    override fun onPrepareOptionsItem(item: MenuItem) {
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        activity.finish()
+        return true
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java
deleted file mode 100644
index 8975221..0000000
--- a/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java
+++ /dev/null
@@ -1,64 +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.actionbarmenu;
-
-import android.content.Context;
-import android.content.Intent;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import com.android.deskclock.R;
-import com.android.deskclock.ScreensaverActivity;
-import com.android.deskclock.events.Events;
-
-import static android.view.Menu.NONE;
-
-/**
- * {@link MenuItemController} for controlling night mode display.
- */
-public final class NightModeMenuItemController implements MenuItemController {
-
-    private static final int NIGHT_MODE_MENU_RES_ID = R.id.menu_item_night_mode;
-
-    private final Context mContext;
-
-    public NightModeMenuItemController(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    public int getId() {
-        return NIGHT_MODE_MENU_RES_ID;
-    }
-
-    @Override
-    public void onCreateOptionsItem(Menu menu) {
-        menu.add(NONE, NIGHT_MODE_MENU_RES_ID, NONE, R.string.menu_item_night_mode)
-                .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-    }
-
-    @Override
-    public void onPrepareOptionsItem(MenuItem item) {
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        mContext.startActivity(new Intent(mContext, ScreensaverActivity.class)
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock));
-        return true;
-    }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.kt
new file mode 100644
index 0000000..d8f2dd4
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.actionbarmenu
+
+import android.content.Context
+import android.content.Intent
+import android.view.Menu
+import android.view.Menu.NONE
+import android.view.MenuItem
+
+import com.android.deskclock.R
+import com.android.deskclock.ScreensaverActivity
+import com.android.deskclock.events.Events
+
+/**
+ * [MenuItemController] for controlling night mode display.
+ */
+class NightModeMenuItemController(private val context: Context) : MenuItemController {
+
+    override val id: Int = R.id.menu_item_night_mode
+
+    override fun onCreateOptionsItem(menu: Menu) {
+        menu.add(NONE, id, NONE, R.string.menu_item_night_mode)
+                .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+    }
+
+    override fun onPrepareOptionsItem(item: MenuItem) {
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        context.startActivity(Intent(context, ScreensaverActivity::class.java)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock))
+        return true
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.java b/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.java
deleted file mode 100644
index 4d66e2d..0000000
--- a/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.java
+++ /dev/null
@@ -1,86 +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.actionbarmenu;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Activity scoped singleton that manages action bar menus. Each menu item is controlled by a
- * {@link MenuItemController} instance.
- */
-public final class OptionsMenuManager {
-
-    private final List<MenuItemController> mControllers = new ArrayList<>();
-
-    /**
-     * Add one or more {@link MenuItemController} to the actionbar menu.
-     * <p/>
-     * This should be called in {@link Activity#onCreate(Bundle)}.
-     */
-    public OptionsMenuManager addMenuItemController(MenuItemController... controllers) {
-        Collections.addAll(mControllers, controllers);
-        return this;
-    }
-
-    /**
-     * Inflates {@link Menu} for the activity.
-     * <p/>
-     * This method should be called during {@link Activity#onCreateOptionsMenu(Menu)}.
-     */
-    public void onCreateOptionsMenu(Menu menu) {
-        for (MenuItemController controller : mControllers) {
-            controller.onCreateOptionsItem(menu);
-        }
-    }
-
-    /**
-     * Prepares the popup to displays all required menu items.
-     * <p/>
-     * This method should be called during {@link Activity#onPrepareOptionsMenu(Menu)} (Menu)}.
-     */
-    public void onPrepareOptionsMenu(Menu menu) {
-        for (MenuItemController controller : mControllers) {
-            final MenuItem menuItem = menu.findItem(controller.getId());
-            if (menuItem != null) {
-                controller.onPrepareOptionsItem(menuItem);
-            }
-        }
-    }
-
-    /**
-     * Handles click action for a menu item.
-     * <p/>
-     * This method should be called during {@link Activity#onOptionsItemSelected(MenuItem)}.
-     */
-    public boolean onOptionsItemSelected(MenuItem item) {
-        final int itemId = item.getItemId();
-        for (MenuItemController controller : mControllers) {
-            if (controller.getId() == itemId
-                    && controller.onOptionsItemSelected(item)) {
-                return true;
-            }
-        }
-        return false;
-    }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.kt b/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.kt
new file mode 100644
index 0000000..ec44544
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.actionbarmenu
+
+import android.app.Activity
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * Activity scoped singleton that manages action bar menus. Each menu item is controlled by a
+ * [MenuItemController] instance.
+ */
+class OptionsMenuManager {
+
+    private val mControllers: MutableList<MenuItemController?> = ArrayList()
+
+    /**
+     * Add one or more [MenuItemController] to the actionbar menu.
+     *
+     * This should be called in [Activity.onCreate].
+     */
+    fun addMenuItemController(vararg controllers: MenuItemController?): OptionsMenuManager {
+        mControllers.addAll(controllers)
+        return this
+    }
+
+    /**
+     * Inflates [Menu] for the activity.
+     *
+     * This method should be called during [Activity.onCreateOptionsMenu].
+     */
+    fun onCreateOptionsMenu(menu: Menu) {
+        for (controller in mControllers) {
+            controller?.onCreateOptionsItem(menu)
+        }
+    }
+
+    /**
+     * Prepares the popup to displays all required menu items.
+     *
+     * This method should be called during [Activity.onPrepareOptionsMenu] (Menu)}.
+     */
+    fun onPrepareOptionsMenu(menu: Menu) {
+        for (controller in mControllers) {
+            controller?.let {
+                val menuItem: MenuItem? = menu.findItem(controller.id)
+                if (menuItem != null) {
+                    controller.onPrepareOptionsItem(menuItem)
+                }
+            }
+        }
+    }
+
+    /**
+     * Handles click action for a menu item.
+     *
+     * This method should be called during [Activity.onOptionsItemSelected].
+     */
+    fun onOptionsItemSelected(item: MenuItem): Boolean {
+        val itemId: Int = item.getItemId()
+        for (controller in mControllers) {
+            if (controller?.id == itemId && controller.onOptionsItemSelected(item)) {
+                return true
+            }
+        }
+        return false
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java
deleted file mode 100644
index 3777a47..0000000
--- a/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java
+++ /dev/null
@@ -1,127 +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.actionbarmenu;
-
-import android.content.Context;
-import android.os.Bundle;
-import androidx.appcompat.widget.SearchView;
-import androidx.appcompat.widget.SearchView.OnQueryTextListener;
-import android.text.InputType;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-
-import com.android.deskclock.R;
-
-import static android.view.Menu.FIRST;
-import static android.view.Menu.NONE;
-
-/**
- * {@link MenuItemController} for search menu.
- */
-public final class SearchMenuItemController implements MenuItemController {
-
-    private static final String KEY_SEARCH_QUERY = "search_query";
-    private static final String KEY_SEARCH_MODE = "search_mode";
-
-    private static final int SEARCH_MENU_RES_ID = R.id.menu_item_search;
-
-    private final Context mContext;
-    private final SearchView.OnQueryTextListener mQueryListener;
-    private final SearchModeChangeListener mSearchModeChangeListener;
-
-    private String mQuery = "";
-    private boolean mSearchMode;
-
-    public SearchMenuItemController(Context context, OnQueryTextListener queryListener,
-            Bundle savedState) {
-        mContext = context;
-        mSearchModeChangeListener = new SearchModeChangeListener();
-        mQueryListener = queryListener;
-
-        if (savedState != null) {
-            mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE, false);
-            mQuery = savedState.getString(KEY_SEARCH_QUERY, "");
-        }
-    }
-
-    public void saveInstance(Bundle outState) {
-        outState.putString(KEY_SEARCH_QUERY, mQuery);
-        outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
-    }
-
-    @Override
-    public int getId() {
-        return SEARCH_MENU_RES_ID;
-    }
-
-    @Override
-    public void onCreateOptionsItem(Menu menu) {
-        final SearchView searchView = new SearchView(mContext);
-        searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
-        searchView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
-        searchView.setQuery(mQuery, false);
-        searchView.setOnCloseListener(mSearchModeChangeListener);
-        searchView.setOnSearchClickListener(mSearchModeChangeListener);
-        searchView.setOnQueryTextListener(mQueryListener);
-
-        menu.add(NONE, SEARCH_MENU_RES_ID, FIRST, android.R.string.search_go)
-                .setActionView(searchView)
-                .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-
-        if (mSearchMode) {
-            searchView.requestFocus();
-            searchView.setIconified(false);
-        }
-    }
-
-    @Override
-    public void onPrepareOptionsItem(MenuItem item) {
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        // The search view is handled by {@link #mSearchListener}. Skip handling here.
-        return false;
-    }
-
-    public String getQueryText() {
-        return mQuery;
-    }
-
-    public void setQueryText(String query) {
-        mQuery = query;
-    }
-
-    /**
-     * Listener for user actions on search view.
-     */
-    private final class SearchModeChangeListener implements View.OnClickListener,
-            SearchView.OnCloseListener {
-        @Override
-        public void onClick(View v) {
-            mSearchMode = true;
-        }
-
-        @Override
-        public boolean onClose() {
-            mSearchMode = false;
-            return false;
-        }
-    }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.kt
new file mode 100644
index 0000000..7c819f8
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.actionbarmenu
+
+import android.content.Context
+import android.os.Bundle
+import android.text.InputType
+import android.view.Menu
+import android.view.Menu.FIRST
+import android.view.Menu.NONE
+import android.view.MenuItem
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import androidx.appcompat.widget.SearchView
+import androidx.appcompat.widget.SearchView.OnQueryTextListener
+
+import com.android.deskclock.R
+
+/**
+ * [MenuItemController] for search menu.
+ */
+class SearchMenuItemController(
+    private val context: Context,
+    private val queryListener: OnQueryTextListener,
+    savedState: Bundle?
+) : MenuItemController {
+
+    override val id: Int = R.id.menu_item_search
+
+    private val mSearchModeChangeListener: SearchModeChangeListener = SearchModeChangeListener()
+
+    var queryText = ""
+    private var mSearchMode = false
+
+    init {
+        if (savedState != null) {
+            mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE, false)
+            queryText = savedState.getString(KEY_SEARCH_QUERY, "")
+        }
+    }
+
+    fun saveInstance(outState: Bundle) {
+        outState.putString(KEY_SEARCH_QUERY, queryText)
+        outState.putBoolean(KEY_SEARCH_MODE, mSearchMode)
+    }
+
+    override fun onCreateOptionsItem(menu: Menu) {
+        val searchView = SearchView(context)
+        searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI)
+        searchView.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_WORDS)
+        searchView.setQuery(queryText, false)
+        searchView.setOnCloseListener(mSearchModeChangeListener)
+        searchView.setOnSearchClickListener(mSearchModeChangeListener)
+        searchView.setOnQueryTextListener(queryListener)
+
+        menu.add(NONE, id, FIRST, android.R.string.search_go)
+                .setActionView(searchView)
+                .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
+
+        if (mSearchMode) {
+            searchView.requestFocus()
+            searchView.setIconified(false)
+        }
+    }
+
+    override fun onPrepareOptionsItem(item: MenuItem) {
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        // The search view is handled by {@link #mSearchListener}. Skip handling here.
+        return false
+    }
+
+    /**
+     * Listener for user actions on search view.
+     */
+    private inner class SearchModeChangeListener
+        : View.OnClickListener, SearchView.OnCloseListener {
+
+        override fun onClick(v: View?) {
+            mSearchMode = true
+        }
+
+        override fun onClose(): Boolean {
+            mSearchMode = false
+            return false
+        }
+    }
+
+    companion object {
+        private const val KEY_SEARCH_QUERY = "search_query"
+        private const val KEY_SEARCH_MODE = "search_mode"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.java b/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.java
deleted file mode 100644
index ecf442e..0000000
--- a/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.java
+++ /dev/null
@@ -1,65 +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.actionbarmenu;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import com.android.deskclock.R;
-import com.android.deskclock.settings.SettingsActivity;
-
-import static android.view.Menu.NONE;
-
-/**
- * {@link MenuItemController} for settings menu.
- */
-public final class SettingsMenuItemController implements MenuItemController {
-
-    public static final int REQUEST_CHANGE_SETTINGS = 1;
-
-    private static final int SETTING_MENU_RES_ID = R.id.menu_item_settings;
-
-    private final Activity mActivity;
-
-    public SettingsMenuItemController(Activity activity) {
-        mActivity = activity;
-    }
-
-    @Override
-    public int getId() {
-        return SETTING_MENU_RES_ID;
-    }
-
-    @Override
-    public void onCreateOptionsItem(Menu menu) {
-        menu.add(NONE, SETTING_MENU_RES_ID, NONE, R.string.menu_item_settings)
-                .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-    }
-
-    @Override
-    public void onPrepareOptionsItem(MenuItem item) {
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        final Intent settingIntent = new Intent(mActivity, SettingsActivity.class);
-        mActivity.startActivityForResult(settingIntent, REQUEST_CHANGE_SETTINGS);
-        return true;
-    }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.kt
new file mode 100644
index 0000000..7204a24
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.actionbarmenu
+
+import android.app.Activity
+import android.content.Intent
+import android.view.Menu
+import android.view.Menu.NONE
+import android.view.MenuItem
+
+import com.android.deskclock.R
+import com.android.deskclock.settings.SettingsActivity
+
+/**
+ * [MenuItemController] for settings menu.
+ */
+class SettingsMenuItemController(private val activity: Activity) : MenuItemController {
+
+    override val id: Int = R.id.menu_item_settings
+
+    override fun onCreateOptionsItem(menu: Menu) {
+        menu.add(NONE, id, NONE, R.string.menu_item_settings)
+                .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+    }
+
+    override fun onPrepareOptionsItem(item: MenuItem) {
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        val settingIntent = Intent(activity, SettingsActivity::class.java)
+        activity.startActivityForResult(settingIntent, REQUEST_CHANGE_SETTINGS)
+        return true
+    }
+
+    companion object {
+        const val REQUEST_CHANGE_SETTINGS = 1
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmActivity.java b/src/com/android/deskclock/alarms/AlarmActivity.java
deleted file mode 100644
index 88632c7..0000000
--- a/src/com/android/deskclock/alarms/AlarmActivity.java
+++ /dev/null
@@ -1,662 +0,0 @@
-/*
- * 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.
- */
-package com.android.deskclock.alarms;
-
-import android.accessibilityservice.AccessibilityServiceInfo;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.ActivityInfo;
-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 androidx.annotation.NonNull;
-import androidx.core.graphics.ColorUtils;
-import androidx.core.view.animation.PathInterpolatorCompat;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.ImageView;
-import android.widget.TextClock;
-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.widget.CircleView;
-
-import java.util.List;
-
-import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
-
-public class AlarmActivity extends BaseActivity
-        implements View.OnClickListener, View.OnTouchListener {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmActivity");
-
-    private static final TimeInterpolator PULSE_INTERPOLATOR =
-            PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f);
-    private static final TimeInterpolator REVEAL_INTERPOLATOR =
-            PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f);
-
-    private static final int PULSE_DURATION_MILLIS = 1000;
-    private static final int ALARM_BOUNCE_DURATION_MILLIS = 500;
-    private static final int ALERT_REVEAL_DURATION_MILLIS = 500;
-    private static final int ALERT_FADE_DURATION_MILLIS = 500;
-    private static final int ALERT_DISMISS_DELAY_MILLIS = 2000;
-
-    private static final float BUTTON_SCALE_DEFAULT = 0.7f;
-    private static final int BUTTON_DRAWABLE_ALPHA_DEFAULT = 165;
-
-    private final Handler mHandler = new Handler();
-    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            LOGGER.v("Received broadcast: %s", action);
-
-            if (!mAlarmHandled) {
-                switch (action) {
-                    case AlarmService.ALARM_SNOOZE_ACTION:
-                        snooze();
-                        break;
-                    case AlarmService.ALARM_DISMISS_ACTION:
-                        dismiss();
-                        break;
-                    case AlarmService.ALARM_DONE_ACTION:
-                        finish();
-                        break;
-                    default:
-                        LOGGER.i("Unknown broadcast: %s", action);
-                        break;
-                }
-            } else {
-                LOGGER.v("Ignored broadcast: %s", action);
-            }
-        }
-    };
-
-    private final ServiceConnection mConnection = new ServiceConnection() {
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            LOGGER.i("Finished binding to AlarmService");
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
-            LOGGER.i("Disconnected from AlarmService");
-        }
-    };
-
-    private AlarmInstance mAlarmInstance;
-    private boolean mAlarmHandled;
-    private AlarmVolumeButtonBehavior mVolumeBehavior;
-    private int mCurrentHourColor;
-    private boolean mReceiverRegistered;
-    /** Whether the AlarmService is currently bound */
-    private boolean mServiceBound;
-
-    private AccessibilityManager mAccessibilityManager;
-
-    private ViewGroup mAlertView;
-    private TextView mAlertTitleView;
-    private TextView mAlertInfoView;
-
-    private ViewGroup mContentView;
-    private ImageView mAlarmButton;
-    private ImageView mSnoozeButton;
-    private ImageView mDismissButton;
-    private TextView mHintView;
-
-    private ValueAnimator mAlarmAnimator;
-    private ValueAnimator mSnoozeAnimator;
-    private ValueAnimator mDismissAnimator;
-    private ValueAnimator mPulseAnimator;
-
-    private int mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
-
-    @Override
-    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) {
-            // The alarm was deleted before the activity got created, so just finish()
-            LOGGER.e("Error displaying alarm for intent: %s", getIntent());
-            finish();
-            return;
-        } else if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) {
-            LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance);
-            finish();
-            return;
-        }
-
-        LOGGER.i("Displaying alarm for instance: %s", mAlarmInstance);
-
-        // Get the volume/camera button behavior setting
-        mVolumeBehavior = DataModel.getDataModel().getAlarmVolumeButtonBehavior();
-
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
-                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
-                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
-                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
-                | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);
-
-        // Hide navigation bar to minimize accidental tap on Home key
-        hideNavigationBar();
-
-        // Close dialogs and window shade, so this is fully visible
-        sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-
-        // Honor rotation on tablets; fix the orientation on phones.
-        if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
-            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
-        }
-
-        mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
-
-        setContentView(R.layout.alarm_activity);
-
-        mAlertView = (ViewGroup) findViewById(R.id.alert);
-        mAlertTitleView = (TextView) mAlertView.findViewById(R.id.alert_title);
-        mAlertInfoView = (TextView) mAlertView.findViewById(R.id.alert_info);
-
-        mContentView = (ViewGroup) findViewById(R.id.content);
-        mAlarmButton = (ImageView) mContentView.findViewById(R.id.alarm);
-        mSnoozeButton = (ImageView) mContentView.findViewById(R.id.snooze);
-        mDismissButton = (ImageView) mContentView.findViewById(R.id.dismiss);
-        mHintView = (TextView) mContentView.findViewById(R.id.hint);
-
-        final TextView titleView = (TextView) mContentView.findViewById(R.id.title);
-        final TextClock digitalClock = (TextClock) mContentView.findViewById(R.id.digital_clock);
-        final CircleView pulseView = (CircleView) mContentView.findViewById(R.id.pulse);
-
-        titleView.setText(mAlarmInstance.getLabelOrDefault(this));
-        Utils.setTimeFormat(digitalClock, false);
-
-        mCurrentHourColor = ThemeUtils.resolveColor(this, android.R.attr.windowBackground);
-        getWindow().setBackgroundDrawable(new ColorDrawable(mCurrentHourColor));
-
-        mAlarmButton.setOnTouchListener(this);
-        mSnoozeButton.setOnClickListener(this);
-        mDismissButton.setOnClickListener(this);
-
-        mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f);
-        mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE);
-        mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor);
-        mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView,
-                PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.getRadius()),
-                PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR,
-                        ColorUtils.setAlphaComponent(pulseView.getFillColor(), 0)));
-        mPulseAnimator.setDuration(PULSE_DURATION_MILLIS);
-        mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR);
-        mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
-        mPulseAnimator.start();
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        // Re-query for AlarmInstance in case the state has changed externally
-        final long instanceId = AlarmInstance.getId(getIntent().getData());
-        mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId);
-
-        if (mAlarmInstance == null) {
-            LOGGER.i("No alarm instance for instanceId: %d", instanceId);
-            finish();
-            return;
-        }
-
-        // Verify that the alarm is still firing before showing the activity
-        if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) {
-            LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance);
-            finish();
-            return;
-        }
-
-        if (!mReceiverRegistered) {
-            // Register to get the alarm done/snooze/dismiss intent.
-            final IntentFilter filter = new IntentFilter(AlarmService.ALARM_DONE_ACTION);
-            filter.addAction(AlarmService.ALARM_SNOOZE_ACTION);
-            filter.addAction(AlarmService.ALARM_DISMISS_ACTION);
-            registerReceiver(mReceiver, filter);
-            mReceiverRegistered = true;
-        }
-
-        bindAlarmService();
-
-        resetAnimations();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-
-        unbindAlarmService();
-
-        // Skip if register didn't happen to avoid IllegalArgumentException
-        if (mReceiverRegistered) {
-            unregisterReceiver(mReceiver);
-            mReceiverRegistered = false;
-        }
-    }
-
-    @Override
-    public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) {
-        // Do this in dispatch to intercept a few of the system keys.
-        LOGGER.v("dispatchKeyEvent: %s", keyEvent);
-
-        final int keyCode = keyEvent.getKeyCode();
-        switch (keyCode) {
-            // Volume keys and camera keys dismiss the alarm.
-            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) {
-                    switch (mVolumeBehavior) {
-                        case SNOOZE:
-                            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
-                                snooze();
-                            }
-                            return true;
-                        case DISMISS:
-                            if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
-                                dismiss();
-                            }
-                            return true;
-                    }
-                }
-        }
-        return super.dispatchKeyEvent(keyEvent);
-    }
-
-    @Override
-    public void onBackPressed() {
-        // Don't allow back to dismiss.
-    }
-
-    @Override
-    public void onClick(View view) {
-        if (mAlarmHandled) {
-            LOGGER.v("onClick ignored: %s", view);
-            return;
-        }
-        LOGGER.v("onClick: %s", view);
-
-        // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons.
-        if (isAccessibilityEnabled()) {
-            if (view == mSnoozeButton) {
-                snooze();
-            } else if (view == mDismissButton) {
-                dismiss();
-            }
-            return;
-        }
-
-        if (view == mSnoozeButton) {
-            hintSnooze();
-        } else if (view == mDismissButton) {
-            hintDismiss();
-        }
-    }
-
-    @Override
-    public boolean onTouch(View view, MotionEvent event) {
-        if (mAlarmHandled) {
-            LOGGER.v("onTouch ignored: %s", event);
-            return false;
-        }
-
-        final int action = event.getActionMasked();
-        if (action == MotionEvent.ACTION_DOWN) {
-            LOGGER.v("onTouch started: %s", event);
-
-            // Track the pointer that initiated the touch sequence.
-            mInitialPointerIndex = event.getPointerId(event.getActionIndex());
-
-            // Stop the pulse, allowing the last pulse to finish.
-            mPulseAnimator.setRepeatCount(0);
-        } else if (action == MotionEvent.ACTION_CANCEL) {
-            LOGGER.v("onTouch canceled: %s", event);
-
-            // Clear the pointer index.
-            mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
-
-            // Reset everything.
-            resetAnimations();
-        }
-
-        final int actionIndex = event.getActionIndex();
-        if (mInitialPointerIndex == MotionEvent.INVALID_POINTER_ID
-                || mInitialPointerIndex != event.getPointerId(actionIndex)) {
-            // Ignore any pointers other than the initial one, bail early.
-            return true;
-        }
-
-        final int[] contentLocation = {0, 0};
-        mContentView.getLocationOnScreen(contentLocation);
-
-        final float x = event.getRawX() - contentLocation[0];
-        final float y = event.getRawY() - contentLocation[1];
-
-        final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
-        final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
-
-        final float snoozeFraction, dismissFraction;
-        if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            snoozeFraction = getFraction(alarmRight, mSnoozeButton.getLeft(), x);
-            dismissFraction = getFraction(alarmLeft, mDismissButton.getRight(), x);
-        } else {
-            snoozeFraction = getFraction(alarmLeft, mSnoozeButton.getRight(), x);
-            dismissFraction = getFraction(alarmRight, mDismissButton.getLeft(), x);
-        }
-        setAnimatedFractions(snoozeFraction, dismissFraction);
-
-        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
-            LOGGER.v("onTouch ended: %s", event);
-
-            mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
-            if (snoozeFraction == 1.0f) {
-                snooze();
-            } else if (dismissFraction == 1.0f) {
-                dismiss();
-            } else {
-                if (snoozeFraction > 0.0f || dismissFraction > 0.0f) {
-                    // Animate back to the initial state.
-                    AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator);
-                } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) {
-                    // User touched the alarm button, hint the dismiss action.
-                    hintDismiss();
-                }
-
-                // Restart the pulse.
-                mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
-                if (!mPulseAnimator.isStarted()) {
-                    mPulseAnimator.start();
-                }
-            }
-        }
-
-        return true;
-    }
-
-    private void hideNavigationBar() {
-        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
-                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
-    }
-
-    /**
-     * Returns {@code true} if accessibility is enabled, to enable alternate behavior for click
-     * handling, etc.
-     */
-    private boolean isAccessibilityEnabled() {
-        if (mAccessibilityManager == null || !mAccessibilityManager.isEnabled()) {
-            // Accessibility is unavailable or disabled.
-            return false;
-        } else if (mAccessibilityManager.isTouchExplorationEnabled()) {
-            // TalkBack's touch exploration mode is enabled.
-            return true;
-        }
-
-        // Check if "Switch Access" is enabled.
-        final List<AccessibilityServiceInfo> enabledAccessibilityServices =
-                mAccessibilityManager.getEnabledAccessibilityServiceList(FEEDBACK_GENERIC);
-        return !enabledAccessibilityServices.isEmpty();
-    }
-
-    private void hintSnooze() {
-        final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
-        final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
-        final float translationX = Math.max(mSnoozeButton.getLeft() - alarmRight, 0)
-                + Math.min(mSnoozeButton.getRight() - alarmLeft, 0);
-        getAlarmBounceAnimator(translationX, translationX < 0.0f ?
-                R.string.description_direction_left : R.string.description_direction_right).start();
-    }
-
-    private void hintDismiss() {
-        final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
-        final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
-        final float translationX = Math.max(mDismissButton.getLeft() - alarmRight, 0)
-                + Math.min(mDismissButton.getRight() - alarmLeft, 0);
-        getAlarmBounceAnimator(translationX, translationX < 0.0f ?
-                R.string.description_direction_left : R.string.description_direction_right).start();
-    }
-
-    /**
-     * Set animators to initial values and restart pulse on alarm button.
-     */
-    private void resetAnimations() {
-        // Set the animators to their initial values.
-        setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
-        // Restart the pulse.
-        mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
-        if (!mPulseAnimator.isStarted()) {
-            mPulseAnimator.start();
-        }
-    }
-
-    /**
-     * Perform snooze animation and send snooze intent.
-     */
-    private void snooze() {
-        mAlarmHandled = true;
-        LOGGER.v("Snoozed: %s", mAlarmInstance);
-
-        final int colorAccent = ThemeUtils.resolveColor(this, R.attr.colorAccent);
-        setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
-
-        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, colorAccent, colorAccent).start();
-
-        AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */);
-
-        Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock);
-
-        // Unbind here, otherwise alarm will keep ringing until activity finishes.
-        unbindAlarmService();
-    }
-
-    /**
-     * Perform dismiss animation and send dismiss intent.
-     */
-    private void dismiss() {
-        mAlarmHandled = true;
-        LOGGER.v("Dismissed: %s", mAlarmInstance);
-
-        setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */);
-
-        getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */,
-                getString(R.string.alarm_alert_off_text) /* accessibilityText */,
-                Color.WHITE, mCurrentHourColor).start();
-
-        AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance);
-
-        Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock);
-
-        // Unbind here, otherwise alarm will keep ringing until activity finishes.
-        unbindAlarmService();
-    }
-
-    /**
-     * Bind AlarmService if not yet bound.
-     */
-    private void bindAlarmService() {
-        if (!mServiceBound) {
-            final Intent intent = new Intent(this, AlarmService.class);
-            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
-            mServiceBound = true;
-        }
-    }
-
-    /**
-     * Unbind AlarmService if bound.
-     */
-    private void unbindAlarmService() {
-        if (mServiceBound) {
-            unbindService(mConnection);
-            mServiceBound = false;
-        }
-    }
-
-    private void setAnimatedFractions(float snoozeFraction, float dismissFraction) {
-        final float alarmFraction = Math.max(snoozeFraction, dismissFraction);
-        AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction);
-        AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction);
-        AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction);
-    }
-
-    private float getFraction(float x0, float x1, float x) {
-        return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f);
-    }
-
-    private ValueAnimator getButtonAnimator(ImageView button, int tintColor) {
-        return ObjectAnimator.ofPropertyValuesHolder(button,
-                PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f),
-                PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f),
-                PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255),
-                PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA,
-                        BUTTON_DRAWABLE_ALPHA_DEFAULT, 255),
-                PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT,
-                        AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor));
-    }
-
-    private ValueAnimator getAlarmBounceAnimator(float translationX, final int hintResId) {
-        final ValueAnimator bounceAnimator = ObjectAnimator.ofFloat(mAlarmButton,
-                View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f);
-        bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR);
-        bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS);
-        bounceAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animator) {
-                mHintView.setText(hintResId);
-                if (mHintView.getVisibility() != View.VISIBLE) {
-                    mHintView.setVisibility(View.VISIBLE);
-                    ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start();
-                }
-            }
-        });
-        return bounceAnimator;
-    }
-
-    private Animator getAlertAnimator(final View source, final int titleResId,
-            final String infoText, final String accessibilityText, final int revealColor,
-            final int backgroundColor) {
-        final ViewGroup containerView = (ViewGroup) findViewById(android.R.id.content);
-
-        final Rect sourceBounds = new Rect(0, 0, source.getHeight(), source.getWidth());
-        containerView.offsetDescendantRectToMyCoords(source, sourceBounds);
-
-        final int centerX = sourceBounds.centerX();
-        final int centerY = sourceBounds.centerY();
-
-        final int xMax = Math.max(centerX, containerView.getWidth() - centerX);
-        final int yMax = Math.max(centerY, containerView.getHeight() - centerY);
-
-        final float startRadius = Math.max(sourceBounds.width(), sourceBounds.height()) / 2.0f;
-        final float endRadius = (float) Math.sqrt(xMax * xMax + yMax * yMax);
-
-        final CircleView revealView = new CircleView(this)
-                .setCenterX(centerX)
-                .setCenterY(centerY)
-                .setFillColor(revealColor);
-        containerView.addView(revealView);
-
-        // TODO: Fade out source icon over the reveal (like LOLLIPOP version).
-
-        final Animator revealAnimator = ObjectAnimator.ofFloat(
-                revealView, CircleView.RADIUS, startRadius, endRadius);
-        revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS);
-        revealAnimator.setInterpolator(REVEAL_INTERPOLATOR);
-        revealAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animator) {
-                mAlertView.setVisibility(View.VISIBLE);
-                mAlertTitleView.setText(titleResId);
-
-                if (infoText != null) {
-                    mAlertInfoView.setText(infoText);
-                    mAlertInfoView.setVisibility(View.VISIBLE);
-                }
-                mContentView.setVisibility(View.GONE);
-
-                getWindow().setBackgroundDrawable(new ColorDrawable(backgroundColor));
-            }
-        });
-
-        final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
-        fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS);
-        fadeAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                containerView.removeView(revealView);
-            }
-        });
-
-        final AnimatorSet alertAnimator = new AnimatorSet();
-        alertAnimator.play(revealAnimator).before(fadeAnimator);
-        alertAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animator) {
-                mAlertView.announceForAccessibility(accessibilityText);
-                mHandler.postDelayed(new Runnable() {
-                    @Override
-                    public void run() {
-                        finish();
-                    }
-                }, ALERT_DISMISS_DELAY_MILLIS);
-            }
-        });
-
-        return alertAnimator;
-    }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmActivity.kt b/src/com/android/deskclock/alarms/AlarmActivity.kt
new file mode 100644
index 0000000..a4caf1a
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmActivity.kt
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.accessibilityservice.AccessibilityServiceInfo
+import android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.animation.TimeInterpolator
+import android.animation.ValueAnimator
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.ServiceConnection
+import android.content.pm.ActivityInfo
+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.os.Looper
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
+import android.widget.TextClock
+import android.widget.TextView
+import androidx.core.graphics.ColorUtils
+import androidx.core.view.animation.PathInterpolatorCompat
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.LogUtils
+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.provider.ClockContract.InstancesColumns
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.widget.CircleView
+
+import kotlin.math.max
+import kotlin.math.sqrt
+
+class AlarmActivity : BaseActivity(), View.OnClickListener, View.OnTouchListener {
+    private val mHandler: Handler = Handler(Looper.myLooper()!!)
+
+    private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent) {
+            val action: String? = intent.getAction()
+            LOGGER.v("Received broadcast: %s", action)
+
+            if (!mAlarmHandled) {
+                when (action) {
+                    AlarmService.ALARM_SNOOZE_ACTION -> snooze()
+                    AlarmService.ALARM_DISMISS_ACTION -> dismiss()
+                    AlarmService.ALARM_DONE_ACTION -> finish()
+                    else -> LOGGER.i("Unknown broadcast: %s", action)
+                }
+            } else {
+                LOGGER.v("Ignored broadcast: %s", action)
+            }
+        }
+    }
+
+    private val mConnection: ServiceConnection = object : ServiceConnection {
+        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+            LOGGER.i("Finished binding to AlarmService")
+        }
+
+        override fun onServiceDisconnected(name: ComponentName?) {
+            LOGGER.i("Disconnected from AlarmService")
+        }
+    }
+
+    private var mAlarmInstance: AlarmInstance? = null
+    private var mAlarmHandled = false
+    private var mVolumeBehavior: AlarmVolumeButtonBehavior? = null
+    private var mCurrentHourColor = 0
+    private var mReceiverRegistered = false
+    /** Whether the AlarmService is currently bound  */
+    private var mServiceBound = false
+
+    private var mAccessibilityManager: AccessibilityManager? = null
+
+    private lateinit var mAlertView: ViewGroup
+    private lateinit var mAlertTitleView: TextView
+    private lateinit var mAlertInfoView: TextView
+
+    private lateinit var mContentView: ViewGroup
+    private lateinit var mAlarmButton: ImageView
+    private lateinit var mSnoozeButton: ImageView
+    private lateinit var mDismissButton: ImageView
+    private lateinit var mHintView: TextView
+
+    private lateinit var mAlarmAnimator: ValueAnimator
+    private lateinit var mSnoozeAnimator: ValueAnimator
+    private lateinit var mDismissAnimator: ValueAnimator
+    private lateinit var mPulseAnimator: ValueAnimator
+
+    private var mInitialPointerIndex: Int = MotionEvent.INVALID_POINTER_ID
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setVolumeControlStream(AudioManager.STREAM_ALARM)
+        val instanceId = AlarmInstance.getId(getIntent().getData()!!)
+        mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId)
+        if (mAlarmInstance == null) {
+            // The alarm was deleted before the activity got created, so just finish()
+            LOGGER.e("Error displaying alarm for intent: %s", getIntent())
+            finish()
+            return
+        } else if (mAlarmInstance!!.mAlarmState != InstancesColumns.FIRED_STATE) {
+            LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance)
+            finish()
+            return
+        }
+
+        LOGGER.i("Displaying alarm for instance: %s", mAlarmInstance)
+
+        // Get the volume/camera button behavior setting
+        mVolumeBehavior = DataModel.dataModel.alarmVolumeButtonBehavior
+
+        if (Utils.isOOrLater) {
+            setShowWhenLocked(true)
+            setTurnScreenOn(true)
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                    or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
+        } else {
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+                    or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+                    or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                    or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+                    or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
+        }
+
+        // Hide navigation bar to minimize accidental tap on Home key
+        hideNavigationBar()
+
+        // Close dialogs and window shade, so this is fully visible
+        sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+
+        // Honor rotation on tablets; fix the orientation on phones.
+        if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR)
+        }
+
+        mAccessibilityManager = getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager?
+
+        setContentView(R.layout.alarm_activity)
+
+        mAlertView = findViewById(R.id.alert) as ViewGroup
+        mAlertTitleView = mAlertView.findViewById(R.id.alert_title) as TextView
+        mAlertInfoView = mAlertView.findViewById(R.id.alert_info) as TextView
+
+        mContentView = findViewById(R.id.content) as ViewGroup
+        mAlarmButton = mContentView.findViewById(R.id.alarm) as ImageView
+        mSnoozeButton = mContentView.findViewById(R.id.snooze) as ImageView
+        mDismissButton = mContentView.findViewById(R.id.dismiss) as ImageView
+        mHintView = mContentView.findViewById(R.id.hint) as TextView
+
+        val titleView: TextView = mContentView.findViewById(R.id.title) as TextView
+        val digitalClock: TextClock = mContentView.findViewById(R.id.digital_clock) as TextClock
+        val pulseView = mContentView.findViewById(R.id.pulse) as CircleView
+
+        titleView.setText(mAlarmInstance!!.getLabelOrDefault(this))
+        Utils.setTimeFormat(digitalClock, false)
+
+        mCurrentHourColor = ThemeUtils.resolveColor(this, android.R.attr.windowBackground)
+        getWindow().setBackgroundDrawable(ColorDrawable(mCurrentHourColor))
+
+        mAlarmButton.setOnTouchListener(this)
+        mSnoozeButton.setOnClickListener(this)
+        mDismissButton.setOnClickListener(this)
+
+        mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f)
+        mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE)
+        mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor)
+        mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView,
+                PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.radius),
+                PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR,
+                        ColorUtils.setAlphaComponent(pulseView.fillColor, 0)))
+        mPulseAnimator.setDuration(PULSE_DURATION_MILLIS.toLong())
+        mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR)
+        mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE)
+        mPulseAnimator.start()
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        // Re-query for AlarmInstance in case the state has changed externally
+        val instanceId = AlarmInstance.getId(getIntent().getData()!!)
+        mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId)
+
+        if (mAlarmInstance == null) {
+            LOGGER.i("No alarm instance for instanceId: %d", instanceId)
+            finish()
+            return
+        }
+
+        // Verify that the alarm is still firing before showing the activity
+        if (mAlarmInstance!!.mAlarmState != InstancesColumns.FIRED_STATE) {
+            LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance)
+            finish()
+            return
+        }
+
+        if (!mReceiverRegistered) {
+            // Register to get the alarm done/snooze/dismiss intent.
+            val filter = IntentFilter(AlarmService.ALARM_DONE_ACTION)
+            filter.addAction(AlarmService.ALARM_SNOOZE_ACTION)
+            filter.addAction(AlarmService.ALARM_DISMISS_ACTION)
+            registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED)
+            mReceiverRegistered = true
+        }
+        bindAlarmService()
+        resetAnimations()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        unbindAlarmService()
+
+        // Skip if register didn't happen to avoid IllegalArgumentException
+        if (mReceiverRegistered) {
+            unregisterReceiver(mReceiver)
+            mReceiverRegistered = false
+        }
+    }
+
+    override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean {
+        // Do this in dispatch to intercept a few of the system keys.
+        LOGGER.v("dispatchKeyEvent: %s", keyEvent)
+
+        val keyCode: Int = keyEvent.getKeyCode()
+        when (keyCode) {
+            KeyEvent.KEYCODE_VOLUME_UP,
+            KeyEvent.KEYCODE_VOLUME_DOWN,
+            KeyEvent.KEYCODE_VOLUME_MUTE,
+            KeyEvent.KEYCODE_HEADSETHOOK,
+            KeyEvent.KEYCODE_CAMERA,
+            KeyEvent.KEYCODE_FOCUS -> if (!mAlarmHandled) {
+                when (mVolumeBehavior) {
+                    AlarmVolumeButtonBehavior.SNOOZE -> {
+                        if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+                            snooze()
+                        }
+                        return true
+                    }
+                    AlarmVolumeButtonBehavior.DISMISS -> {
+                        if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+                            dismiss()
+                        }
+                        return true
+                    }
+                    AlarmVolumeButtonBehavior.NOTHING -> {
+                    }
+                }
+            }
+        }
+        return super.dispatchKeyEvent(keyEvent)
+    }
+
+    override fun onBackPressed() {
+        // Don't allow back to dismiss.
+    }
+
+    override fun onClick(view: View) {
+        if (mAlarmHandled) {
+            LOGGER.v("onClick ignored: %s", view)
+            return
+        }
+        LOGGER.v("onClick: %s", view)
+
+        // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons.
+        if (isAccessibilityEnabled) {
+            if (view == mSnoozeButton) {
+                snooze()
+            } else if (view == mDismissButton) {
+                dismiss()
+            }
+            return
+        }
+
+        if (view == mSnoozeButton) {
+            hintSnooze()
+        } else if (view == mDismissButton) {
+            hintDismiss()
+        }
+    }
+
+    override fun onTouch(view: View?, event: MotionEvent): Boolean {
+        if (mAlarmHandled) {
+            LOGGER.v("onTouch ignored: %s", event)
+            return false
+        }
+
+        val action: Int = event.getActionMasked()
+        if (action == MotionEvent.ACTION_DOWN) {
+            LOGGER.v("onTouch started: %s", event)
+
+            // Track the pointer that initiated the touch sequence.
+            mInitialPointerIndex = event.getPointerId(event.getActionIndex())
+
+            // Stop the pulse, allowing the last pulse to finish.
+            mPulseAnimator.setRepeatCount(0)
+        } else if (action == MotionEvent.ACTION_CANCEL) {
+            LOGGER.v("onTouch canceled: %s", event)
+
+            // Clear the pointer index.
+            mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID
+
+            // Reset everything.
+            resetAnimations()
+        }
+
+        val actionIndex: Int = event.getActionIndex()
+        if (mInitialPointerIndex == MotionEvent.INVALID_POINTER_ID ||
+                mInitialPointerIndex != event.getPointerId(actionIndex)) {
+            // Ignore any pointers other than the initial one, bail early.
+            return true
+        }
+
+        val contentLocation = intArrayOf(0, 0)
+        mContentView.getLocationOnScreen(contentLocation)
+
+        val x: Float = event.getRawX() - contentLocation[0]
+        val y: Float = event.getRawY() - contentLocation[1]
+
+        val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft()
+        val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight()
+
+        val snoozeFraction: Float
+        val dismissFraction: Float
+        if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+            snoozeFraction =
+                    getFraction(alarmRight.toFloat(), mSnoozeButton.getLeft().toFloat(), x)
+            dismissFraction =
+                    getFraction(alarmLeft.toFloat(), mDismissButton.getRight().toFloat(), x)
+        } else {
+            snoozeFraction = getFraction(alarmLeft.toFloat(), mSnoozeButton.getRight().toFloat(), x)
+            dismissFraction =
+                    getFraction(alarmRight.toFloat(), mDismissButton.getLeft().toFloat(), x)
+        }
+        setAnimatedFractions(snoozeFraction, dismissFraction)
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
+            LOGGER.v("onTouch ended: %s", event)
+
+            mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID
+            if (snoozeFraction == 1.0f) {
+                snooze()
+            } else if (dismissFraction == 1.0f) {
+                dismiss()
+            } else {
+                if (snoozeFraction > 0.0f || dismissFraction > 0.0f) {
+                    // Animate back to the initial state.
+                    AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator)
+                } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) {
+                    // User touched the alarm button, hint the dismiss action.
+                    hintDismiss()
+                }
+
+                // Restart the pulse.
+                mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE)
+                if (!mPulseAnimator.isStarted()) {
+                    mPulseAnimator.start()
+                }
+            }
+        }
+
+        return true
+    }
+
+    private fun hideNavigationBar() {
+        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+    }
+
+    /**
+     * Returns `true` if accessibility is enabled, to enable alternate behavior for click
+     * handling, etc.
+     */
+    private val isAccessibilityEnabled: Boolean
+        get() {
+            if (mAccessibilityManager == null || !mAccessibilityManager!!.isEnabled()) {
+                // Accessibility is unavailable or disabled.
+                return false
+            } else if (mAccessibilityManager!!.isTouchExplorationEnabled()) {
+                // TalkBack's touch exploration mode is enabled.
+                return true
+            }
+
+            // Check if "Switch Access" is enabled.
+            val enabledAccessibilityServices: List<AccessibilityServiceInfo> =
+                    mAccessibilityManager!!.getEnabledAccessibilityServiceList(FEEDBACK_GENERIC)
+            return !enabledAccessibilityServices.isEmpty()
+        }
+
+    private fun hintSnooze() {
+        val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft()
+        val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight()
+        val translationX = (Math.max(mSnoozeButton.getLeft() - alarmRight, 0) +
+                Math.min(mSnoozeButton.getRight() - alarmLeft, 0)).toFloat()
+        getAlarmBounceAnimator(translationX, if (translationX < 0.0f) {
+            R.string.description_direction_left
+        } else {
+            R.string.description_direction_right
+        }).start()
+    }
+
+    private fun hintDismiss() {
+        val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft()
+        val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight()
+        val translationX = (Math.max(mDismissButton.getLeft() - alarmRight, 0) +
+                Math.min(mDismissButton.getRight() - alarmLeft, 0)).toFloat()
+        getAlarmBounceAnimator(translationX, if (translationX < 0.0f) {
+            R.string.description_direction_left
+        } else {
+            R.string.description_direction_right
+        }).start()
+    }
+
+    /**
+     * Set animators to initial values and restart pulse on alarm button.
+     */
+    private fun resetAnimations() {
+        // Set the animators to their initial values.
+        setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */)
+        // Restart the pulse.
+        mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE)
+        if (!mPulseAnimator.isStarted()) {
+            mPulseAnimator.start()
+        }
+    }
+
+    /**
+     * Perform snooze animation and send snooze intent.
+     */
+    private fun snooze() {
+        mAlarmHandled = true
+        LOGGER.v("Snoozed: %s", mAlarmInstance)
+
+        val colorAccent = ThemeUtils.resolveColor(this, R.attr.colorAccent)
+        setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */)
+
+        val snoozeMinutes = DataModel.dataModel.snoozeLength
+        val infoText: String = getResources().getQuantityString(
+                R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes)
+        val accessibilityText: String = getResources().getQuantityString(
+                R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes)
+
+        getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText,
+                accessibilityText, colorAccent, colorAccent).start()
+
+        AlarmStateManager.setSnoozeState(this, mAlarmInstance!!, false /* showToast */)
+
+        Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock)
+
+        // Unbind here, otherwise alarm will keep ringing until activity finishes.
+        unbindAlarmService()
+    }
+
+    /**
+     * Perform dismiss animation and send dismiss intent.
+     */
+    private fun dismiss() {
+        mAlarmHandled = true
+        LOGGER.v("Dismissed: %s", mAlarmInstance)
+
+        setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */)
+
+        getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */,
+                getString(R.string.alarm_alert_off_text) /* accessibilityText */,
+                Color.WHITE, mCurrentHourColor).start()
+
+        AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance!!)
+
+        Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock)
+
+        // Unbind here, otherwise alarm will keep ringing until activity finishes.
+        unbindAlarmService()
+    }
+
+    /**
+     * Bind AlarmService if not yet bound.
+     */
+    private fun bindAlarmService() {
+        if (!mServiceBound) {
+            val intent = Intent(this, AlarmService::class.java)
+            bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
+            mServiceBound = true
+        }
+    }
+
+    /**
+     * Unbind AlarmService if bound.
+     */
+    private fun unbindAlarmService() {
+        if (mServiceBound) {
+            unbindService(mConnection)
+            mServiceBound = false
+        }
+    }
+
+    private fun setAnimatedFractions(snoozeFraction: Float, dismissFraction: Float) {
+        val alarmFraction = Math.max(snoozeFraction, dismissFraction)
+        AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction)
+        AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction)
+        AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction)
+    }
+
+    private fun getFraction(x0: Float, x1: Float, x: Float): Float {
+        return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f)
+    }
+
+    private fun getButtonAnimator(button: ImageView?, tintColor: Int): ValueAnimator {
+        return ObjectAnimator.ofPropertyValuesHolder(button,
+                PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f),
+                PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f),
+                PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255),
+                PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA,
+                        BUTTON_DRAWABLE_ALPHA_DEFAULT, 255),
+                PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT,
+                        AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor))
+    }
+
+    private fun getAlarmBounceAnimator(translationX: Float, hintResId: Int): ValueAnimator {
+        val bounceAnimator: ValueAnimator = ObjectAnimator.ofFloat(mAlarmButton,
+                View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f)
+        bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR)
+        bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS.toLong())
+        bounceAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationStart(animator: Animator) {
+                mHintView.setText(hintResId)
+                if (mHintView.getVisibility() != View.VISIBLE) {
+                    mHintView.setVisibility(View.VISIBLE)
+                    ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start()
+                }
+            }
+        })
+        return bounceAnimator
+    }
+
+    private fun getAlertAnimator(
+        source: View,
+        titleResId: Int,
+        infoText: String?,
+        accessibilityText: String,
+        revealColor: Int,
+        backgroundColor: Int
+    ): Animator {
+        val containerView: ViewGroup = findViewById(android.R.id.content) as ViewGroup
+
+        val sourceBounds = Rect(0, 0, source.getHeight(), source.getWidth())
+        containerView.offsetDescendantRectToMyCoords(source, sourceBounds)
+
+        val centerX: Int = sourceBounds.centerX()
+        val centerY: Int = sourceBounds.centerY()
+
+        val xMax = max(centerX, containerView.getWidth() - centerX)
+        val yMax = max(centerY, containerView.getHeight() - centerY)
+
+        val startRadius: Float = max(sourceBounds.width(), sourceBounds.height()) / 2.0f
+        val endRadius = sqrt(xMax * xMax + yMax * yMax.toDouble()).toFloat()
+
+        val revealView = CircleView(this)
+                .setCenterX(centerX.toFloat())
+                .setCenterY(centerY.toFloat())
+                .setFillColor(revealColor)
+        containerView.addView(revealView)
+
+        // TODO: Fade out source icon over the reveal (like LOLLIPOP version).
+
+        val revealAnimator: Animator = ObjectAnimator.ofFloat(
+                revealView, CircleView.RADIUS, startRadius, endRadius)
+        revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS.toLong())
+        revealAnimator.setInterpolator(REVEAL_INTERPOLATOR)
+        revealAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animator: Animator) {
+                mAlertView.setVisibility(View.VISIBLE)
+                mAlertTitleView.setText(titleResId)
+                if (infoText != null) {
+                    mAlertInfoView.setText(infoText)
+                    mAlertInfoView.setVisibility(View.VISIBLE)
+                }
+                mContentView.setVisibility(View.GONE)
+                getWindow().setBackgroundDrawable(ColorDrawable(backgroundColor))
+            }
+        })
+
+        val fadeAnimator: ValueAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f)
+        fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS.toLong())
+        fadeAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                containerView.removeView(revealView)
+            }
+        })
+
+        val alertAnimator = AnimatorSet()
+        alertAnimator.play(revealAnimator).before(fadeAnimator)
+        alertAnimator.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animator: Animator) {
+                mAlertView.announceForAccessibility(accessibilityText)
+                mHandler.postDelayed(Runnable { finish() }, ALERT_DISMISS_DELAY_MILLIS.toLong())
+            }
+        })
+
+        return alertAnimator
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("AlarmActivity")
+
+        private val PULSE_INTERPOLATOR: TimeInterpolator =
+                PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f)
+        private val REVEAL_INTERPOLATOR: TimeInterpolator =
+                PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f)
+
+        private const val PULSE_DURATION_MILLIS = 1000
+        private const val ALARM_BOUNCE_DURATION_MILLIS = 500
+        private const val ALERT_REVEAL_DURATION_MILLIS = 500
+        private const val ALERT_FADE_DURATION_MILLIS = 500
+        private const val ALERT_DISMISS_DELAY_MILLIS = 2000
+
+        private const val BUTTON_SCALE_DEFAULT = 0.7f
+        private const val BUTTON_DRAWABLE_ALPHA_DEFAULT = 165
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmKlaxon.java b/src/com/android/deskclock/alarms/AlarmKlaxon.java
deleted file mode 100644
index a162423..0000000
--- a/src/com/android/deskclock/alarms/AlarmKlaxon.java
+++ /dev/null
@@ -1,93 +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.alarms;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.os.Build;
-import android.os.Vibrator;
-
-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;
-
-/**
- * Manages playing alarm ringtones and vibrating the device.
- */
-final class AlarmKlaxon {
-
-    private static final long[] VIBRATE_PATTERN = {500, 500};
-
-    private static boolean sStarted = false;
-    private static AsyncRingtonePlayer sAsyncRingtonePlayer;
-
-    private AlarmKlaxon() {}
-
-    public static void stop(Context context) {
-        if (sStarted) {
-            LogUtils.v("AlarmKlaxon.stop()");
-            sStarted = false;
-            getAsyncRingtonePlayer(context).stop();
-            ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).cancel();
-        }
-    }
-
-    public static void start(Context context, AlarmInstance instance) {
-        // Make sure we are stopped before starting
-        stop(context);
-        LogUtils.v("AlarmKlaxon.start()");
-
-        if (!AlarmInstance.NO_RINGTONE_URI.equals(instance.mRingtone)) {
-            final long crescendoDuration = DataModel.getDataModel().getAlarmCrescendoDuration();
-            getAsyncRingtonePlayer(context).play(instance.mRingtone, crescendoDuration);
-        }
-
-        if (instance.mVibrate) {
-            final Vibrator vibrator = getVibrator(context);
-            if (Utils.isLOrLater()) {
-                vibrateLOrLater(vibrator);
-            } else {
-                vibrator.vibrate(VIBRATE_PATTERN, 0);
-            }
-        }
-
-        sStarted = true;
-    }
-
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    private static void vibrateLOrLater(Vibrator vibrator) {
-        vibrator.vibrate(VIBRATE_PATTERN, 0, new AudioAttributes.Builder()
-                .setUsage(AudioAttributes.USAGE_ALARM)
-                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                .build());
-    }
-
-    private static Vibrator getVibrator(Context context) {
-        return ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE));
-    }
-
-    private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
-        if (sAsyncRingtonePlayer == null) {
-            sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
-        }
-
-        return sAsyncRingtonePlayer;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmKlaxon.kt b/src/com/android/deskclock/alarms/AlarmKlaxon.kt
new file mode 100644
index 0000000..8ccf4fb
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmKlaxon.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.TargetApi
+import android.content.Context
+import android.media.AudioAttributes
+import android.os.Build
+import android.os.Vibrator
+
+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.provider.ClockContract.AlarmSettingColumns
+
+/**
+ * Manages playing alarm ringtones and vibrating the device.
+ */
+internal object AlarmKlaxon {
+
+    private val VIBRATE_PATTERN = longArrayOf(500, 500)
+
+    private var sStarted = false
+    private var sAsyncRingtonePlayer: AsyncRingtonePlayer? = null
+
+    @JvmStatic
+    fun stop(context: Context) {
+        if (sStarted) {
+            LogUtils.v("AlarmKlaxon.stop()")
+            sStarted = false
+            getAsyncRingtonePlayer(context)!!.stop()
+            (context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).cancel()
+        }
+    }
+
+    @JvmStatic
+    fun start(context: Context, instance: AlarmInstance) {
+        // Make sure we are stopped before starting
+        stop(context)
+        LogUtils.v("AlarmKlaxon.start()")
+
+        if (!AlarmSettingColumns.NO_RINGTONE_URI.equals(instance.mRingtone)) {
+            val crescendoDuration = DataModel.dataModel.alarmCrescendoDuration
+            getAsyncRingtonePlayer(context)!!.play(instance.mRingtone, crescendoDuration)
+        }
+
+        if (instance.mVibrate) {
+            val vibrator: Vibrator = getVibrator(context)
+            if (Utils.isLOrLater) {
+                vibrateLOrLater(vibrator)
+            } else {
+                vibrator.vibrate(VIBRATE_PATTERN, 0)
+            }
+        }
+
+        sStarted = true
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun vibrateLOrLater(vibrator: Vibrator) {
+        vibrator.vibrate(VIBRATE_PATTERN, 0, AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_ALARM)
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                .build())
+    }
+
+    private fun getVibrator(context: Context): Vibrator {
+        return context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+    }
+
+    @Synchronized
+    private fun getAsyncRingtonePlayer(context: Context): AsyncRingtonePlayer? {
+        if (sAsyncRingtonePlayer == null) {
+            sAsyncRingtonePlayer = AsyncRingtonePlayer(context.getApplicationContext())
+        }
+
+        return sAsyncRingtonePlayer
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmNotifications.java b/src/com/android/deskclock/alarms/AlarmNotifications.java
deleted file mode 100644
index 5dc44c9..0000000
--- a/src/com/android/deskclock/alarms/AlarmNotifications.java
+++ /dev/null
@@ -1,580 +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.alarms;
-
-import android.annotation.TargetApi;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Build;
-import android.service.notification.StatusBarNotification;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Locale;
-import java.util.Objects;
-
-final class AlarmNotifications {
-    static final String EXTRA_NOTIFICATION_ID = "extra_notification_id";
-
-    /**
-     * Notification channel containing all low priority notifications.
-     */
-    private static final String ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID =
-            "alarmLowPriorityNotification";
-
-    /**
-     * Notification channel containing all high priority notifications.
-     */
-    private static final String ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID =
-            "alarmHighPriorityNotification";
-
-    /**
-     * Notification channel containing all snooze notifications.
-     */
-    private static final String ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID =
-            "alarmSnoozeNotification";
-
-    /**
-     * Notification channel containing all missed notifications.
-     */
-    private static final String ALARM_MISSED_NOTIFICATION_CHANNEL_ID =
-            "alarmMissedNotification";
-
-    /**
-     * Notification channel containing all alarm notifications.
-     */
-    private static final String ALARM_NOTIFICATION_CHANNEL_ID = "alarmNotification";
-
-    /**
-     * Formats times such that chronological order and lexicographical order agree.
-     */
-    private static final DateFormat SORT_KEY_FORMAT =
-            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
-
-    /**
-     * This value is coordinated with group ids from
-     * {@link com.android.deskclock.data.NotificationModel}
-     */
-    private static final String UPCOMING_GROUP_KEY = "1";
-
-    /**
-     * This value is coordinated with group ids from
-     * {@link com.android.deskclock.data.NotificationModel}
-     */
-    private static final String MISSED_GROUP_KEY = "4";
-
-    /**
-     * This value is coordinated with notification ids from
-     * {@link com.android.deskclock.data.NotificationModel}
-     */
-    private static final int ALARM_GROUP_NOTIFICATION_ID = Integer.MAX_VALUE - 4;
-
-    /**
-     * This value is coordinated with notification ids from
-     * {@link com.android.deskclock.data.NotificationModel}
-     */
-    private static final int ALARM_GROUP_MISSED_NOTIFICATION_ID = Integer.MAX_VALUE - 5;
-
-    /**
-     * 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 builder = new NotificationCompat.Builder(
-                 context, ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID)
-                         .setShowWhen(false)
-                        .setContentTitle(context.getString(
-                                R.string.alarm_alert_predismiss_title))
-                        .setContentText(AlarmUtils.getAlarmText(
-                                context, instance, true /* includeLabel */))
-                        .setColor(ContextCompat.getColor(context, R.color.default_background))
-                        .setSmallIcon(R.drawable.stat_notify_alarm)
-                        .setAutoCancel(false)
-                        .setSortKey(createSortKey(instance))
-                        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
-                        .setCategory(NotificationCompat.CATEGORY_ALARM)
-                        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                        .setLocalOnly(true);
-
-        if (Utils.isNOrLater()) {
-            builder.setGroup(UPCOMING_GROUP_KEY);
-        }
-
-        // Setup up hide notification
-        Intent hideIntent = AlarmStateManager.createStateChangeIntent(context,
-                AlarmStateManager.ALARM_DELETE_TAG, instance,
-                AlarmInstance.HIDE_NOTIFICATION_STATE);
-        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);
-        builder.addAction(R.drawable.ic_alarm_off_24dp,
-                context.getString(R.string.alarm_alert_dismiss_text),
-                PendingIntent.getService(context, id,
-                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        // Setup content action if instance is owned by alarm
-        Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
-        builder.setContentIntent(PendingIntent.getActivity(context, id,
-                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            nm.createNotificationChannel(channel);
-        }
-        final Notification notification = builder.build();
-        nm.notify(id, notification);
-        updateUpcomingAlarmGroupNotification(context, -1, notification);
-    }
-
-    static synchronized void showHighPriorityNotification(Context context,
-            AlarmInstance instance) {
-        LogUtils.v("Displaying high priority notification for alarm instance: " + instance.mId);
-
-        NotificationCompat.Builder builder = new NotificationCompat.Builder(
-                context, ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID)
-                        .setShowWhen(false)
-                        .setContentTitle(context.getString(
-                                R.string.alarm_alert_predismiss_title))
-                        .setContentText(AlarmUtils.getAlarmText(
-                                context, instance, true /* includeLabel */))
-                        .setColor(ContextCompat.getColor(context, R.color.default_background))
-                        .setSmallIcon(R.drawable.stat_notify_alarm)
-                        .setAutoCancel(false)
-                        .setSortKey(createSortKey(instance))
-                        .setPriority(NotificationCompat.PRIORITY_HIGH)
-                        .setCategory(NotificationCompat.CATEGORY_ALARM)
-                        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                        .setLocalOnly(true);
-
-        if (Utils.isNOrLater()) {
-            builder.setGroup(UPCOMING_GROUP_KEY);
-        }
-
-        // Setup up dismiss action
-        Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
-                AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.PREDISMISSED_STATE);
-        final int id = instance.hashCode();
-        builder.addAction(R.drawable.ic_alarm_off_24dp,
-                context.getString(R.string.alarm_alert_dismiss_text),
-                PendingIntent.getService(context, id,
-                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        // Setup content action if instance is owned by alarm
-        Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
-        builder.setContentIntent(PendingIntent.getActivity(context, id,
-                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            nm.createNotificationChannel(channel);
-        }
-        final Notification notification = builder.build();
-        nm.notify(id, notification);
-        updateUpcomingAlarmGroupNotification(context, -1, notification);
-    }
-
-    @TargetApi(Build.VERSION_CODES.N)
-    private static boolean isGroupSummary(Notification n) {
-        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,
-            int canceledNotificationId, Notification postedNotification) {
-        final NotificationManager nm =
-                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-        final StatusBarNotification[] notifications = nm.getActiveNotifications();
-        Notification firstActiveNotification = postedNotification;
-        for (StatusBarNotification statusBarNotification : notifications) {
-            final Notification n = statusBarNotification.getNotification();
-            if (!isGroupSummary(n)
-                    && group.equals(n.getGroup())
-                    && statusBarNotification.getId() != canceledNotificationId) {
-                if (firstActiveNotification == null
-                        || n.getSortKey().compareTo(firstActiveNotification.getSortKey()) < 0) {
-                    firstActiveNotification = n;
-                }
-            }
-        }
-        return firstActiveNotification;
-    }
-
-    @TargetApi(Build.VERSION_CODES.N)
-    private static Notification getActiveGroupSummaryNotification(Context context, String group) {
-        final NotificationManager nm =
-                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-        final StatusBarNotification[] notifications = nm.getActiveNotifications();
-        for (StatusBarNotification statusBarNotification : notifications) {
-            final Notification n = statusBarNotification.getNotification();
-            if (isGroupSummary(n) && group.equals(n.getGroup())) {
-                return n;
-            }
-        }
-        return null;
-    }
-
-    private static void updateUpcomingAlarmGroupNotification(Context context,
-            int canceledNotificationId, Notification postedNotification) {
-        if (!Utils.isNOrLater()) {
-            return;
-        }
-
-        final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    ALARM_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            nm.createNotificationChannel(channel);
-        }
-
-        final Notification firstUpcoming = getFirstActiveNotification(context, UPCOMING_GROUP_KEY,
-                canceledNotificationId, postedNotification);
-        if (firstUpcoming == null) {
-            nm.cancel(ALARM_GROUP_NOTIFICATION_ID);
-            return;
-        }
-
-        Notification summary = getActiveGroupSummaryNotification(context, UPCOMING_GROUP_KEY);
-        if (summary == null
-                || !Objects.equals(summary.contentIntent, firstUpcoming.contentIntent)) {
-            summary = new NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
-                    .setShowWhen(false)
-                    .setContentIntent(firstUpcoming.contentIntent)
-                    .setColor(ContextCompat.getColor(context, R.color.default_background))
-                    .setSmallIcon(R.drawable.stat_notify_alarm)
-                    .setGroup(UPCOMING_GROUP_KEY)
-                    .setGroupSummary(true)
-                    .setPriority(NotificationCompat.PRIORITY_HIGH)
-                    .setCategory(NotificationCompat.CATEGORY_ALARM)
-                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                    .setLocalOnly(true)
-                    .build();
-            nm.notify(ALARM_GROUP_NOTIFICATION_ID, summary);
-        }
-    }
-
-    private static void updateMissedAlarmGroupNotification(Context context,
-            int canceledNotificationId, Notification postedNotification) {
-        if (!Utils.isNOrLater()) {
-            return;
-        }
-
-        final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    ALARM_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            nm.createNotificationChannel(channel);
-        }
-
-        final Notification firstMissed = getFirstActiveNotification(context, MISSED_GROUP_KEY,
-                canceledNotificationId, postedNotification);
-        if (firstMissed == null) {
-            nm.cancel(ALARM_GROUP_MISSED_NOTIFICATION_ID);
-            return;
-        }
-
-        Notification summary = getActiveGroupSummaryNotification(context, MISSED_GROUP_KEY);
-        if (summary == null
-                || !Objects.equals(summary.contentIntent, firstMissed.contentIntent)) {
-            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-                NotificationChannel channel = new NotificationChannel(
-                        ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
-                        context.getString(R.string.default_label),
-                        NotificationManagerCompat.IMPORTANCE_DEFAULT);
-                nm.createNotificationChannel(channel);
-            }
-            summary = new NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
-                    .setShowWhen(false)
-                    .setContentIntent(firstMissed.contentIntent)
-                    .setColor(ContextCompat.getColor(context, R.color.default_background))
-                    .setSmallIcon(R.drawable.stat_notify_alarm)
-                    .setGroup(MISSED_GROUP_KEY)
-                    .setGroupSummary(true)
-                    .setPriority(NotificationCompat.PRIORITY_HIGH)
-                    .setCategory(NotificationCompat.CATEGORY_ALARM)
-                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                    .setLocalOnly(true)
-                    .build();
-            nm.notify(ALARM_GROUP_MISSED_NOTIFICATION_ID, summary);
-        }
-    }
-
-    static synchronized void showSnoozeNotification(Context context,
-            AlarmInstance instance) {
-        LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId);
-
-        NotificationCompat.Builder builder = new NotificationCompat.Builder(
-                context, ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID)
-                        .setShowWhen(false)
-                        .setContentTitle(instance.getLabelOrDefault(context))
-                        .setContentText(context.getString(R.string.alarm_alert_snooze_until,
-                                AlarmUtils.getFormattedTime(context, instance.getAlarmTime())))
-                        .setColor(ContextCompat.getColor(context, R.color.default_background))
-                        .setSmallIcon(R.drawable.stat_notify_alarm)
-                        .setAutoCancel(false)
-                        .setSortKey(createSortKey(instance))
-                        .setPriority(NotificationCompat.PRIORITY_MAX)
-                        .setCategory(NotificationCompat.CATEGORY_ALARM)
-                        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                        .setLocalOnly(true);
-
-        if (Utils.isNOrLater()) {
-            builder.setGroup(UPCOMING_GROUP_KEY);
-        }
-
-        // Setup up dismiss action
-        Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
-                AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
-        final int id = instance.hashCode();
-        builder.addAction(R.drawable.ic_alarm_off_24dp,
-                context.getString(R.string.alarm_alert_dismiss_text),
-                PendingIntent.getService(context, id,
-                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        // Setup content action if instance is owned by alarm
-        Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
-        builder.setContentIntent(PendingIntent.getActivity(context, id,
-                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            nm.createNotificationChannel(channel);
-        }
-        final Notification notification = builder.build();
-        nm.notify(id, notification);
-        updateUpcomingAlarmGroupNotification(context, -1, notification);
-    }
-
-    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 builder = new NotificationCompat.Builder(
-                context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
-                        .setShowWhen(false)
-                        .setContentTitle(context.getString(R.string.alarm_missed_title))
-                        .setContentText(instance.mLabel.isEmpty() ? alarmTime :
-                                context.getString(R.string.alarm_missed_text, alarmTime, label))
-                        .setColor(ContextCompat.getColor(context, R.color.default_background))
-                        .setSortKey(createSortKey(instance))
-                        .setSmallIcon(R.drawable.stat_notify_alarm)
-                        .setPriority(NotificationCompat.PRIORITY_HIGH)
-                        .setCategory(NotificationCompat.CATEGORY_ALARM)
-                        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                        .setLocalOnly(true);
-
-        if (Utils.isNOrLater()) {
-            builder.setGroup(MISSED_GROUP_KEY);
-        }
-
-        final int id = instance.hashCode();
-
-        // Setup dismiss intent
-        Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
-                AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
-        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, id);
-        showAndDismiss.setAction(AlarmStateManager.SHOW_AND_DISMISS_ALARM_ACTION);
-        builder.setContentIntent(PendingIntent.getBroadcast(context, id,
-                showAndDismiss, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            nm.createNotificationChannel(channel);
-        }
-        final Notification notification = builder.build();
-        nm.notify(id, notification);
-        updateMissedAlarmGroupNotification(context, -1, notification);
-    }
-
-    static synchronized void showAlarmNotification(Service service, AlarmInstance instance) {
-        LogUtils.v("Displaying alarm notification for alarm instance: " + instance.mId);
-
-        Resources resources = service.getResources();
-        NotificationCompat.Builder notification = new NotificationCompat.Builder(
-                service, ALARM_NOTIFICATION_CHANNEL_ID)
-                        .setContentTitle(instance.getLabelOrDefault(service))
-                        .setContentText(AlarmUtils.getFormattedTime(
-                                service, instance.getAlarmTime()))
-                        .setColor(ContextCompat.getColor(service, R.color.default_background))
-                        .setSmallIcon(R.drawable.stat_notify_alarm)
-                        .setOngoing(true)
-                        .setAutoCancel(false)
-                        .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
-                        .setWhen(0)
-                        .setCategory(NotificationCompat.CATEGORY_ALARM)
-                        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                        .setLocalOnly(true);
-
-        // Setup Snooze Action
-        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,
-                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);
-
-        // Setup Dismiss Action
-        Intent dismissIntent = AlarmStateManager.createStateChangeIntent(service,
-                AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
-        dismissIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true);
-        PendingIntent dismissPendingIntent = PendingIntent.getService(service,
-                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);
-
-        // Setup Content Action
-        Intent contentIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
-                instance.mId);
-        notification.setContentIntent(PendingIntent.getActivity(service,
-                ALARM_FIRING_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
-        // Setup fullscreen intent
-        Intent fullScreenIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
-                instance.mId);
-        // set action, so we can be different then content pending intent
-        fullScreenIntent.setAction("fullscreen_activity");
-        fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
-                Intent.FLAG_ACTIVITY_NO_USER_ACTION);
-        notification.setFullScreenIntent(PendingIntent.getActivity(service,
-                ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT),
-                true);
-        notification.setPriority(NotificationCompat.PRIORITY_MAX);
-
-        clearNotification(service, instance);
-        service.startForeground(ALARM_FIRING_NOTIFICATION_ID, notification.build());
-    }
-
-    static synchronized void clearNotification(Context context, AlarmInstance instance) {
-        LogUtils.v("Clearing notifications for alarm instance: " + instance.mId);
-        NotificationManagerCompat nm = NotificationManagerCompat.from(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.
-     */
-    static void updateNotification(Context context, AlarmInstance instance) {
-        switch (instance.mAlarmState) {
-            case AlarmInstance.LOW_NOTIFICATION_STATE:
-                showLowPriorityNotification(context, instance);
-                break;
-            case AlarmInstance.HIGH_NOTIFICATION_STATE:
-                showHighPriorityNotification(context, instance);
-                break;
-            case AlarmInstance.SNOOZE_STATE:
-                showSnoozeNotification(context, instance);
-                break;
-            case AlarmInstance.MISSED_STATE:
-                showMissedNotification(context, instance);
-                break;
-            default:
-                LogUtils.d("No notification to update");
-        }
-    }
-
-    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)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-    }
-
-    /**
-     * Alarm notifications are sorted chronologically. Missed alarms are sorted chronologically
-     * <strong>after</strong> all upcoming/snoozed alarms by including the "MISSED" prefix on the
-     * sort key.
-     *
-     * @param instance the alarm instance for which the notification is generated
-     * @return the sort key that specifies the order of this alarm notification
-     */
-    private static String createSortKey(AlarmInstance instance) {
-        final String timeKey = SORT_KEY_FORMAT.format(instance.getAlarmTime().getTime());
-        final boolean missedAlarm = instance.mAlarmState == AlarmInstance.MISSED_STATE;
-        return missedAlarm ? ("MISSED " + timeKey) : timeKey;
-    }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmNotifications.kt b/src/com/android/deskclock/alarms/AlarmNotifications.kt
new file mode 100644
index 0000000..ed7d8ea
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmNotifications.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.TargetApi
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.service.notification.StatusBarNotification
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.DeskClock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+internal object AlarmNotifications {
+    const val EXTRA_NOTIFICATION_ID = "extra_notification_id"
+
+    /**
+     * Notification channel containing all low priority notifications.
+     */
+    private const val ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID = "alarmLowPriorityNotification"
+
+    /**
+     * Notification channel containing all high priority notifications.
+     */
+    private const val ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID = "alarmHighPriorityNotification"
+
+    /**
+     * Notification channel containing all snooze notifications.
+     */
+    private const val ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID = "alarmSnoozeNotification"
+
+    /**
+     * Notification channel containing all missed notifications.
+     */
+    private const val ALARM_MISSED_NOTIFICATION_CHANNEL_ID = "alarmMissedNotification"
+
+    /**
+     * Notification channel containing all alarm notifications.
+     */
+    private const val ALARM_NOTIFICATION_CHANNEL_ID = "alarmNotification"
+
+    /**
+     * Formats times such that chronological order and lexicographical order agree.
+     */
+    private val SORT_KEY_FORMAT: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+
+    /**
+     * This value is coordinated with group ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val UPCOMING_GROUP_KEY = "1"
+
+    /**
+     * This value is coordinated with group ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val MISSED_GROUP_KEY = "4"
+
+    /**
+     * This value is coordinated with notification ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val ALARM_GROUP_NOTIFICATION_ID = Int.MAX_VALUE - 4
+
+    /**
+     * This value is coordinated with notification ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val ALARM_GROUP_MISSED_NOTIFICATION_ID = Int.MAX_VALUE - 5
+
+    /**
+     * This value is coordinated with notification ids from
+     * [com.android.deskclock.data.NotificationModel]
+     */
+    private const val ALARM_FIRING_NOTIFICATION_ID = Int.MAX_VALUE - 7
+
+    @JvmStatic
+    @Synchronized
+    fun showLowPriorityNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying low priority notification for alarm instance: " + instance.mId)
+
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(context.getString(
+                        R.string.alarm_alert_predismiss_title))
+                .setContentText(AlarmUtils.getAlarmText(
+                        context, instance, true /* includeLabel */))
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setAutoCancel(false)
+                .setSortKey(createSortKey(instance))
+                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater) {
+            builder.setGroup(UPCOMING_GROUP_KEY)
+        }
+
+        // Setup up hide notification
+        val hideIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DELETE_TAG, instance,
+                InstancesColumns.HIDE_NOTIFICATION_STATE)
+        val id = instance.hashCode()
+        builder.setDeleteIntent(PendingIntent.getService(context, id,
+                hideIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup up dismiss action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.PREDISMISSED_STATE)
+        builder.addAction(R.drawable.ic_alarm_off_24dp,
+                context.getString(R.string.alarm_alert_dismiss_text),
+                PendingIntent.getService(context, id,
+                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content action if instance is owned by alarm
+        val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+        builder.setContentIntent(PendingIntent.getActivity(context, id,
+                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateUpcomingAlarmGroupNotification(context, -1, notification)
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun showHighPriorityNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying high priority notification for alarm instance: " + instance.mId)
+
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(context.getString(
+                        R.string.alarm_alert_predismiss_title))
+                .setContentText(AlarmUtils.getAlarmText(
+                        context, instance, true /* includeLabel */))
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setAutoCancel(false)
+                .setSortKey(createSortKey(instance))
+                .setPriority(NotificationCompat.PRIORITY_HIGH)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater) {
+            builder.setGroup(UPCOMING_GROUP_KEY)
+        }
+
+        // Setup up dismiss action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.PREDISMISSED_STATE)
+        val id = instance.hashCode()
+        builder.addAction(R.drawable.ic_alarm_off_24dp,
+                context.getString(R.string.alarm_alert_dismiss_text),
+                PendingIntent.getService(context, id,
+                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content action if instance is owned by alarm
+        val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+        builder.setContentIntent(PendingIntent.getActivity(context, id,
+                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_HIGH)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateUpcomingAlarmGroupNotification(context, -1, notification)
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun isGroupSummary(n: Notification): Boolean {
+        return n.flags and 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
+     * [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 fun getFirstActiveNotification(
+        context: Context,
+        group: String,
+        canceledNotificationId: Int,
+        postedNotification: Notification?
+    ): Notification? {
+        val nm: NotificationManager =
+                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        val notifications: Array<StatusBarNotification> = nm.getActiveNotifications()
+        var firstActiveNotification: Notification? = postedNotification
+        for (statusBarNotification in notifications) {
+            val n: Notification = statusBarNotification.getNotification()
+            if (!isGroupSummary(n) && group == n.getGroup() &&
+                    statusBarNotification.getId() != canceledNotificationId) {
+                if (firstActiveNotification == null ||
+                        n.getSortKey().compareTo(firstActiveNotification.getSortKey()) < 0) {
+                    firstActiveNotification = n
+                }
+            }
+        }
+        return firstActiveNotification
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun getActiveGroupSummaryNotification(context: Context, group: String): Notification? {
+        val nm: NotificationManager =
+                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        val notifications: Array<StatusBarNotification> = nm.getActiveNotifications()
+        for (statusBarNotification in notifications) {
+            val n: Notification = statusBarNotification.getNotification()
+            if (isGroupSummary(n) && group == n.getGroup()) {
+                return n
+            }
+        }
+        return null
+    }
+
+    private fun updateUpcomingAlarmGroupNotification(
+        context: Context,
+        canceledNotificationId: Int,
+        postedNotification: Notification?
+    ) {
+        if (!Utils.isNOrLater) {
+            return
+        }
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_HIGH)
+            nm.createNotificationChannel(channel)
+        }
+
+        val firstUpcoming: Notification? = getFirstActiveNotification(context, UPCOMING_GROUP_KEY,
+                canceledNotificationId, postedNotification)
+        if (firstUpcoming == null) {
+            nm.cancel(ALARM_GROUP_NOTIFICATION_ID)
+            return
+        }
+
+        var summary: Notification? = getActiveGroupSummaryNotification(context, UPCOMING_GROUP_KEY)
+        if (summary == null ||
+                summary.contentIntent != firstUpcoming.contentIntent) {
+            summary = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
+                    .setShowWhen(false)
+                    .setContentIntent(firstUpcoming.contentIntent)
+                    .setColor(ContextCompat.getColor(context, R.color.default_background))
+                    .setSmallIcon(R.drawable.stat_notify_alarm)
+                    .setGroup(UPCOMING_GROUP_KEY)
+                    .setGroupSummary(true)
+                    .setPriority(NotificationCompat.PRIORITY_HIGH)
+                    .setCategory(NotificationCompat.CATEGORY_EVENT)
+                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                    .setLocalOnly(true)
+                    .build()
+            nm.notify(ALARM_GROUP_NOTIFICATION_ID, summary)
+        }
+    }
+
+    private fun updateMissedAlarmGroupNotification(
+        context: Context,
+        canceledNotificationId: Int,
+        postedNotification: Notification?
+    ) {
+        if (!Utils.isNOrLater) {
+            return
+        }
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_HIGH)
+            nm.createNotificationChannel(channel)
+        }
+
+        val firstMissed: Notification? = getFirstActiveNotification(context, MISSED_GROUP_KEY,
+                canceledNotificationId, postedNotification)
+        if (firstMissed == null) {
+            nm.cancel(ALARM_GROUP_MISSED_NOTIFICATION_ID)
+            return
+        }
+
+        var summary: Notification? = getActiveGroupSummaryNotification(context, MISSED_GROUP_KEY)
+        if (summary == null ||
+                summary.contentIntent != firstMissed.contentIntent) {
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+                val channel = NotificationChannel(
+                        ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
+                        context.getString(R.string.default_label),
+                        NotificationManagerCompat.IMPORTANCE_HIGH)
+                nm.createNotificationChannel(channel)
+            }
+            summary = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
+                    .setShowWhen(false)
+                    .setContentIntent(firstMissed.contentIntent)
+                    .setColor(ContextCompat.getColor(context, R.color.default_background))
+                    .setSmallIcon(R.drawable.stat_notify_alarm)
+                    .setGroup(MISSED_GROUP_KEY)
+                    .setGroupSummary(true)
+                    .setPriority(NotificationCompat.PRIORITY_HIGH)
+                    .setCategory(NotificationCompat.CATEGORY_EVENT)
+                    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                    .setLocalOnly(true)
+                    .build()
+            nm.notify(ALARM_GROUP_MISSED_NOTIFICATION_ID, summary)
+        }
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun showSnoozeNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId)
+
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(instance.getLabelOrDefault(context))
+                .setContentText(context.getString(R.string.alarm_alert_snooze_until,
+                        AlarmUtils.getFormattedTime(context, instance.alarmTime)))
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setAutoCancel(false)
+                .setSortKey(createSortKey(instance))
+                .setPriority(NotificationCompat.PRIORITY_MAX)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater) {
+            builder.setGroup(UPCOMING_GROUP_KEY)
+        }
+
+        // Setup up dismiss action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+        val id = instance.hashCode()
+        builder.addAction(R.drawable.ic_alarm_off_24dp,
+                context.getString(R.string.alarm_alert_dismiss_text),
+                PendingIntent.getService(context, id,
+                        dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content action if instance is owned by alarm
+        val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+        builder.setContentIntent(PendingIntent.getActivity(context, id,
+                viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateUpcomingAlarmGroupNotification(context, -1, notification)
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun showMissedNotification(
+        context: Context,
+        instance: AlarmInstance
+    ) {
+        LogUtils.v("Displaying missed notification for alarm instance: " + instance.mId)
+
+        val label = instance.mLabel
+        val alarmTime: String = AlarmUtils.getFormattedTime(context, instance.alarmTime)
+        val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+                context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
+                .setShowWhen(false)
+                .setContentTitle(context.getString(R.string.alarm_missed_title))
+                .setContentText(if (instance.mLabel!!.isEmpty()) {
+                    alarmTime
+                } else {
+                    context.getString(R.string.alarm_missed_text, alarmTime, label)
+                })
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+                .setSortKey(createSortKey(instance))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setPriority(NotificationCompat.PRIORITY_HIGH)
+                .setCategory(NotificationCompat.CATEGORY_EVENT)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        if (Utils.isNOrLater) {
+            builder.setGroup(MISSED_GROUP_KEY)
+        }
+
+        val id = instance.hashCode()
+
+        // Setup dismiss intent
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+        builder.setDeleteIntent(PendingIntent.getService(context, id,
+                dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup content intent
+        val showAndDismiss: Intent = AlarmInstance.createIntent(context,
+                AlarmStateManager::class.java, instance.mId)
+        showAndDismiss.putExtra(EXTRA_NOTIFICATION_ID, id)
+        showAndDismiss.setAction(AlarmStateManager.SHOW_AND_DISMISS_ALARM_ACTION)
+        builder.setContentIntent(PendingIntent.getBroadcast(context, id,
+                showAndDismiss, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            nm.createNotificationChannel(channel)
+        }
+        val notification: Notification = builder.build()
+        nm.notify(id, notification)
+        updateMissedAlarmGroupNotification(context, -1, notification)
+    }
+
+    @Synchronized
+    fun showAlarmNotification(service: Service, instance: AlarmInstance) {
+        LogUtils.v("Displaying alarm notification for alarm instance: " + instance.mId)
+
+        val resources: Resources = service.getResources()
+        val notification: NotificationCompat.Builder = NotificationCompat.Builder(
+                service, ALARM_NOTIFICATION_CHANNEL_ID)
+                .setContentTitle(instance.getLabelOrDefault(service))
+                .setContentText(AlarmUtils.getFormattedTime(
+                        service, instance.alarmTime))
+                .setColor(ContextCompat.getColor(service, R.color.default_background))
+                .setSmallIcon(R.drawable.stat_notify_alarm)
+                .setOngoing(true)
+                .setAutoCancel(false)
+                .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
+                .setWhen(0)
+                .setCategory(NotificationCompat.CATEGORY_ALARM)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+
+        // Setup Snooze Action
+        val snoozeIntent: Intent = AlarmStateManager.createStateChangeIntent(service,
+                AlarmStateManager.ALARM_SNOOZE_TAG, instance, InstancesColumns.SNOOZE_STATE)
+        snoozeIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true)
+        val snoozePendingIntent: PendingIntent = 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)
+
+        // Setup Dismiss Action
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(service,
+                AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+        dismissIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true)
+        val dismissPendingIntent: PendingIntent = PendingIntent.getService(service,
+                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)
+
+        // Setup Content Action
+        val contentIntent: Intent = AlarmInstance.createIntent(service, AlarmActivity::class.java,
+                instance.mId)
+        notification.setContentIntent(PendingIntent.getActivity(service,
+                ALARM_FIRING_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+        // Setup fullscreen intent
+        val fullScreenIntent: Intent =
+                AlarmInstance.createIntent(service, AlarmActivity::class.java, instance.mId)
+        // set action, so we can be different then content pending intent
+        fullScreenIntent.setAction("fullscreen_activity")
+        fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
+                Intent.FLAG_ACTIVITY_NO_USER_ACTION)
+        notification.setFullScreenIntent(PendingIntent.getActivity(service,
+                ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT),
+                true)
+        notification.setPriority(NotificationCompat.PRIORITY_MAX)
+
+        clearNotification(service, instance)
+        service.startForeground(ALARM_FIRING_NOTIFICATION_ID, notification.build())
+    }
+
+    @JvmStatic
+    @Synchronized
+    fun clearNotification(context: Context, instance: AlarmInstance) {
+        LogUtils.v("Clearing notifications for alarm instance: " + instance.mId)
+        val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+        val 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.
+     */
+    @JvmStatic
+    fun updateNotification(context: Context, instance: AlarmInstance) {
+        when (instance.mAlarmState) {
+            InstancesColumns.LOW_NOTIFICATION_STATE -> {
+                showLowPriorityNotification(context, instance)
+            }
+            InstancesColumns.HIGH_NOTIFICATION_STATE -> {
+                showHighPriorityNotification(context, instance)
+            }
+            InstancesColumns.SNOOZE_STATE -> showSnoozeNotification(context, instance)
+            InstancesColumns.MISSED_STATE -> showMissedNotification(context, instance)
+            else -> LogUtils.d("No notification to update")
+        }
+    }
+
+    @JvmStatic
+    fun createViewAlarmIntent(context: Context?, instance: AlarmInstance): Intent {
+        val alarmId = instance.mAlarmId ?: Alarm.INVALID_ID
+        return Alarm.createIntent(context, DeskClock::class.java, alarmId)
+                .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+    }
+
+    /**
+     * Alarm notifications are sorted chronologically. Missed alarms are sorted chronologically
+     * **after** all upcoming/snoozed alarms by including the "MISSED" prefix on the
+     * sort key.
+     *
+     * @param instance the alarm instance for which the notification is generated
+     * @return the sort key that specifies the order of this alarm notification
+     */
+    private fun createSortKey(instance: AlarmInstance): String {
+        val timeKey = SORT_KEY_FORMAT.format(instance.alarmTime.time)
+        val missedAlarm = instance.mAlarmState == InstancesColumns.MISSED_STATE
+        return if (missedAlarm) "MISSED $timeKey" else timeKey
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmService.java b/src/com/android/deskclock/alarms/AlarmService.java
deleted file mode 100644
index b9a97db..0000000
--- a/src/com/android/deskclock/alarms/AlarmService.java
+++ /dev/null
@@ -1,267 +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.alarms;
-
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Binder;
-import android.os.IBinder;
-import android.telephony.PhoneStateListener;
-import android.telephony.TelephonyManager;
-
-import com.android.deskclock.AlarmAlertWakeLock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.AlarmInstance;
-
-/**
- * This service is in charge of starting/stopping the alarm. It will bring up and manage the
- * {@link AlarmActivity} as well as {@link AlarmKlaxon}.
- *
- * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver
- * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents.
- */
-public class AlarmService extends Service {
-    /**
-     * AlarmActivity and AlarmService (when unbound) listen for this broadcast intent
-     * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before
-     * ALARM_DONE_ACTION).
-     */
-    public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
-
-    /**
-     * AlarmActivity and AlarmService listen for this broadcast intent so that other
-     * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
-     */
-    public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
-
-    /** A public action sent by AlarmService when the alarm has started. */
-    public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT";
-
-    /** A public action sent by AlarmService when the alarm has stopped for any reason. */
-    public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE";
-
-    /** Private action used to stop an alarm with this service. */
-    public static final String STOP_ALARM_ACTION = "STOP_ALARM";
-
-    /** 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;
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        mIsBound = true;
-        return mBinder;
-    }
-
-    @Override
-    public boolean onUnbind(Intent intent) {
-        mIsBound = false;
-        return super.onUnbind(intent);
-    }
-
-    /**
-     * Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
-     * or using a different instance.
-     *
-     * @param context application context
-     * @param instance you are trying to stop
-     */
-    public static void stopAlarm(Context context, AlarmInstance instance) {
-        final Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId)
-                .setAction(STOP_ALARM_ACTION);
-
-        // We don't need a wake lock here, since we are trying to kill an alarm
-        context.startService(intent);
-    }
-
-    private TelephonyManager mTelephonyManager;
-    private AlarmInstance mCurrentAlarm = null;
-
-    private void startAlarm(AlarmInstance instance) {
-        LogUtils.v("AlarmService.start with instance: " + instance.mId);
-        if (mCurrentAlarm != null) {
-            AlarmStateManager.setMissedState(this, mCurrentAlarm);
-            stopCurrentAlarm();
-        }
-
-        AlarmAlertWakeLock.acquireCpuWakeLock(this);
-
-        mCurrentAlarm = instance;
-        AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
-        mTelephonyManager.listen(mPhoneStateListener.init(), PhoneStateListener.LISTEN_CALL_STATE);
-        AlarmKlaxon.start(this, mCurrentAlarm);
-        sendBroadcast(new Intent(ALARM_ALERT_ACTION));
-    }
-
-    private void stopCurrentAlarm() {
-        if (mCurrentAlarm == null) {
-            LogUtils.v("There is no current alarm to stop");
-            return;
-        }
-
-        final long instanceId = mCurrentAlarm.mId;
-        LogUtils.v("AlarmService.stop with instance: %s", instanceId);
-
-        AlarmKlaxon.stop(this);
-        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
-        sendBroadcast(new Intent(ALARM_DONE_ACTION));
-
-        stopForeground(true /* removeNotification */);
-
-        mCurrentAlarm = null;
-        AlarmAlertWakeLock.releaseCpuLock();
-    }
-
-    private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            final String action = intent.getAction();
-            LogUtils.i("AlarmService received intent %s", action);
-            if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) {
-                LogUtils.i("No valid firing alarm");
-                return;
-            }
-
-            if (mIsBound) {
-                LogUtils.i("AlarmActivity bound; AlarmService no-op");
-                return;
-            }
-
-            switch (action) {
-                case ALARM_SNOOZE_ACTION:
-                    // Set the alarm state to snoozed.
-                    // If this broadcast receiver is handling the snooze intent then AlarmActivity
-                    // must not be showing, so always show snooze toast.
-                    AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */);
-                    Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
-                    break;
-                case ALARM_DISMISS_ACTION:
-                    // Set the alarm state to dismissed.
-                    AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm);
-                    Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
-                    break;
-            }
-        }
-    };
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
-
-        // Register the broadcast receiver
-        final IntentFilter filter = new IntentFilter(ALARM_SNOOZE_ACTION);
-        filter.addAction(ALARM_DISMISS_ACTION);
-        registerReceiver(mActionsReceiver, filter);
-        mIsRegistered = true;
-    }
-
-    @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()) {
-            case AlarmStateManager.CHANGE_STATE_ACTION:
-                AlarmStateManager.handleIntent(this, intent);
-
-                // If state is changed to firing, actually fire the alarm!
-                final int alarmState = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1);
-                if (alarmState == AlarmInstance.FIRED_STATE) {
-                    final ContentResolver cr = this.getContentResolver();
-                    final AlarmInstance instance = AlarmInstance.getInstance(cr, instanceId);
-                    if (instance == null) {
-                        LogUtils.e("No instance found to start alarm: %d", instanceId);
-                        if (mCurrentAlarm != null) {
-                            // Only release lock if we are not firing alarm
-                            AlarmAlertWakeLock.releaseCpuLock();
-                        }
-                        break;
-                    }
-
-                    if (mCurrentAlarm != null && mCurrentAlarm.mId == instanceId) {
-                        LogUtils.e("Alarm already started for instance: %d", instanceId);
-                        break;
-                    }
-                    startAlarm(instance);
-                }
-                break;
-            case STOP_ALARM_ACTION:
-                if (mCurrentAlarm != null && mCurrentAlarm.mId != instanceId) {
-                    LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d",
-                            instanceId, mCurrentAlarm.mId);
-                    break;
-                }
-                stopCurrentAlarm();
-                stopSelf();
-        }
-
-        return Service.START_NOT_STICKY;
-    }
-
-    @Override
-    public void onDestroy() {
-        LogUtils.v("AlarmService.onDestroy() called");
-        super.onDestroy();
-        if (mCurrentAlarm != null) {
-            stopCurrentAlarm();
-        }
-
-        if (mIsRegistered) {
-            unregisterReceiver(mActionsReceiver);
-            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/AlarmService.kt b/src/com/android/deskclock/alarms/AlarmService.kt
new file mode 100644
index 0000000..005167b
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmService.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Service
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Binder
+import android.os.IBinder
+import android.telephony.PhoneStateListener
+import android.telephony.TelephonyManager
+
+import com.android.deskclock.AlarmAlertWakeLock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+/**
+ * This service is in charge of starting/stopping the alarm. It will bring up and manage the
+ * [AlarmActivity] as well as [AlarmKlaxon].
+ *
+ * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver
+ * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents.
+ */
+class AlarmService : Service() {
+    /** Binder given to AlarmActivity.  */
+    private val mBinder: IBinder = Binder()
+
+    /** Whether the service is currently bound to AlarmActivity  */
+    private var mIsBound = false
+
+    /** Listener for changes in phone state.  */
+    private val mPhoneStateListener = PhoneStateChangeListener()
+
+    /** Whether the receiver is currently registered  */
+    private var mIsRegistered = false
+
+    override fun onBind(intent: Intent?): IBinder {
+        mIsBound = true
+        return mBinder
+    }
+
+    override fun onUnbind(intent: Intent?): Boolean {
+        mIsBound = false
+        return super.onUnbind(intent)
+    }
+
+    private lateinit var mTelephonyManager: TelephonyManager
+    private var mCurrentAlarm: AlarmInstance? = null
+
+    private fun startAlarm(instance: AlarmInstance) {
+        LogUtils.v("AlarmService.start with instance: " + instance.mId)
+        if (mCurrentAlarm != null) {
+            AlarmStateManager.setMissedState(this, mCurrentAlarm!!)
+            stopCurrentAlarm()
+        }
+
+        AlarmAlertWakeLock.acquireCpuWakeLock(this)
+
+        mCurrentAlarm = instance
+        AlarmNotifications.showAlarmNotification(this, mCurrentAlarm!!)
+        mTelephonyManager.listen(mPhoneStateListener.init(), PhoneStateListener.LISTEN_CALL_STATE)
+        AlarmKlaxon.start(this, mCurrentAlarm!!)
+        sendBroadcast(Intent(ALARM_ALERT_ACTION))
+    }
+
+    private fun stopCurrentAlarm() {
+        if (mCurrentAlarm == null) {
+            LogUtils.v("There is no current alarm to stop")
+            return
+        }
+
+        val instanceId = mCurrentAlarm!!.mId
+        LogUtils.v("AlarmService.stop with instance: %s", instanceId)
+
+        AlarmKlaxon.stop(this)
+        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE)
+        sendBroadcast(Intent(ALARM_DONE_ACTION))
+
+        stopForeground(true /* removeNotification */)
+
+        mCurrentAlarm = null
+        AlarmAlertWakeLock.releaseCpuLock()
+    }
+
+    private val mActionsReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            val action: String? = intent.getAction()
+            LogUtils.i("AlarmService received intent %s", action)
+            if (mCurrentAlarm == null ||
+                    mCurrentAlarm!!.mAlarmState != InstancesColumns.FIRED_STATE) {
+                LogUtils.i("No valid firing alarm")
+                return
+            }
+
+            if (mIsBound) {
+                LogUtils.i("AlarmActivity bound; AlarmService no-op")
+                return
+            }
+
+            when (action) {
+                ALARM_SNOOZE_ACTION -> {
+                    // Set the alarm state to snoozed.
+                    // If this broadcast receiver is handling the snooze intent then AlarmActivity
+                    // must not be showing, so always show snooze toast.
+                    AlarmStateManager.setSnoozeState(context, mCurrentAlarm!!, true /* showToast */)
+                    Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent)
+                }
+                ALARM_DISMISS_ACTION -> {
+                    // Set the alarm state to dismissed.
+                    AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm!!)
+                    Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent)
+                }
+            }
+        }
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        mTelephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+
+        // Register the broadcast receiver
+        val filter = IntentFilter(ALARM_SNOOZE_ACTION)
+        filter.addAction(ALARM_DISMISS_ACTION)
+        registerReceiver(mActionsReceiver, filter, Context.RECEIVER_EXPORTED)
+        mIsRegistered = true
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        LogUtils.v("AlarmService.onStartCommand() with %s", intent)
+        if (intent == null) {
+            return Service.START_NOT_STICKY
+        }
+
+        val instanceId = AlarmInstance.getId(intent.getData()!!)
+        when (intent.getAction()) {
+            AlarmStateManager.CHANGE_STATE_ACTION -> {
+                AlarmStateManager.handleIntent(this, intent)
+
+                // If state is changed to firing, actually fire the alarm!
+                val alarmState: Int = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1)
+                if (alarmState == InstancesColumns.FIRED_STATE) {
+                    val cr: ContentResolver = this.getContentResolver()
+                    val instance: AlarmInstance? = AlarmInstance.getInstance(cr, instanceId)
+                    if (instance == null) {
+                        LogUtils.e("No instance found to start alarm: %d", instanceId)
+                        if (mCurrentAlarm != null) {
+                            // Only release lock if we are not firing alarm
+                            AlarmAlertWakeLock.releaseCpuLock()
+                        }
+                    } else if (mCurrentAlarm != null && mCurrentAlarm!!.mId == instanceId) {
+                        LogUtils.e("Alarm already started for instance: %d", instanceId)
+                    } else {
+                        startAlarm(instance)
+                    }
+                }
+            }
+            STOP_ALARM_ACTION -> {
+                if (mCurrentAlarm != null && mCurrentAlarm!!.mId != instanceId) {
+                    LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d",
+                            instanceId, mCurrentAlarm!!.mId)
+                } else {
+                    stopCurrentAlarm()
+                    stopSelf()
+                }
+            }
+        }
+
+        return Service.START_NOT_STICKY
+    }
+
+    override fun onDestroy() {
+        LogUtils.v("AlarmService.onDestroy() called")
+        super.onDestroy()
+        if (mCurrentAlarm != null) {
+            stopCurrentAlarm()
+        }
+
+        if (mIsRegistered) {
+            unregisterReceiver(mActionsReceiver)
+            mIsRegistered = false
+        }
+    }
+
+    private inner class PhoneStateChangeListener : PhoneStateListener() {
+        private var mPhoneCallState = 0
+
+        fun init(): PhoneStateChangeListener {
+            mPhoneCallState = -1
+            return this
+        }
+
+        override fun onCallStateChanged(state: Int, ignored: String?) {
+            if (mPhoneCallState == -1) {
+                mPhoneCallState = state
+            }
+
+            if (state != TelephonyManager.CALL_STATE_IDLE && state != mPhoneCallState) {
+                startService(AlarmStateManager.createStateChangeIntent(this@AlarmService,
+                        "AlarmService", mCurrentAlarm!!, InstancesColumns.MISSED_STATE))
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * AlarmActivity and AlarmService (when unbound) listen for this broadcast intent
+         * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before
+         * ALARM_DONE_ACTION).
+         */
+        const val ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE"
+
+        /**
+         * AlarmActivity and AlarmService listen for this broadcast intent so that other
+         * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
+         */
+        const val ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS"
+
+        /** A public action sent by AlarmService when the alarm has started.  */
+        const val ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT"
+
+        /** A public action sent by AlarmService when the alarm has stopped for any reason.  */
+        const val ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE"
+
+        /** Private action used to stop an alarm with this service.  */
+        const val STOP_ALARM_ACTION = "STOP_ALARM"
+
+        /**
+         * Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
+         * or using a different instance.
+         *
+         * @param context application context
+         * @param instance you are trying to stop
+         */
+        @JvmStatic
+        fun stopAlarm(context: Context, instance: AlarmInstance) {
+            val intent: Intent =
+                    AlarmInstance.createIntent(context, AlarmService::class.java, instance.mId)
+                            .setAction(STOP_ALARM_ACTION)
+
+            // We don't need a wake lock here, since we are trying to kill an alarm
+            context.startService(intent)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmStateManager.java b/src/com/android/deskclock/alarms/AlarmStateManager.java
deleted file mode 100644
index 0f70a0f..0000000
--- a/src/com/android/deskclock/alarms/AlarmStateManager.java
+++ /dev/null
@@ -1,1037 +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.alarms;
-
-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.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.provider.Settings;
-import androidx.core.app.NotificationManagerCompat;
-import android.text.format.DateFormat;
-import android.widget.Toast;
-
-import com.android.deskclock.AlarmAlertWakeLock;
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.AsyncHandler;
-import com.android.deskclock.DeskClock;
-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 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;
-
-/**
- * This class handles all the state changes for alarm instances. You need to
- * register all alarm instances with the state manager if you want them to
- * be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE),
- * then you must also re-register instances to fix their states.
- *
- * Please see {@link #registerInstance) for special transitions when major time changes
- * occur.
- *
- * Following states:
- *
- * SILENT_STATE:
- * This state is used when the alarm is activated, but doesn't need to display anything. It
- * is in charge of changing the alarm instance state to a LOW_NOTIFICATION_STATE.
- *
- * LOW_NOTIFICATION_STATE:
- * This state is used to notify the user that the alarm will go off
- * {@link AlarmInstance#LOW_NOTIFICATION_HOUR_OFFSET}. This
- * state handles the state changes to HIGH_NOTIFICATION_STATE, HIDE_NOTIFICATION_STATE and
- * DISMISS_STATE.
- *
- * HIDE_NOTIFICATION_STATE:
- * This is a transient state of the LOW_NOTIFICATION_STATE, where the user wants to hide the
- * notification. This will sit and wait until the HIGH_PRIORITY_NOTIFICATION should go off.
- *
- * HIGH_NOTIFICATION_STATE:
- * This state behaves like the LOW_NOTIFICATION_STATE, but doesn't allow the user to hide it.
- * This state is in charge of triggering a FIRED_STATE or DISMISS_STATE.
- *
- * SNOOZED_STATE:
- * The SNOOZED_STATE behaves like a HIGH_NOTIFICATION_STATE, but with a different message. It
- * also increments the alarm time in the instance to reflect the new snooze time.
- *
- * FIRED_STATE:
- * The FIRED_STATE is used when the alarm is firing. It will start the AlarmService, and wait
- * until the user interacts with the alarm via SNOOZED_STATE or DISMISS_STATE change. If the user
- * doesn't then it might be change to MISSED_STATE if auto-silenced was enabled.
- *
- * MISSED_STATE:
- * The MISSED_STATE is used when the alarm already fired, but the user could not interact with
- * it. At this point the alarm instance is dead and we check the parent alarm to see if we need
- * to disable or schedule a new alarm_instance. There is also a notification shown to the user
- * that he/she missed the alarm and that stays for
- * {@link AlarmInstance#MISSED_TIME_TO_LIVE_HOUR_OFFSET} or until the user acknownledges it.
- *
- * DISMISS_STATE:
- * This is really a transient state that will properly delete the alarm instance. Use this state,
- * whenever you want to get rid of the alarm instance. This state will also check the alarm
- * parent to see if it should disable or schedule a new alarm instance.
- */
-public final class AlarmStateManager extends BroadcastReceiver {
-    // Intent action to trigger an instance state change.
-    public static final String CHANGE_STATE_ACTION = "change_state";
-
-    // Intent action to show the alarm and dismiss the instance
-    public static final String SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm";
-
-    // Intent action for an AlarmManager alarm serving only to set the next alarm indicators
-    private static final String INDICATOR_ACTION = "indicator";
-
-    // System intent action to notify AppWidget that we changed the alarm text.
-    public static final String ACTION_ALARM_CHANGED = "com.android.deskclock.ALARM_CHANGED";
-
-    // Extra key to set the desired state change.
-    public static final String ALARM_STATE_EXTRA = "intent.extra.alarm.state";
-
-    // Extra key to indicate the state change was launched from a notification.
-    public static final String FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification";
-
-    // Extra key to set the global broadcast id.
-    private static final String ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id";
-
-    // Intent category tags used to dismiss, snooze or delete an alarm
-    public static final String ALARM_DISMISS_TAG = "DISMISS_TAG";
-    public static final String ALARM_SNOOZE_TAG = "SNOOZE_TAG";
-    public static final String ALARM_DELETE_TAG = "DELETE_TAG";
-
-    // Intent category tag used when schedule state change intents in alarm manager.
-    private static final String ALARM_MANAGER_TAG = "ALARM_MANAGER";
-
-    // Buffer time in seconds to fire alarm instead of marking it missed.
-    public static final int ALARM_FIRE_BUFFER = 15;
-
-    // A factory for the current time; can be mocked for testing purposes.
-    private static CurrentTimeFactory sCurrentTimeFactory;
-
-    // Schedules alarm state transitions; can be mocked for testing purposes.
-    private static StateChangeScheduler sStateChangeScheduler =
-            new AlarmManagerStateChangeScheduler();
-
-    private static Calendar getCurrentTime() {
-        return sCurrentTimeFactory == null
-                ? DataModel.getDataModel().getCalendar()
-                : sCurrentTimeFactory.getCurrentTime();
-    }
-
-    static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) {
-        sCurrentTimeFactory = currentTimeFactory;
-    }
-
-    static void setStateChangeScheduler(StateChangeScheduler stateChangeScheduler) {
-        if (stateChangeScheduler == null) {
-            stateChangeScheduler = new AlarmManagerStateChangeScheduler();
-        }
-        sStateChangeScheduler = stateChangeScheduler;
-    }
-
-    /**
-     * Update the next alarm stored in framework. This value is also displayed in digital widgets
-     * and the clock tab in this app.
-     */
-    private static void updateNextAlarm(Context context) {
-        final AlarmInstance nextAlarm = getNextFiringAlarm(context);
-
-        if (Utils.isPreL()) {
-            updateNextAlarmInSystemSettings(context, nextAlarm);
-        } else {
-            updateNextAlarmInAlarmManager(context, nextAlarm);
-        }
-    }
-
-    /**
-     * Returns an alarm instance of an alarm that's going to fire next.
-     *
-     * @param context application context
-     * @return an alarm instance that will fire earliest relative to current time.
-     */
-    public static AlarmInstance getNextFiringAlarm(Context context) {
-        final ContentResolver cr = context.getContentResolver();
-        final String activeAlarmQuery = AlarmInstance.ALARM_STATE + "<" + AlarmInstance.FIRED_STATE;
-        final List<AlarmInstance> alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery);
-
-        AlarmInstance nextAlarm = null;
-        for (AlarmInstance instance : alarmInstances) {
-            if (nextAlarm == null || instance.getAlarmTime().before(nextAlarm.getAlarmTime())) {
-                nextAlarm = instance;
-            }
-        }
-        return nextAlarm;
-    }
-
-    /**
-     * Used in pre-L devices, where "next alarm" is stored in system settings.
-     */
-    @SuppressWarnings("deprecation")
-    @TargetApi(Build.VERSION_CODES.KITKAT)
-    private static void updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm) {
-        // Format the next alarm time if an alarm is scheduled.
-        String time = "";
-        if (nextAlarm != null) {
-            time = AlarmUtils.getFormattedTime(context, nextAlarm.getAlarmTime());
-        }
-
-        try {
-            // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions
-            Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time);
-
-            LogUtils.i("Updated next alarm time to: \'" + time + '\'');
-
-            // Send broadcast message so pre-L AppWidgets will recognize an update.
-            context.sendBroadcast(new Intent(ACTION_ALARM_CHANGED));
-        } catch (SecurityException se) {
-            // The user has most likely revoked WRITE_SETTINGS.
-            LogUtils.e("Unable to update next alarm to: \'" + time + '\'', se);
-        }
-    }
-
-    /**
-     * Used in L and later devices where "next alarm" is stored in the Alarm Manager.
-     */
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    private static void updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm) {
-        // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the
-        // alarm that is going to fire next. The operation is constructed such that it is ignored
-        // by AlarmStateManager.
-
-        final AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
-
-        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.
-            PendingIntent viewIntent = PendingIntent.getActivity(context, nextAlarm.hashCode(),
-                    AlarmNotifications.createViewAlarmIntent(context, nextAlarm),
-                    PendingIntent.FLAG_UPDATE_CURRENT);
-
-            final AlarmClockInfo info = new AlarmClockInfo(alarmTime, viewIntent);
-            Utils.updateNextAlarm(alarmManager, info, operation);
-        } else if (operation != null) {
-            LogUtils.i("Canceling upcoming AlarmClockInfo");
-            alarmManager.cancel(operation);
-        }
-    }
-
-    /**
-     * Used by dismissed and missed states, to update parent alarm. This will either
-     * disable, delete or reschedule parent alarm.
-     *
-     * @param context  application context
-     * @param instance to update parent for
-     */
-    private static void updateParentAlarm(Context context, AlarmInstance instance) {
-        ContentResolver cr = context.getContentResolver();
-        Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
-        if (alarm == null) {
-            LogUtils.e("Parent has been deleted with instance: " + instance.toString());
-            return;
-        }
-
-        if (!alarm.daysOfWeek.isRepeating()) {
-            if (alarm.deleteAfterUse) {
-                LogUtils.i("Deleting parent alarm: " + alarm.id);
-                Alarm.deleteAlarm(cr, alarm.id);
-            } else {
-                LogUtils.i("Disabling parent alarm: " + alarm.id);
-                alarm.enabled = false;
-                Alarm.updateAlarm(cr, alarm);
-            }
-        } else {
-            // Schedule the next repeating instance which may be before the current instance if a
-            // time jump has occurred. Otherwise, if the current instance is the next instance
-            // and has already been fired, schedule the subsequent instance.
-            AlarmInstance nextRepeatedInstance = alarm.createInstanceAfter(getCurrentTime());
-            if (instance.mAlarmState > AlarmInstance.FIRED_STATE
-                    && nextRepeatedInstance.getAlarmTime().equals(instance.getAlarmTime())) {
-                nextRepeatedInstance = alarm.createInstanceAfter(instance.getAlarmTime());
-            }
-
-            LogUtils.i("Creating new instance for repeating alarm " + alarm.id + " at " +
-                    AlarmUtils.getFormattedTime(context, nextRepeatedInstance.getAlarmTime()));
-            AlarmInstance.addInstance(cr, nextRepeatedInstance);
-            registerInstance(context, nextRepeatedInstance, true);
-        }
-    }
-
-    /**
-     * Utility method to create a proper change state intent.
-     *
-     * @param context  application context
-     * @param tag      used to make intent differ from other state change intents.
-     * @param instance to change state to
-     * @param state    to change to.
-     * @return intent that can be used to change an alarm instance state
-     */
-    public static Intent createStateChangeIntent(Context context, String tag,
-            AlarmInstance instance, Integer state) {
-        // This intent is directed to AlarmService, though the actual handling of it occurs here
-        // in AlarmStateManager. The reason is that evidence exists showing the jump between the
-        // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by the
-        // Out Of Memory killer. If clock is killed during that jump, firing an alarm can fail to
-        // occur. To be safer, the call begins in AlarmService, which has the power to display the
-        // firing alarm if needed, so no jump is needed.
-        Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId);
-        intent.setAction(CHANGE_STATE_ACTION);
-        intent.addCategory(tag);
-        intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.getDataModel().getGlobalIntentId());
-        if (state != null) {
-            intent.putExtra(ALARM_STATE_EXTRA, state.intValue());
-        }
-        return intent;
-    }
-
-    /**
-     * Schedule alarm instance state changes with {@link AlarmManager}.
-     *
-     * @param ctx      application context
-     * @param time     to trigger state change
-     * @param instance to change state to
-     * @param newState to change to
-     */
-    private static void scheduleInstanceStateChange(Context ctx, Calendar time,
-            AlarmInstance instance, int newState) {
-        sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState);
-    }
-
-    /**
-     * Cancel all {@link AlarmManager} timers for instance.
-     *
-     * @param ctx      application context
-     * @param instance to disable all {@link AlarmManager} timers
-     */
-    private static void cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance) {
-        sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance);
-    }
-
-
-    /**
-     * This will set the alarm instance to the SILENT_STATE and update
-     * the application notifications and schedule any state changes that need
-     * to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setSilentState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting silent state to instance " + instance.mId);
-
-        // Update alarm in db
-        ContentResolver contentResolver = context.getContentResolver();
-        instance.mAlarmState = AlarmInstance.SILENT_STATE;
-        AlarmInstance.updateInstance(contentResolver, instance);
-
-        // Setup instance notification and scheduling timers
-        AlarmNotifications.clearNotification(context, instance);
-        scheduleInstanceStateChange(context, instance.getLowNotificationTime(),
-                instance, AlarmInstance.LOW_NOTIFICATION_STATE);
-    }
-
-    /**
-     * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update
-     * the application notifications and schedule any state changes that need
-     * to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setLowNotificationState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting low notification state to instance " + instance.mId);
-
-        // Update alarm state in db
-        ContentResolver contentResolver = context.getContentResolver();
-        instance.mAlarmState = AlarmInstance.LOW_NOTIFICATION_STATE;
-        AlarmInstance.updateInstance(contentResolver, instance);
-
-        // Setup instance notification and scheduling timers
-        AlarmNotifications.showLowPriorityNotification(context, instance);
-        scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
-                instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
-    }
-
-    /**
-     * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update
-     * the application notifications and schedule any state changes that need
-     * to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setHideNotificationState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting hide notification state to instance " + instance.mId);
-
-        // Update alarm state in db
-        ContentResolver contentResolver = context.getContentResolver();
-        instance.mAlarmState = AlarmInstance.HIDE_NOTIFICATION_STATE;
-        AlarmInstance.updateInstance(contentResolver, instance);
-
-        // Setup instance notification and scheduling timers
-        AlarmNotifications.clearNotification(context, instance);
-        scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
-                instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
-    }
-
-    /**
-     * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update
-     * the application notifications and schedule any state changes that need
-     * to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setHighNotificationState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting high notification state to instance " + instance.mId);
-
-        // Update alarm state in db
-        ContentResolver contentResolver = context.getContentResolver();
-        instance.mAlarmState = AlarmInstance.HIGH_NOTIFICATION_STATE;
-        AlarmInstance.updateInstance(contentResolver, instance);
-
-        // Setup instance notification and scheduling timers
-        AlarmNotifications.showHighPriorityNotification(context, instance);
-        scheduleInstanceStateChange(context, instance.getAlarmTime(),
-                instance, AlarmInstance.FIRED_STATE);
-    }
-
-    /**
-     * This will set the alarm instance to the FIRED_STATE and update
-     * the application notifications and schedule any state changes that need
-     * to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setFiredState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting fire state to instance " + instance.mId);
-
-        // Update alarm state in db
-        ContentResolver contentResolver = context.getContentResolver();
-        instance.mAlarmState = AlarmInstance.FIRED_STATE;
-        AlarmInstance.updateInstance(contentResolver, instance);
-
-        if (instance.mAlarmId != null) {
-            // if the time changed *backward* and pushed an instance from missed back to fired,
-            // remove any other scheduled instances that may exist
-            AlarmInstance.deleteOtherInstances(context, contentResolver, instance.mAlarmId,
-                    instance.mId);
-        }
-
-        Events.sendAlarmEvent(R.string.action_fire, 0);
-
-        Calendar timeout = instance.getTimeout();
-        if (timeout != null) {
-            scheduleInstanceStateChange(context, timeout, instance, AlarmInstance.MISSED_STATE);
-        }
-
-        // Instance not valid anymore, so find next alarm that will fire and notify system
-        updateNextAlarm(context);
-    }
-
-    /**
-     * This will set the alarm instance to the SNOOZE_STATE and update
-     * the application notifications and schedule any state changes that need
-     * to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setSnoozeState(final Context context, AlarmInstance instance,
-            boolean showToast) {
-        // Stop alarm if this instance is firing it
-        AlarmService.stopAlarm(context, instance);
-
-        // Calculate the new snooze alarm time
-        final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength();
-        Calendar newAlarmTime = Calendar.getInstance();
-        newAlarmTime.add(Calendar.MINUTE, snoozeMinutes);
-
-        // Update alarm state and new alarm time in db.
-        LogUtils.i("Setting snoozed state to instance " + instance.mId + " for "
-                + AlarmUtils.getFormattedTime(context, newAlarmTime));
-        instance.setAlarmTime(newAlarmTime);
-        instance.mAlarmState = AlarmInstance.SNOOZE_STATE;
-        AlarmInstance.updateInstance(context.getContentResolver(), instance);
-
-        // Setup instance notification and scheduling timers
-        AlarmNotifications.showSnoozeNotification(context, instance);
-        scheduleInstanceStateChange(context, instance.getAlarmTime(),
-                instance, AlarmInstance.FIRED_STATE);
-
-        // Display the snooze minutes in a toast.
-        if (showToast) {
-            final Handler mainHandler = new Handler(context.getMainLooper());
-            final Runnable myRunnable = new Runnable() {
-                @Override
-                public void run() {
-                    String displayTime = String.format(context.getResources().getQuantityText
-                            (R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(),
-                            snoozeMinutes);
-                    Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show();
-                }
-            };
-            mainHandler.post(myRunnable);
-        }
-
-        // Instance time changed, so find next alarm that will fire and notify system
-        updateNextAlarm(context);
-    }
-
-    /**
-     * This will set the alarm instance to the MISSED_STATE and update
-     * the application notifications and schedule any state changes that need
-     * to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setMissedState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting missed state to instance " + instance.mId);
-        // Stop alarm if this instance is firing it
-        AlarmService.stopAlarm(context, instance);
-
-        // Check parent if it needs to reschedule, disable or delete itself
-        if (instance.mAlarmId != null) {
-            updateParentAlarm(context, instance);
-        }
-
-        // Update alarm state
-        ContentResolver contentResolver = context.getContentResolver();
-        instance.mAlarmState = AlarmInstance.MISSED_STATE;
-        AlarmInstance.updateInstance(contentResolver, instance);
-
-        // Setup instance notification and scheduling timers
-        AlarmNotifications.showMissedNotification(context, instance);
-        scheduleInstanceStateChange(context, instance.getMissedTimeToLive(),
-                instance, AlarmInstance.DISMISSED_STATE);
-
-        // Instance is not valid anymore, so find next alarm that will fire and notify system
-        updateNextAlarm(context);
-    }
-
-    /**
-     * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state
-     * change to DISMISSED_STATE at the regularly scheduled firing time.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void setPreDismissState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting predismissed state to instance " + instance.mId);
-
-        // Update alarm in db
-        final ContentResolver contentResolver = context.getContentResolver();
-        instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE;
-        AlarmInstance.updateInstance(contentResolver, instance);
-
-        // Setup instance notification and scheduling timers
-        AlarmNotifications.clearNotification(context, instance);
-        scheduleInstanceStateChange(context, instance.getAlarmTime(), instance,
-                AlarmInstance.DISMISSED_STATE);
-
-        // Check parent if it needs to reschedule, disable or delete itself
-        if (instance.mAlarmId != null) {
-            updateParentAlarm(context, instance);
-        }
-
-        updateNextAlarm(context);
-    }
-
-    /**
-     * This just sets the alarm instance to DISMISSED_STATE.
-     */
-    public static void setDismissState(Context context, AlarmInstance instance) {
-        LogUtils.i("Setting dismissed state to instance " + instance.mId);
-        instance.mAlarmState = AlarmInstance.DISMISSED_STATE;
-        final ContentResolver contentResolver = context.getContentResolver();
-        AlarmInstance.updateInstance(contentResolver, instance);
-    }
-
-    /**
-     * This will delete the alarm instance, update the application notifications, and schedule
-     * any state changes that need to occur in the future.
-     *
-     * @param context  application context
-     * @param instance to set state to
-     */
-    public static void deleteInstanceAndUpdateParent(Context context, AlarmInstance instance) {
-        LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm.");
-
-        // Remove all other timers and notifications associated to it
-        unregisterInstance(context, instance);
-
-        // Check parent if it needs to reschedule, disable or delete itself
-        if (instance.mAlarmId != null) {
-            updateParentAlarm(context, instance);
-        }
-
-        // Delete instance as it is not needed anymore
-        AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
-
-        // Instance is not valid anymore, so find next alarm that will fire and notify system
-        updateNextAlarm(context);
-    }
-
-    /**
-     * This will set the instance state to DISMISSED_STATE and remove its notifications and
-     * alarm timers.
-     *
-     * @param context  application context
-     * @param instance to unregister
-     */
-    public static void unregisterInstance(Context context, AlarmInstance instance) {
-        LogUtils.i("Unregistering instance " + instance.mId);
-        // Stop alarm if this instance is firing it
-        AlarmService.stopAlarm(context, instance);
-        AlarmNotifications.clearNotification(context, instance);
-        cancelScheduledInstanceStateChange(context, instance);
-        setDismissState(context, instance);
-    }
-
-    /**
-     * This registers the AlarmInstance to the state manager. This will look at the instance
-     * and choose the most appropriate state to put it in. This is primarily used by new
-     * alarms, but it can also be called when the system time changes.
-     *
-     * Most state changes are handled by the states themselves, but during major time changes we
-     * have to correct the alarm instance state. This means we have to handle special cases as
-     * describe below:
-     *
-     * <ul>
-     *     <li>Make sure all dismissed alarms are never re-activated</li>
-     *     <li>Make sure pre-dismissed alarms stay predismissed</li>
-     *     <li>Make sure firing alarms stayed fired unless they should be auto-silenced</li>
-     *     <li>Missed instance that have parents should be re-enabled if we went back in time</li>
-     *     <li>If alarm was SNOOZED, then show the notification but don't update time</li>
-     *     <li>If low priority notification was hidden, then make sure it stays hidden</li>
-     * </ul>
-     *
-     * If none of these special case are found, then we just check the time and see what is the
-     * proper state for the instance.
-     *
-     * @param context  application context
-     * @param instance to register
-     */
-    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();
-        final Calendar lowNotificationTime = instance.getLowNotificationTime();
-        final Calendar highNotificationTime = instance.getHighNotificationTime();
-        final Calendar missedTTL = instance.getMissedTimeToLive();
-
-        // Handle special use cases here
-        if (instance.mAlarmState == AlarmInstance.DISMISSED_STATE) {
-            // This should never happen, but add a quick check here
-            LogUtils.e("Alarm Instance is dismissed, but never deleted");
-            deleteInstanceAndUpdateParent(context, instance);
-            return;
-        } else if (instance.mAlarmState == AlarmInstance.FIRED_STATE) {
-            // Keep alarm firing, unless it should be timed out
-            boolean hasTimeout = timeoutTime != null && currentTime.after(timeoutTime);
-            if (!hasTimeout) {
-                setFiredState(context, instance);
-                return;
-            }
-        } else if (instance.mAlarmState == AlarmInstance.MISSED_STATE) {
-            if (currentTime.before(alarmTime)) {
-                if (instance.mAlarmId == null) {
-                    LogUtils.i("Cannot restore missed instance for one-time alarm");
-                    // This instance parent got deleted (ie. deleteAfterUse), so
-                    // we should not re-activate it.-
-                    deleteInstanceAndUpdateParent(context, instance);
-                    return;
-                }
-
-                // TODO: This will re-activate missed snoozed alarms, but will
-                // use our normal notifications. This is not ideal, but very rare use-case.
-                // We should look into fixing this in the future.
-
-                // Make sure we re-enable the parent alarm of the instance
-                // because it will get activated by by the below code
-                alarm.enabled = true;
-                Alarm.updateAlarm(cr, alarm);
-            }
-        } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) {
-            if (currentTime.before(alarmTime)) {
-                setPreDismissState(context, instance);
-            } else {
-                deleteInstanceAndUpdateParent(context, instance);
-            }
-            return;
-        }
-
-        // Fix states that are time sensitive
-        if (currentTime.after(missedTTL)) {
-            // Alarm is so old, just dismiss it
-            deleteInstanceAndUpdateParent(context, instance);
-        } else if (currentTime.after(alarmTime)) {
-            // There is a chance that the TIME_SET occurred right when the alarm should go off, so
-            // we need to add a check to see if we should fire the alarm instead of marking it
-            // missed.
-            Calendar alarmBuffer = Calendar.getInstance();
-            alarmBuffer.setTime(alarmTime.getTime());
-            alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER);
-            if (currentTime.before(alarmBuffer)) {
-                setFiredState(context, instance);
-            } else {
-                setMissedState(context, instance);
-            }
-        } else if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) {
-            // We only want to display snooze notification and not update the time,
-            // so handle showing the notification directly
-            AlarmNotifications.showSnoozeNotification(context, instance);
-            scheduleInstanceStateChange(context, instance.getAlarmTime(),
-                    instance, AlarmInstance.FIRED_STATE);
-        } else if (currentTime.after(highNotificationTime)) {
-            setHighNotificationState(context, instance);
-        } else if (currentTime.after(lowNotificationTime)) {
-            // Only show low notification if it wasn't hidden in the past
-            if (instance.mAlarmState == AlarmInstance.HIDE_NOTIFICATION_STATE) {
-                setHideNotificationState(context, instance);
-            } else {
-                setLowNotificationState(context, instance);
-            }
-        } else {
-            // Alarm is still active, so initialize as a silent alarm
-            setSilentState(context, instance);
-        }
-
-        // The caller prefers to handle updateNextAlarm for optimization
-        if (updateNextAlarm) {
-            updateNextAlarm(context);
-        }
-    }
-
-    /**
-     * This will delete and unregister all instances associated with alarmId, without affect
-     * the alarm itself. This should be used whenever modifying or deleting an alarm.
-     *
-     * @param context application context
-     * @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) {
-            unregisterInstance(context, instance);
-            AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
-        }
-        updateNextAlarm(context);
-    }
-
-    /**
-     * Delete and unregister all instances unless they are snoozed. This is used whenever an alarm
-     * 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) {
-            if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) {
-                continue;
-            }
-            unregisterInstance(context, instance);
-            AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
-        }
-        updateNextAlarm(context);
-    }
-
-    /**
-     * Fix and update all alarm instance when a time change event occurs.
-     *
-     * @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();
-
-        // Sort the instances in reverse chronological order so that later instances are fixed or
-        // deleted before re-scheduling prior instances (which may re-create or update the later
-        // instances).
-        final List<AlarmInstance> instances = AlarmInstance.getInstances(
-                contentResolver, null /* selection */);
-        Collections.sort(instances, new Comparator<AlarmInstance>() {
-            @Override
-            public int compare(AlarmInstance lhs, AlarmInstance rhs) {
-                return rhs.getAlarmTime().compareTo(lhs.getAlarmTime());
-            }
-        });
-
-        for (AlarmInstance instance : instances) {
-            final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId);
-            if (alarm == null) {
-                unregisterInstance(context, instance);
-                AlarmInstance.deleteInstance(contentResolver, instance.mId);
-                LogUtils.e("Found instance without matching alarm; deleting instance %s", instance);
-                continue;
-            }
-            final Calendar priorAlarmTime = alarm.getPreviousAlarmTime(instance.getAlarmTime());
-            final Calendar missedTTLTime = instance.getMissedTimeToLive();
-            if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) {
-                final Calendar oldAlarmTime = instance.getAlarmTime();
-                final Calendar newAlarmTime = alarm.getNextAlarmTime(currentTime);
-                final CharSequence oldTime = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime);
-                final CharSequence newTime = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime);
-                LogUtils.i("A time change has caused an existing alarm scheduled to fire at %s to" +
-                        " be replaced by a new alarm scheduled to fire at %s", oldTime, newTime);
-
-                // The time change is so dramatic the AlarmInstance doesn't make any sense;
-                // remove it and schedule the new appropriate instance.
-                AlarmStateManager.deleteInstanceAndUpdateParent(context, instance);
-            } else {
-                registerInstance(context, instance, false /* updateNextAlarm */);
-            }
-        }
-
-        updateNextAlarm(context);
-    }
-
-    /**
-     * Utility method to set alarm instance state via constants.
-     *
-     * @param context  application context
-     * @param instance to change state on
-     * @param state    to change to
-     */
-    private static void setAlarmState(Context context, AlarmInstance instance, int state) {
-        if (instance == null) {
-            LogUtils.e("Null alarm instance while setting state to %d", state);
-            return;
-        }
-        switch (state) {
-            case AlarmInstance.SILENT_STATE:
-                setSilentState(context, instance);
-                break;
-            case AlarmInstance.LOW_NOTIFICATION_STATE:
-                setLowNotificationState(context, instance);
-                break;
-            case AlarmInstance.HIDE_NOTIFICATION_STATE:
-                setHideNotificationState(context, instance);
-                break;
-            case AlarmInstance.HIGH_NOTIFICATION_STATE:
-                setHighNotificationState(context, instance);
-                break;
-            case AlarmInstance.FIRED_STATE:
-                setFiredState(context, instance);
-                break;
-            case AlarmInstance.SNOOZE_STATE:
-                setSnoozeState(context, instance, true /* showToast */);
-                break;
-            case AlarmInstance.MISSED_STATE:
-                setMissedState(context, instance);
-                break;
-            case AlarmInstance.PREDISMISSED_STATE:
-                setPreDismissState(context, instance);
-                break;
-            case AlarmInstance.DISMISSED_STATE:
-                deleteInstanceAndUpdateParent(context, instance);
-                break;
-            default:
-                LogUtils.e("Trying to change to unknown alarm state: " + state);
-        }
-    }
-
-    @Override
-    public void onReceive(final Context context, final Intent intent) {
-        if (INDICATOR_ACTION.equals(intent.getAction())) {
-            return;
-        }
-
-        final PendingResult result = goAsync();
-        final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
-        wl.acquire();
-        AsyncHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                handleIntent(context, intent);
-                result.finish();
-                wl.release();
-            }
-        });
-    }
-
-    public static void handleIntent(Context context, Intent intent) {
-        final String action = intent.getAction();
-        LogUtils.v("AlarmStateManager received intent " + intent);
-        if (CHANGE_STATE_ACTION.equals(action)) {
-            Uri uri = intent.getData();
-            AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
-                    AlarmInstance.getId(uri));
-            if (instance == null) {
-                LogUtils.e("Can not change state for unknown instance: " + uri);
-                return;
-            }
-
-            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) {
-                LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId + " AlarmState: " +
-                        alarmState);
-                // Allows dismiss/snooze requests to go through
-                if (!intent.hasCategory(ALARM_DISMISS_TAG) &&
-                        !intent.hasCategory(ALARM_SNOOZE_TAG)) {
-                    LogUtils.i("Ignoring old Intent");
-                    return;
-                }
-            }
-
-            if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
-                if (intent.hasCategory(ALARM_DISMISS_TAG)) {
-                    Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification);
-                } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) {
-                    Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification);
-                }
-            }
-
-            if (alarmState >= 0) {
-                setAlarmState(context, instance, alarmState);
-            } else {
-                registerInstance(context, instance, true);
-            }
-        } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) {
-            Uri uri = intent.getData();
-            AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
-                    AlarmInstance.getId(uri));
-
-            if (instance == null) {
-                LogUtils.e("Null alarminstance for SHOW_AND_DISMISS");
-                // dismiss the notification
-                final int id = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1);
-                if (id != -1) {
-                    NotificationManagerCompat.from(context).cancel(id);
-                }
-                return;
-            }
-
-            long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
-            final Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId)
-                    .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
-                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
-            // Open DeskClock which is now positioned on the alarms tab.
-            context.startActivity(viewAlarmIntent);
-
-            deleteInstanceAndUpdateParent(context, instance);
-        }
-    }
-
-    /**
-     * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm
-     * indicators.
-     */
-    public static Intent createIndicatorIntent(Context context) {
-        return new Intent(context, AlarmStateManager.class).setAction(INDICATOR_ACTION);
-    }
-
-    /**
-     * Abstract away how the current time is computed. If no implementation of this interface is
-     * given the default is to return {@link Calendar#getInstance()}. Otherwise, the factory
-     * instance is consulted for the current time.
-     */
-    interface CurrentTimeFactory {
-        Calendar getCurrentTime();
-    }
-
-    /**
-     * Abstracts away how state changes are scheduled. The {@link AlarmManagerStateChangeScheduler}
-     * implementation schedules callbacks within the system AlarmManager. Alternate
-     * implementations, such as test case mocks can subvert this behavior.
-     */
-    interface StateChangeScheduler {
-        void scheduleInstanceStateChange(Context context, Calendar time,
-                AlarmInstance instance, int newState);
-
-        void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance);
-    }
-
-    /**
-     * Schedules state change callbacks within the AlarmManager.
-     */
-    private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler {
-        @Override
-        public void scheduleInstanceStateChange(Context context, Calendar time,
-                AlarmInstance instance, int newState) {
-            final long timeInMillis = time.getTimeInMillis();
-            LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
-                    instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis);
-            final Intent stateChangeIntent =
-                    createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState);
-            // Treat alarm state change as high priority, use foreground broadcasts
-            stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
-            PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
-                    stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
-            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);
-            } else {
-                am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
-            }
-        }
-
-        @Override
-        public void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance) {
-            LogUtils.v("Canceling instance " + instance.mId + " timers");
-
-            // Create a PendingIntent that will match any one set for this instance
-            PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
-                    createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null),
-                    PendingIntent.FLAG_NO_CREATE);
-
-            if (pendingIntent != null) {
-                AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
-                am.cancel(pendingIntent);
-                pendingIntent.cancel();
-            }
-        }
-    }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmStateManager.kt b/src/com/android/deskclock/alarms/AlarmStateManager.kt
new file mode 100644
index 0000000..2478ef5
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmStateManager.kt
@@ -0,0 +1,1011 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.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.Context.ALARM_SERVICE
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Handler
+import android.os.PowerManager
+import android.provider.Settings
+import android.provider.Settings.System.NEXT_ALARM_FORMATTED
+import android.text.format.DateFormat
+import android.widget.Toast
+import androidx.core.app.NotificationManagerCompat
+
+import com.android.deskclock.AlarmAlertWakeLock
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.AsyncHandler
+import com.android.deskclock.DeskClock
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.LogUtils
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.util.Calendar
+
+/**
+ * This class handles all the state changes for alarm instances. You need to
+ * register all alarm instances with the state manager if you want them to
+ * be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE),
+ * then you must also re-register instances to fix their states.
+ *
+ * Please see [) for special transitions when major time changes][.registerInstance]
+ */
+class AlarmStateManager : BroadcastReceiver() {
+
+    override fun onReceive(context: Context, intent: Intent) {
+        if (INDICATOR_ACTION == intent.getAction()) {
+            return
+        }
+
+        val result: PendingResult = goAsync()
+        val wl: PowerManager.WakeLock = AlarmAlertWakeLock.createPartialWakeLock(context)
+        wl.acquire()
+        AsyncHandler.post {
+            handleIntent(context, intent)
+            result.finish()
+            wl.release()
+        }
+    }
+
+    /**
+     * Abstract away how the current time is computed. If no implementation of this interface is
+     * given the default is to return [Calendar.getInstance]. Otherwise, the factory
+     * instance is consulted for the current time.
+     */
+    interface CurrentTimeFactory {
+        val currentTime: Calendar
+    }
+
+    /**
+     * Abstracts away how state changes are scheduled. The [AlarmManagerStateChangeScheduler]
+     * implementation schedules callbacks within the system AlarmManager. Alternate
+     * implementations, such as test case mocks can subvert this behavior.
+     */
+    interface StateChangeScheduler {
+        fun scheduleInstanceStateChange(
+            context: Context,
+            time: Calendar,
+            instance: AlarmInstance,
+            newState: Int
+        )
+
+        fun cancelScheduledInstanceStateChange(context: Context, instance: AlarmInstance)
+    }
+
+    /**
+     * Schedules state change callbacks within the AlarmManager.
+     */
+    private class AlarmManagerStateChangeScheduler : StateChangeScheduler {
+        override fun scheduleInstanceStateChange(
+            context: Context,
+            time: Calendar,
+            instance: AlarmInstance,
+            newState: Int
+        ) {
+            val timeInMillis = time.timeInMillis
+            LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
+                    instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis)
+            val stateChangeIntent: Intent =
+                    createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState)
+            // Treat alarm state change as high priority, use foreground broadcasts
+            stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+            val pendingIntent: PendingIntent =
+                    PendingIntent.getService(context, instance.hashCode(),
+                    stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+
+            val am: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager
+            if (Utils.isMOrLater) {
+                // Ensure the alarm fires even if the device is dozing.
+                am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
+            } else {
+                am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
+            }
+        }
+
+        override fun cancelScheduledInstanceStateChange(context: Context, instance: AlarmInstance) {
+            LogUtils.v("Canceling instance " + instance.mId + " timers")
+
+            // Create a PendingIntent that will match any one set for this instance
+            val pendingIntent: PendingIntent? =
+                    PendingIntent.getService(context, instance.hashCode(),
+                    createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null),
+                    PendingIntent.FLAG_NO_CREATE)
+
+            pendingIntent?.let {
+                val am: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager
+                am.cancel(it)
+                it.cancel()
+            }
+        }
+    }
+
+    companion object {
+        // Intent action to trigger an instance state change.
+        const val CHANGE_STATE_ACTION = "change_state"
+
+        // Intent action to show the alarm and dismiss the instance
+        const val SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm"
+
+        // Intent action for an AlarmManager alarm serving only to set the next alarm indicators
+        private const val INDICATOR_ACTION = "indicator"
+
+        // System intent action to notify AppWidget that we changed the alarm text.
+        const val ACTION_ALARM_CHANGED = "com.android.deskclock.ALARM_CHANGED"
+
+        // Extra key to set the desired state change.
+        const val ALARM_STATE_EXTRA = "intent.extra.alarm.state"
+
+        // Extra key to indicate the state change was launched from a notification.
+        const val FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification"
+
+        // Extra key to set the global broadcast id.
+        private const val ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id"
+
+        // Intent category tags used to dismiss, snooze or delete an alarm
+        const val ALARM_DISMISS_TAG = "DISMISS_TAG"
+        const val ALARM_SNOOZE_TAG = "SNOOZE_TAG"
+        const val ALARM_DELETE_TAG = "DELETE_TAG"
+
+        // Intent category tag used when schedule state change intents in alarm manager.
+        private const val ALARM_MANAGER_TAG = "ALARM_MANAGER"
+
+        // Buffer time in seconds to fire alarm instead of marking it missed.
+        const val ALARM_FIRE_BUFFER = 15
+
+        // A factory for the current time; can be mocked for testing purposes.
+        private var sCurrentTimeFactory: CurrentTimeFactory? = null
+
+        // Schedules alarm state transitions; can be mocked for testing purposes.
+        private var sStateChangeScheduler: StateChangeScheduler = AlarmManagerStateChangeScheduler()
+
+        private val currentTime: Calendar
+            get() = (if (sCurrentTimeFactory == null) {
+                DataModel.dataModel.calendar
+            } else {
+                sCurrentTimeFactory!!.currentTime
+            })
+
+        fun setCurrentTimeFactory(currentTimeFactory: CurrentTimeFactory?) {
+            sCurrentTimeFactory = currentTimeFactory
+        }
+
+        fun setStateChangeScheduler(stateChangeScheduler: StateChangeScheduler?) {
+            sStateChangeScheduler = stateChangeScheduler ?: AlarmManagerStateChangeScheduler()
+        }
+
+        /**
+         * Update the next alarm stored in framework. This value is also displayed in digital
+         * widgets and the clock tab in this app.
+         */
+        private fun updateNextAlarm(context: Context) {
+            val nextAlarm = getNextFiringAlarm(context)
+
+            if (Utils.isPreL) {
+                updateNextAlarmInSystemSettings(context, nextAlarm)
+            } else {
+                updateNextAlarmInAlarmManager(context, nextAlarm)
+            }
+        }
+
+        /**
+         * Returns an alarm instance of an alarm that's going to fire next.
+         *
+         * @param context application context
+         * @return an alarm instance that will fire earliest relative to current time.
+         */
+        @JvmStatic
+        fun getNextFiringAlarm(context: Context): AlarmInstance? {
+            val cr: ContentResolver = context.getContentResolver()
+            val activeAlarmQuery: String =
+                    InstancesColumns.ALARM_STATE + "<" + InstancesColumns.FIRED_STATE
+            val alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery)
+
+            var nextAlarm: AlarmInstance? = null
+            for (instance in alarmInstances) {
+                if (nextAlarm == null || instance.alarmTime.before(nextAlarm.alarmTime)) {
+                    nextAlarm = instance
+                }
+            }
+            return nextAlarm
+        }
+
+        /**
+         * Used in pre-L devices, where "next alarm" is stored in system settings.
+         */
+        @TargetApi(Build.VERSION_CODES.KITKAT)
+        private fun updateNextAlarmInSystemSettings(context: Context, nextAlarm: AlarmInstance?) {
+            // Format the next alarm time if an alarm is scheduled.
+            var time = ""
+            if (nextAlarm != null) {
+                time = AlarmUtils.getFormattedTime(context, nextAlarm.alarmTime)
+            }
+
+            try {
+                // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions
+                Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time)
+                LogUtils.i("Updated next alarm time to: '$time'")
+
+                // Send broadcast message so pre-L AppWidgets will recognize an update.
+                context.sendBroadcast(Intent(ACTION_ALARM_CHANGED))
+            } catch (se: SecurityException) {
+                // The user has most likely revoked WRITE_SETTINGS.
+                LogUtils.e("Unable to update next alarm to: '$time'", se)
+            }
+        }
+
+        /**
+         * Used in L and later devices where "next alarm" is stored in the Alarm Manager.
+         */
+        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+        private fun updateNextAlarmInAlarmManager(context: Context, nextAlarm: AlarmInstance?) {
+            // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the
+            // alarm that is going to fire next. The operation is constructed such that it is
+            // ignored by AlarmStateManager.
+
+            val alarmManager: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager
+
+            val flags = if (nextAlarm == null) PendingIntent.FLAG_NO_CREATE else 0
+            val operation: PendingIntent? = PendingIntent.getBroadcast(context, 0 /* requestCode */,
+                    createIndicatorIntent(context), flags)
+
+            if (nextAlarm != null) {
+                LogUtils.i("Setting upcoming AlarmClockInfo for alarm: " + nextAlarm.mId)
+                val alarmTime: Long = nextAlarm.alarmTime.timeInMillis
+
+                // Create an intent that can be used to show or edit details of the next alarm.
+                val viewIntent: PendingIntent =
+                        PendingIntent.getActivity(context, nextAlarm.hashCode(),
+                        AlarmNotifications.createViewAlarmIntent(context, nextAlarm),
+                        PendingIntent.FLAG_UPDATE_CURRENT)
+
+                val info = AlarmClockInfo(alarmTime, viewIntent)
+                Utils.updateNextAlarm(alarmManager, info, operation)
+            } else if (operation != null) {
+                LogUtils.i("Canceling upcoming AlarmClockInfo")
+                alarmManager.cancel(operation)
+            }
+        }
+
+        /**
+         * Used by dismissed and missed states, to update parent alarm. This will either
+         * disable, delete or reschedule parent alarm.
+         *
+         * @param context application context
+         * @param instance to update parent for
+         */
+        private fun updateParentAlarm(context: Context, instance: AlarmInstance) {
+            val cr: ContentResolver = context.getContentResolver()
+            val alarm = Alarm.getAlarm(cr, instance.mAlarmId!!)
+            if (alarm == null) {
+                LogUtils.e("Parent has been deleted with instance: $instance")
+                return
+            }
+
+            if (!alarm.daysOfWeek.isRepeating) {
+                if (alarm.deleteAfterUse) {
+                    LogUtils.i("Deleting parent alarm: " + alarm.id)
+                    Alarm.deleteAlarm(cr, alarm.id)
+                } else {
+                    LogUtils.i("Disabling parent alarm: " + alarm.id)
+                    alarm.enabled = false
+                    Alarm.updateAlarm(cr, alarm)
+                }
+            } else {
+                // Schedule the next repeating instance which may be before the current instance if
+                // a time jump has occurred. Otherwise, if the current instance is the next instance
+                // and has already been fired, schedule the subsequent instance.
+                var nextRepeatedInstance = alarm.createInstanceAfter(currentTime)
+                if (instance.mAlarmState > InstancesColumns.FIRED_STATE &&
+                        nextRepeatedInstance.alarmTime == instance.alarmTime) {
+                    nextRepeatedInstance = alarm.createInstanceAfter(instance.alarmTime)
+                }
+
+                LogUtils.i("Creating new instance for repeating alarm " + alarm.id +
+                        " at " +
+                        AlarmUtils.getFormattedTime(context, nextRepeatedInstance.alarmTime))
+                AlarmInstance.addInstance(cr, nextRepeatedInstance)
+                registerInstance(context, nextRepeatedInstance, true)
+            }
+        }
+
+        /**
+         * Utility method to create a proper change state intent.
+         *
+         * @param context application context
+         * @param tag used to make intent differ from other state change intents.
+         * @param instance to change state to
+         * @param state to change to.
+         * @return intent that can be used to change an alarm instance state
+         */
+        fun createStateChangeIntent(
+            context: Context?,
+            tag: String?,
+            instance: AlarmInstance,
+            state: Int?
+        ): Intent {
+            // This intent is directed to AlarmService, though the actual handling of it occurs here
+            // in AlarmStateManager. The reason is that evidence exists showing the jump between the
+            // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by
+            // the Out Of Memory killer. If clock is killed during that jump, firing an alarm can
+            // fail to occur. To be safer, the call begins in AlarmService, which has the power to
+            // display the firing alarm if needed, so no jump is needed.
+            val intent: Intent =
+                    AlarmInstance.createIntent(context, AlarmService::class.java, instance.mId)
+            intent.setAction(CHANGE_STATE_ACTION)
+            intent.addCategory(tag)
+            intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.dataModel.globalIntentId)
+            if (state != null) {
+                intent.putExtra(ALARM_STATE_EXTRA, state.toInt())
+            }
+            return intent
+        }
+
+        /**
+         * Schedule alarm instance state changes with [AlarmManager].
+         *
+         * @param ctx application context
+         * @param time to trigger state change
+         * @param instance to change state to
+         * @param newState to change to
+         */
+        private fun scheduleInstanceStateChange(
+            ctx: Context,
+            time: Calendar,
+            instance: AlarmInstance,
+            newState: Int
+        ) {
+            sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState)
+        }
+
+        /**
+         * Cancel all [AlarmManager] timers for instance.
+         *
+         * @param ctx application context
+         * @param instance to disable all [AlarmManager] timers
+         */
+        private fun cancelScheduledInstanceStateChange(ctx: Context, instance: AlarmInstance) {
+            sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance)
+        }
+
+        /**
+         * This will set the alarm instance to the SILENT_STATE and update
+         * the application notifications and schedule any state changes that need
+         * to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        private fun setSilentState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting silent state to instance " + instance.mId)
+
+            // Update alarm in db
+            val contentResolver: ContentResolver = context.getContentResolver()
+            instance.mAlarmState = InstancesColumns.SILENT_STATE
+            AlarmInstance.updateInstance(contentResolver, instance)
+
+            // Setup instance notification and scheduling timers
+            AlarmNotifications.clearNotification(context, instance)
+            scheduleInstanceStateChange(context, instance.lowNotificationTime,
+                    instance, InstancesColumns.LOW_NOTIFICATION_STATE)
+        }
+
+        /**
+         * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update
+         * the application notifications and schedule any state changes that need
+         * to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        private fun setLowNotificationState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting low notification state to instance " + instance.mId)
+
+            // Update alarm state in db
+            val contentResolver: ContentResolver = context.getContentResolver()
+            instance.mAlarmState = InstancesColumns.LOW_NOTIFICATION_STATE
+            AlarmInstance.updateInstance(contentResolver, instance)
+
+            // Setup instance notification and scheduling timers
+            AlarmNotifications.showLowPriorityNotification(context, instance)
+            scheduleInstanceStateChange(context, instance.highNotificationTime,
+                    instance, InstancesColumns.HIGH_NOTIFICATION_STATE)
+        }
+
+        /**
+         * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update
+         * the application notifications and schedule any state changes that need
+         * to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        private fun setHideNotificationState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting hide notification state to instance " + instance.mId)
+
+            // Update alarm state in db
+            val contentResolver: ContentResolver = context.getContentResolver()
+            instance.mAlarmState = InstancesColumns.HIDE_NOTIFICATION_STATE
+            AlarmInstance.updateInstance(contentResolver, instance)
+
+            // Setup instance notification and scheduling timers
+            AlarmNotifications.clearNotification(context, instance)
+            scheduleInstanceStateChange(context, instance.highNotificationTime,
+                    instance, InstancesColumns.HIGH_NOTIFICATION_STATE)
+        }
+
+        /**
+         * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update
+         * the application notifications and schedule any state changes that need
+         * to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        private fun setHighNotificationState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting high notification state to instance " + instance.mId)
+
+            // Update alarm state in db
+            val contentResolver: ContentResolver = context.getContentResolver()
+            instance.mAlarmState = InstancesColumns.HIGH_NOTIFICATION_STATE
+            AlarmInstance.updateInstance(contentResolver, instance)
+
+            // Setup instance notification and scheduling timers
+            AlarmNotifications.showHighPriorityNotification(context, instance)
+            scheduleInstanceStateChange(context, instance.alarmTime,
+                    instance, InstancesColumns.FIRED_STATE)
+        }
+
+        /**
+         * This will set the alarm instance to the FIRED_STATE and update
+         * the application notifications and schedule any state changes that need
+         * to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        private fun setFiredState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting fire state to instance " + instance.mId)
+
+            // Update alarm state in db
+            val contentResolver: ContentResolver = context.getContentResolver()
+            instance.mAlarmState = InstancesColumns.FIRED_STATE
+            AlarmInstance.updateInstance(contentResolver, instance)
+
+            instance.mAlarmId?.let {
+                // if the time changed *backward* and pushed an instance from missed back to fired,
+                // remove any other scheduled instances that may exist
+                AlarmInstance.deleteOtherInstances(context, contentResolver, it, instance.mId)
+            }
+
+            Events.sendAlarmEvent(R.string.action_fire, 0)
+
+            val timeout: Calendar? = instance.timeout
+            timeout?.let {
+                scheduleInstanceStateChange(context, it, instance, InstancesColumns.MISSED_STATE)
+            }
+
+            // Instance not valid anymore, so find next alarm that will fire and notify system
+            updateNextAlarm(context)
+        }
+
+        /**
+         * This will set the alarm instance to the SNOOZE_STATE and update
+         * the application notifications and schedule any state changes that need
+         * to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        @JvmStatic
+        fun setSnoozeState(
+            context: Context,
+            instance: AlarmInstance,
+            showToast: Boolean
+        ) {
+            // Stop alarm if this instance is firing it
+            AlarmService.stopAlarm(context, instance)
+
+            // Calculate the new snooze alarm time
+            val snoozeMinutes = DataModel.dataModel.snoozeLength
+            val newAlarmTime = Calendar.getInstance()
+            newAlarmTime.add(Calendar.MINUTE, snoozeMinutes)
+
+            // Update alarm state and new alarm time in db.
+            LogUtils.i("Setting snoozed state to instance " + instance.mId + " for " +
+                    AlarmUtils.getFormattedTime(context, newAlarmTime))
+            instance.alarmTime = newAlarmTime
+            instance.mAlarmState = InstancesColumns.SNOOZE_STATE
+            AlarmInstance.updateInstance(context.getContentResolver(), instance)
+
+            // Setup instance notification and scheduling timers
+            AlarmNotifications.showSnoozeNotification(context, instance)
+            scheduleInstanceStateChange(context, instance.alarmTime,
+                    instance, InstancesColumns.FIRED_STATE)
+
+            // Display the snooze minutes in a toast.
+            if (showToast) {
+                val mainHandler = Handler(context.getMainLooper())
+                val myRunnable = Runnable {
+                    val displayTime =
+                        String.format(
+                            context
+                                    .getResources()
+                                    .getQuantityText(R.plurals.alarm_alert_snooze_set,
+                                            snoozeMinutes)
+                                    .toString(),
+                            snoozeMinutes)
+                    Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show()
+                }
+                mainHandler.post(myRunnable)
+            }
+
+            // Instance time changed, so find next alarm that will fire and notify system
+            updateNextAlarm(context)
+        }
+
+        /**
+         * This will set the alarm instance to the MISSED_STATE and update
+         * the application notifications and schedule any state changes that need
+         * to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        fun setMissedState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting missed state to instance " + instance.mId)
+            // Stop alarm if this instance is firing it
+            AlarmService.stopAlarm(context, instance)
+
+            // Check parent if it needs to reschedule, disable or delete itself
+            if (instance.mAlarmId != null) {
+                updateParentAlarm(context, instance)
+            }
+
+            // Update alarm state
+            val contentResolver: ContentResolver = context.getContentResolver()
+            instance.mAlarmState = InstancesColumns.MISSED_STATE
+            AlarmInstance.updateInstance(contentResolver, instance)
+
+            // Setup instance notification and scheduling timers
+            AlarmNotifications.showMissedNotification(context, instance)
+            scheduleInstanceStateChange(context, instance.missedTimeToLive,
+                    instance, InstancesColumns.DISMISSED_STATE)
+
+            // Instance is not valid anymore, so find next alarm that will fire and notify system
+            updateNextAlarm(context)
+        }
+
+        /**
+         * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state
+         * change to DISMISSED_STATE at the regularly scheduled firing time.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        @JvmStatic
+        fun setPreDismissState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting predismissed state to instance " + instance.mId)
+
+            // Update alarm in db
+            val contentResolver: ContentResolver = context.getContentResolver()
+            instance.mAlarmState = InstancesColumns.PREDISMISSED_STATE
+            AlarmInstance.updateInstance(contentResolver, instance)
+
+            // Setup instance notification and scheduling timers
+            AlarmNotifications.clearNotification(context, instance)
+            scheduleInstanceStateChange(context, instance.alarmTime, instance,
+                    InstancesColumns.DISMISSED_STATE)
+
+            // Check parent if it needs to reschedule, disable or delete itself
+            if (instance.mAlarmId != null) {
+                updateParentAlarm(context, instance)
+            }
+
+            updateNextAlarm(context)
+        }
+
+        /**
+         * This just sets the alarm instance to DISMISSED_STATE.
+         */
+        private fun setDismissState(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Setting dismissed state to instance " + instance.mId)
+            instance.mAlarmState = InstancesColumns.DISMISSED_STATE
+            val contentResolver: ContentResolver = context.getContentResolver()
+            AlarmInstance.updateInstance(contentResolver, instance)
+        }
+
+        /**
+         * This will delete the alarm instance, update the application notifications, and schedule
+         * any state changes that need to occur in the future.
+         *
+         * @param context application context
+         * @param instance to set state to
+         */
+        @JvmStatic
+        fun deleteInstanceAndUpdateParent(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm.")
+
+            // Remove all other timers and notifications associated to it
+            unregisterInstance(context, instance)
+
+            // Check parent if it needs to reschedule, disable or delete itself
+            if (instance.mAlarmId != null) {
+                updateParentAlarm(context, instance)
+            }
+
+            // Delete instance as it is not needed anymore
+            AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId)
+
+            // Instance is not valid anymore, so find next alarm that will fire and notify system
+            updateNextAlarm(context)
+        }
+
+        /**
+         * This will set the instance state to DISMISSED_STATE and remove its notifications and
+         * alarm timers.
+         *
+         * @param context application context
+         * @param instance to unregister
+         */
+        fun unregisterInstance(context: Context, instance: AlarmInstance) {
+            LogUtils.i("Unregistering instance " + instance.mId)
+            // Stop alarm if this instance is firing it
+            AlarmService.stopAlarm(context, instance)
+            AlarmNotifications.clearNotification(context, instance)
+            cancelScheduledInstanceStateChange(context, instance)
+            setDismissState(context, instance)
+        }
+
+        /**
+         * This registers the AlarmInstance to the state manager. This will look at the instance
+         * and choose the most appropriate state to put it in. This is primarily used by new
+         * alarms, but it can also be called when the system time changes.
+         *
+         * Most state changes are handled by the states themselves, but during major time changes we
+         * have to correct the alarm instance state. This means we have to handle special cases as
+         * describe below:
+         *
+         *
+         *  * Make sure all dismissed alarms are never re-activated
+         *  * Make sure pre-dismissed alarms stay predismissed
+         *  * Make sure firing alarms stayed fired unless they should be auto-silenced
+         *  * Missed instance that have parents should be re-enabled if we went back in time
+         *  * If alarm was SNOOZED, then show the notification but don't update time
+         *  * If low priority notification was hidden, then make sure it stays hidden
+         *
+         *
+         * If none of these special case are found, then we just check the time and see what is the
+         * proper state for the instance.
+         *
+         * @param context application context
+         * @param instance to register
+         */
+        @JvmStatic
+        fun registerInstance(
+            context: Context,
+            instance: AlarmInstance,
+            updateNextAlarm: Boolean
+        ) {
+            LogUtils.i("Registering instance: " + instance.mId)
+            val cr: ContentResolver = context.getContentResolver()
+            val alarm = Alarm.getAlarm(cr, instance.mAlarmId!!)
+            val currentTime = currentTime
+            val alarmTime: Calendar = instance.alarmTime
+            val timeoutTime: Calendar? = instance.timeout
+            val lowNotificationTime: Calendar = instance.lowNotificationTime
+            val highNotificationTime: Calendar = instance.highNotificationTime
+            val missedTTL: Calendar = instance.missedTimeToLive
+
+            // Handle special use cases here
+            if (instance.mAlarmState == InstancesColumns.DISMISSED_STATE) {
+                // This should never happen, but add a quick check here
+                LogUtils.e("Alarm Instance is dismissed, but never deleted")
+                deleteInstanceAndUpdateParent(context, instance)
+                return
+            } else if (instance.mAlarmState == InstancesColumns.FIRED_STATE) {
+                // Keep alarm firing, unless it should be timed out
+                val hasTimeout = timeoutTime != null && currentTime.after(timeoutTime)
+                if (!hasTimeout) {
+                    setFiredState(context, instance)
+                    return
+                }
+            } else if (instance.mAlarmState == InstancesColumns.MISSED_STATE) {
+                if (currentTime.before(alarmTime)) {
+                    if (instance.mAlarmId == null) {
+                        LogUtils.i("Cannot restore missed instance for one-time alarm")
+                        // This instance parent got deleted (ie. deleteAfterUse), so
+                        // we should not re-activate it.-
+                        deleteInstanceAndUpdateParent(context, instance)
+                        return
+                    }
+
+                    // TODO: This will re-activate missed snoozed alarms, but will
+                    // use our normal notifications. This is not ideal, but very rare use-case.
+                    // We should look into fixing this in the future.
+
+                    // Make sure we re-enable the parent alarm of the instance
+                    // because it will get activated by by the below code
+                    alarm!!.enabled = true
+                    Alarm.updateAlarm(cr, alarm)
+                }
+            } else if (instance.mAlarmState == InstancesColumns.PREDISMISSED_STATE) {
+                if (currentTime.before(alarmTime)) {
+                    setPreDismissState(context, instance)
+                } else {
+                    deleteInstanceAndUpdateParent(context, instance)
+                }
+                return
+            }
+
+            // Fix states that are time sensitive
+            if (currentTime.after(missedTTL)) {
+                // Alarm is so old, just dismiss it
+                deleteInstanceAndUpdateParent(context, instance)
+            } else if (currentTime.after(alarmTime)) {
+                // There is a chance that the TIME_SET occurred right when the alarm should go off,
+                // so we need to add a check to see if we should fire the alarm instead of marking
+                // it missed.
+                val alarmBuffer = Calendar.getInstance()
+                alarmBuffer.time = alarmTime.time
+                alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER)
+                if (currentTime.before(alarmBuffer)) {
+                    setFiredState(context, instance)
+                } else {
+                    setMissedState(context, instance)
+                }
+            } else if (instance.mAlarmState == InstancesColumns.SNOOZE_STATE) {
+                // We only want to display snooze notification and not update the time,
+                // so handle showing the notification directly
+                AlarmNotifications.showSnoozeNotification(context, instance)
+                scheduleInstanceStateChange(context, instance.alarmTime,
+                        instance, InstancesColumns.FIRED_STATE)
+            } else if (currentTime.after(highNotificationTime)) {
+                setHighNotificationState(context, instance)
+            } else if (currentTime.after(lowNotificationTime)) {
+                // Only show low notification if it wasn't hidden in the past
+                if (instance.mAlarmState == InstancesColumns.HIDE_NOTIFICATION_STATE) {
+                    setHideNotificationState(context, instance)
+                } else {
+                    setLowNotificationState(context, instance)
+                }
+            } else {
+                // Alarm is still active, so initialize as a silent alarm
+                setSilentState(context, instance)
+            }
+
+            // The caller prefers to handle updateNextAlarm for optimization
+            if (updateNextAlarm) {
+                updateNextAlarm(context)
+            }
+        }
+
+        /**
+         * This will delete and unregister all instances associated with alarmId, without affect
+         * the alarm itself. This should be used whenever modifying or deleting an alarm.
+         *
+         * @param context application context
+         * @param alarmId to find instances to delete.
+         */
+        @JvmStatic
+        fun deleteAllInstances(context: Context, alarmId: Long) {
+            LogUtils.i("Deleting all instances of alarm: $alarmId")
+            val cr: ContentResolver = context.getContentResolver()
+            val instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId)
+            for (instance in instances) {
+                unregisterInstance(context, instance)
+                AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId)
+            }
+            updateNextAlarm(context)
+        }
+
+        /**
+         * Delete and unregister all instances unless they are snoozed. This is used whenever an
+         * alarm is modified superficially (label, vibrate, or ringtone change).
+         */
+        fun deleteNonSnoozeInstances(context: Context, alarmId: Long) {
+            LogUtils.i("Deleting all non-snooze instances of alarm: $alarmId")
+            val cr: ContentResolver = context.getContentResolver()
+            val instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId)
+            for (instance in instances) {
+                if (instance.mAlarmState == InstancesColumns.SNOOZE_STATE) {
+                    continue
+                }
+                unregisterInstance(context, instance)
+                AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId)
+            }
+            updateNextAlarm(context)
+        }
+
+        /**
+         * Fix and update all alarm instance when a time change event occurs.
+         *
+         * @param context application context
+         */
+        @JvmStatic
+        fun fixAlarmInstances(context: Context) {
+            LogUtils.i("Fixing alarm instances")
+            // Register all instances after major time changes or when phone restarts
+            val contentResolver: ContentResolver = context.getContentResolver()
+            val currentTime = currentTime
+
+            // Sort the instances in reverse chronological order so that later instances are fixed
+            // or deleted before re-scheduling prior instances (which may re-create or update the
+            // later instances).
+            val instances = AlarmInstance.getInstances(
+                    contentResolver, null /* selection */)
+            instances.sortWith(Comparator { lhs, rhs -> rhs.alarmTime.compareTo(lhs.alarmTime) })
+
+            for (instance in instances) {
+                val alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId!!)
+                if (alarm == null) {
+                    unregisterInstance(context, instance)
+                    AlarmInstance.deleteInstance(contentResolver, instance.mId)
+                    LogUtils.e("Found instance without matching alarm; deleting instance %s",
+                            instance)
+                    continue
+                }
+                val priorAlarmTime = alarm.getPreviousAlarmTime(instance.alarmTime)
+                val missedTTLTime: Calendar = instance.missedTimeToLive
+                if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) {
+                    val oldAlarmTime: Calendar = instance.alarmTime
+                    val newAlarmTime = alarm.getNextAlarmTime(currentTime)
+                    val oldTime: CharSequence =
+                            DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime)
+                    val newTime: CharSequence =
+                            DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime)
+                    LogUtils.i("A time change has caused an existing alarm scheduled" +
+                            " to fire at %s to be replaced by a new alarm scheduled to fire at %s",
+                            oldTime, newTime)
+
+                    // The time change is so dramatic the AlarmInstance doesn't make any sense;
+                    // remove it and schedule the new appropriate instance.
+                    deleteInstanceAndUpdateParent(context, instance)
+                } else {
+                    registerInstance(context, instance, false /* updateNextAlarm */)
+                }
+            }
+
+            updateNextAlarm(context)
+        }
+
+        /**
+         * Utility method to set alarm instance state via constants.
+         *
+         * @param context application context
+         * @param instance to change state on
+         * @param state to change to
+         */
+        private fun setAlarmState(context: Context, instance: AlarmInstance?, state: Int) {
+            if (instance == null) {
+                LogUtils.e("Null alarm instance while setting state to %d", state)
+                return
+            }
+            when (state) {
+                InstancesColumns.SILENT_STATE -> setSilentState(context, instance)
+                InstancesColumns.LOW_NOTIFICATION_STATE -> {
+                    setLowNotificationState(context, instance)
+                }
+                InstancesColumns.HIDE_NOTIFICATION_STATE -> {
+                    setHideNotificationState(context, instance)
+                }
+                InstancesColumns.HIGH_NOTIFICATION_STATE -> {
+                    setHighNotificationState(context, instance)
+                }
+                InstancesColumns.FIRED_STATE -> setFiredState(context, instance)
+                InstancesColumns.SNOOZE_STATE -> {
+                    setSnoozeState(context, instance, true /* showToast */)
+                }
+                InstancesColumns.MISSED_STATE -> setMissedState(context, instance)
+                InstancesColumns.PREDISMISSED_STATE -> setPreDismissState(context, instance)
+                InstancesColumns.DISMISSED_STATE -> deleteInstanceAndUpdateParent(context, instance)
+                else -> LogUtils.e("Trying to change to unknown alarm state: $state")
+            }
+        }
+
+        fun handleIntent(context: Context, intent: Intent) {
+            val action: String? = intent.getAction()
+            LogUtils.v("AlarmStateManager received intent $intent")
+            if (CHANGE_STATE_ACTION == action) {
+                val uri: Uri = intent.getData()!!
+                val instance: AlarmInstance? =
+                    AlarmInstance.getInstance(context.getContentResolver(),
+                        AlarmInstance.getId(uri))
+                if (instance == null) {
+                    LogUtils.e("Can not change state for unknown instance: $uri")
+                    return
+                }
+
+                val globalId = DataModel.dataModel.globalIntentId
+                val intentId: Int = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1)
+                val alarmState: Int = intent.getIntExtra(ALARM_STATE_EXTRA, -1)
+                if (intentId != globalId) {
+                    LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId +
+                            " AlarmState: " + alarmState)
+                    // Allows dismiss/snooze requests to go through
+                    if (!intent.hasCategory(ALARM_DISMISS_TAG) &&
+                            !intent.hasCategory(ALARM_SNOOZE_TAG)) {
+                        LogUtils.i("Ignoring old Intent")
+                        return
+                    }
+                }
+
+                if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
+                    if (intent.hasCategory(ALARM_DISMISS_TAG)) {
+                        Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification)
+                    } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) {
+                        Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification)
+                    }
+                }
+
+                if (alarmState >= 0) {
+                    setAlarmState(context, instance, alarmState)
+                } else {
+                    registerInstance(context, instance, true)
+                }
+            } else if (SHOW_AND_DISMISS_ALARM_ACTION == action) {
+                val uri: Uri = intent.getData()!!
+                val instance: AlarmInstance? =
+                        AlarmInstance.getInstance(context.getContentResolver(),
+                        AlarmInstance.getId(uri))
+
+                if (instance == null) {
+                    LogUtils.e("Null alarminstance for SHOW_AND_DISMISS")
+                    // dismiss the notification
+                    val id: Int = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1)
+                    if (id != -1) {
+                        NotificationManagerCompat.from(context).cancel(id)
+                    }
+                    return
+                }
+
+                val alarmId = instance.mAlarmId ?: Alarm.INVALID_ID
+                val viewAlarmIntent: Intent =
+                    Alarm.createIntent(context, DeskClock::class.java, alarmId)
+                        .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+                // Open DeskClock which is now positioned on the alarms tab.
+                context.startActivity(viewAlarmIntent)
+
+                deleteInstanceAndUpdateParent(context, instance)
+            }
+        }
+
+        /**
+         * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm
+         * indicators.
+         */
+        private fun createIndicatorIntent(context: Context?): Intent {
+            return Intent(context, AlarmStateManager::class.java).setAction(INDICATOR_ACTION)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
deleted file mode 100644
index 6c94649..0000000
--- a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
+++ /dev/null
@@ -1,205 +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.Fragment;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Vibrator;
-
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.LabelDialogFragment;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-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;
-
-/**
- * Click handler for an alarm time item.
- */
-public final class AlarmTimeClickHandler {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmTimeClickHandler");
-
-    private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap";
-
-    private final Fragment mFragment;
-    private final Context mContext;
-    private final AlarmUpdateHandler mAlarmUpdateHandler;
-    private final ScrollHandler mScrollHandler;
-
-    private Alarm mSelectedAlarm;
-    private Bundle mPreviousDaysOfWeekMap;
-
-    public AlarmTimeClickHandler(Fragment fragment, Bundle savedState,
-            AlarmUpdateHandler alarmUpdateHandler, ScrollHandler smoothScrollController) {
-        mFragment = fragment;
-        mContext = mFragment.getActivity().getApplicationContext();
-        mAlarmUpdateHandler = alarmUpdateHandler;
-        mScrollHandler = smoothScrollController;
-        if (savedState != null) {
-            mPreviousDaysOfWeekMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP);
-        }
-        if (mPreviousDaysOfWeekMap == null) {
-            mPreviousDaysOfWeekMap = new Bundle();
-        }
-    }
-
-    public void setSelectedAlarm(Alarm selectedAlarm) {
-        mSelectedAlarm = selectedAlarm;
-    }
-
-    public void saveInstance(Bundle outState) {
-        outState.putBundle(KEY_PREVIOUS_DAY_MAP, mPreviousDaysOfWeekMap);
-    }
-
-    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);
-        }
-    }
-
-    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 Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
-                if (v.hasVibrator()) {
-                    v.vibrate(300);
-                }
-            }
-        }
-    }
-
-    public void setAlarmRepeatEnabled(Alarm alarm, boolean isEnabled) {
-        final Calendar now = Calendar.getInstance();
-        final Calendar oldNextAlarmTime = alarm.getNextAlarmTime(now);
-        final String alarmId = String.valueOf(alarm.id);
-        if (isEnabled) {
-            // Set all previously set days
-            // or
-            // Set all days if no previous.
-            final int bitSet = mPreviousDaysOfWeekMap.getInt(alarmId);
-            alarm.daysOfWeek = Weekdays.fromBits(bitSet);
-            if (!alarm.daysOfWeek.isRepeating()) {
-                alarm.daysOfWeek = Weekdays.ALL;
-            }
-        } else {
-            // Remember the set days in case the user wants it back.
-            final int bitSet = alarm.daysOfWeek.getBits();
-            mPreviousDaysOfWeekMap.putInt(alarmId, bitSet);
-
-            // Remove all repeat days
-            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);
-
-        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(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;
-        Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock);
-        TimePickerDialogFragment.show(mFragment, alarm.hour, alarm.minutes);
-    }
-
-    public void dismissAlarmInstance(AlarmInstance alarmInstance) {
-        final Intent dismissIntent = AlarmStateManager.createStateChangeIntent(
-                mContext, AlarmStateManager.ALARM_DISMISS_TAG, alarmInstance,
-                AlarmInstance.PREDISMISSED_STATE);
-        mContext.startService(dismissIntent);
-        mAlarmUpdateHandler.showPredismissToast(alarmInstance);
-    }
-
-    public void onRingtoneClicked(Context context, Alarm alarm) {
-        mSelectedAlarm = alarm;
-        Events.sendAlarmEvent(R.string.action_set_ringtone, R.string.label_deskclock);
-
-        final Intent intent =
-                RingtonePickerActivity.createAlarmRingtonePickerIntent(context, alarm);
-        context.startActivity(intent);
-    }
-
-    public void onEditLabelClicked(Alarm alarm) {
-        Events.sendAlarmEvent(R.string.action_set_label, R.string.label_deskclock);
-        final LabelDialogFragment fragment =
-                LabelDialogFragment.newInstance(alarm, alarm.label, mFragment.getTag());
-        LabelDialogFragment.show(mFragment.getFragmentManager(), fragment);
-    }
-
-    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();
-            a.hour = hourOfDay;
-            a.minutes = minute;
-            a.enabled = true;
-            mAlarmUpdateHandler.asyncAddAlarm(a);
-        } else {
-            mSelectedAlarm.hour = hourOfDay;
-            mSelectedAlarm.minutes = minute;
-            mSelectedAlarm.enabled = true;
-            mScrollHandler.setSmoothScrollStableId(mSelectedAlarm.id);
-            mAlarmUpdateHandler.asyncUpdateAlarm(mSelectedAlarm, true, false);
-            mSelectedAlarm = null;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt
new file mode 100644
index 0000000..a2a58cf
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Vibrator
+import androidx.fragment.app.Fragment
+
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.LabelDialogFragment
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+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.provider.ClockContract.InstancesColumns
+import com.android.deskclock.ringtone.RingtonePickerActivity
+
+import java.util.Calendar
+
+/**
+ * Click handler for an alarm time item.
+ */
+class AlarmTimeClickHandler(
+    private val mFragment: Fragment,
+    savedState: Bundle?,
+    private val mAlarmUpdateHandler: AlarmUpdateHandler,
+    private val mScrollHandler: ScrollHandler
+) {
+
+    private val mContext: Context = mFragment.requireActivity().getApplicationContext()
+    private var mSelectedAlarm: Alarm? = null
+    private var mPreviousDaysOfWeekMap: Bundle? = null
+
+    init {
+        if (savedState != null) {
+            mPreviousDaysOfWeekMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP)
+        }
+        if (mPreviousDaysOfWeekMap == null) {
+            mPreviousDaysOfWeekMap = Bundle()
+        }
+    }
+
+    fun setSelectedAlarm(selectedAlarm: Alarm?) {
+        mSelectedAlarm = selectedAlarm
+    }
+
+    fun saveInstance(outState: Bundle) {
+        outState.putBundle(KEY_PREVIOUS_DAY_MAP, mPreviousDaysOfWeekMap)
+    }
+
+    fun setAlarmEnabled(alarm: Alarm, newState: Boolean) {
+        if (newState != alarm.enabled) {
+            alarm.enabled = newState
+            Events.sendAlarmEvent(if (newState) R.string.action_enable else R.string.action_disable,
+                    R.string.label_deskclock)
+            mAlarmUpdateHandler.asyncUpdateAlarm(alarm, alarm.enabled, minorUpdate = false)
+            LOGGER.d("Updating alarm enabled state to $newState")
+        }
+    }
+
+    fun setAlarmVibrationEnabled(alarm: Alarm, newState: Boolean) {
+        if (newState != alarm.vibrate) {
+            alarm.vibrate = newState
+            Events.sendAlarmEvent(R.string.action_toggle_vibrate, R.string.label_deskclock)
+            mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+            LOGGER.d("Updating vibrate state to $newState")
+
+            if (newState) {
+                // Buzz the vibrator to preview the alarm firing behavior.
+                val v: Vibrator = mContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+                if (v.hasVibrator()) {
+                    v.vibrate(300)
+                }
+            }
+        }
+    }
+
+    fun setAlarmRepeatEnabled(alarm: Alarm, isEnabled: Boolean) {
+        val now = Calendar.getInstance()
+        val oldNextAlarmTime = alarm.getNextAlarmTime(now)
+        val alarmId = alarm.id.toString()
+        if (isEnabled) {
+            // Set all previously set days
+            // or
+            // Set all days if no previous.
+            val bitSet: Int = mPreviousDaysOfWeekMap!!.getInt(alarmId)
+            alarm.daysOfWeek = Weekdays.fromBits(bitSet)
+            if (!alarm.daysOfWeek.isRepeating) {
+                alarm.daysOfWeek = Weekdays.ALL
+            }
+        } else {
+            // Remember the set days in case the user wants it back.
+            val bitSet = alarm.daysOfWeek.bits
+            mPreviousDaysOfWeekMap!!.putInt(alarmId, bitSet)
+
+            // Remove all repeat days
+            alarm.daysOfWeek = Weekdays.NONE
+        }
+
+        // if the change altered the next scheduled alarm time, tell the user
+        val newNextAlarmTime = alarm.getNextAlarmTime(now)
+        val popupToast = oldNextAlarmTime != newNextAlarmTime
+        Events.sendAlarmEvent(R.string.action_toggle_repeat_days, R.string.label_deskclock)
+        mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, minorUpdate = false)
+    }
+
+    fun setDayOfWeekEnabled(alarm: Alarm, checked: Boolean, index: Int) {
+        val now = Calendar.getInstance()
+        val oldNextAlarmTime = alarm.getNextAlarmTime(now)
+
+        val weekday = DataModel.dataModel.weekdayOrder.calendarDays[index]
+        alarm.daysOfWeek = alarm.daysOfWeek.setBit(weekday, checked)
+
+        // if the change altered the next scheduled alarm time, tell the user
+        val newNextAlarmTime = alarm.getNextAlarmTime(now)
+        val popupToast = oldNextAlarmTime != newNextAlarmTime
+        mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, minorUpdate = false)
+    }
+
+    fun onDeleteClicked(itemHolder: AlarmItemHolder) {
+        if (mFragment is AlarmClockFragment) {
+            (mFragment as AlarmClockFragment).removeItem(itemHolder)
+        }
+        val alarm = itemHolder.item
+        Events.sendAlarmEvent(R.string.action_delete, R.string.label_deskclock)
+        mAlarmUpdateHandler.asyncDeleteAlarm(alarm)
+        LOGGER.d("Deleting alarm.")
+    }
+
+    fun onClockClicked(alarm: Alarm) {
+        mSelectedAlarm = alarm
+        Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock)
+        TimePickerDialogFragment.show(mFragment, alarm.hour, alarm.minutes)
+    }
+
+    fun dismissAlarmInstance(alarmInstance: AlarmInstance) {
+        val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(
+                mContext, AlarmStateManager.ALARM_DISMISS_TAG, alarmInstance,
+                InstancesColumns.PREDISMISSED_STATE)
+        mContext.startService(dismissIntent)
+        mAlarmUpdateHandler.showPredismissToast(alarmInstance)
+    }
+
+    fun onRingtoneClicked(context: Context, alarm: Alarm) {
+        mSelectedAlarm = alarm
+        Events.sendAlarmEvent(R.string.action_set_ringtone, R.string.label_deskclock)
+
+        val intent: Intent = RingtonePickerActivity.createAlarmRingtonePickerIntent(context, alarm)
+        context.startActivity(intent)
+    }
+
+    fun onEditLabelClicked(alarm: Alarm) {
+        Events.sendAlarmEvent(R.string.action_set_label, R.string.label_deskclock)
+        val fragment = LabelDialogFragment.newInstance(alarm, alarm.label, mFragment.getTag())
+        LabelDialogFragment.show(mFragment.getFragmentManager(), fragment)
+    }
+
+    fun onTimeSet(hourOfDay: Int, minute: Int) {
+        if (mSelectedAlarm == null) {
+            // If mSelectedAlarm is null then we're creating a new alarm.
+            val a = Alarm()
+            a.hour = hourOfDay
+            a.minutes = minute
+            a.enabled = true
+            mAlarmUpdateHandler.asyncAddAlarm(a)
+        } else {
+            mSelectedAlarm!!.hour = hourOfDay
+            mSelectedAlarm!!.minutes = minute
+            mSelectedAlarm!!.enabled = true
+            mScrollHandler.setSmoothScrollStableId(mSelectedAlarm!!.id)
+            mAlarmUpdateHandler
+                    .asyncUpdateAlarm(mSelectedAlarm!!, popToast = true, minorUpdate = false)
+            mSelectedAlarm = null
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("AlarmTimeClickHandler")
+
+        private const val KEY_PREVIOUS_DAY_MAP = "previousDayMap"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmUpdateHandler.java b/src/com/android/deskclock/alarms/AlarmUpdateHandler.java
deleted file mode 100644
index 8f6fc5d..0000000
--- a/src/com/android/deskclock/alarms/AlarmUpdateHandler.java
+++ /dev/null
@@ -1,222 +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.content.ContentResolver;
-import android.content.Context;
-import android.os.AsyncTask;
-import com.google.android.material.snackbar.Snackbar;
-import android.text.format.DateFormat;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.widget.toast.SnackbarManager;
-
-import java.util.Calendar;
-import java.util.List;
-
-/**
- * API for asynchronously mutating a single alarm.
- */
-public final class AlarmUpdateHandler {
-
-    private final Context mAppContext;
-    private final ScrollHandler mScrollHandler;
-    private final View mSnackbarAnchor;
-
-    // For undo
-    private Alarm mDeletedAlarm;
-
-    public AlarmUpdateHandler(Context context, ScrollHandler scrollHandler,
-            ViewGroup snackbarAnchor) {
-        mAppContext = context.getApplicationContext();
-        mScrollHandler = scrollHandler;
-        mSnackbarAnchor = snackbarAnchor;
-    }
-
-    /**
-     * Adds a new alarm on the background.
-     *
-     * @param alarm The alarm to be added.
-     */
-    public void asyncAddAlarm(final Alarm alarm) {
-        final AsyncTask<Void, Void, AlarmInstance> updateTask =
-                new AsyncTask<Void, Void, AlarmInstance>() {
-                    @Override
-                    protected AlarmInstance doInBackground(Void... parameters) {
-                        if (alarm != null) {
-                            Events.sendAlarmEvent(R.string.action_create, R.string.label_deskclock);
-                            ContentResolver cr = mAppContext.getContentResolver();
-
-                            // Add alarm to db
-                            Alarm newAlarm = Alarm.addAlarm(cr, alarm);
-
-                            // Be ready to scroll to this alarm on UI later.
-                            mScrollHandler.setSmoothScrollStableId(newAlarm.id);
-
-                            // Create and add instance to db
-                            if (newAlarm.enabled) {
-                                return setupAlarmInstance(newAlarm);
-                            }
-                        }
-                        return null;
-                    }
-
-                    @Override
-                    protected void onPostExecute(AlarmInstance instance) {
-                        if (instance != null) {
-                            AlarmUtils.popAlarmSetSnackbar(
-                                    mSnackbarAnchor, instance.getAlarmTime().getTimeInMillis());
-                        }
-                    }
-                };
-        updateTask.execute();
-    }
-
-    /**
-     * Modifies an alarm on the background, and optionally show a toast when done.
-     *
-     * @param alarm       The alarm to be modified.
-     * @param popToast    whether or not a toast should be displayed when done.
-     * @param minorUpdate if true, don't affect any currently snoozed instances.
-     */
-    public void asyncUpdateAlarm(final Alarm alarm, final boolean popToast,
-            final boolean minorUpdate) {
-        final AsyncTask<Void, Void, AlarmInstance> updateTask =
-                new AsyncTask<Void, Void, AlarmInstance>() {
-                    @Override
-                    protected AlarmInstance doInBackground(Void... parameters) {
-                        ContentResolver cr = mAppContext.getContentResolver();
-
-                        // Update alarm
-                        Alarm.updateAlarm(cr, alarm);
-
-                        if (minorUpdate) {
-                            // just update the instance in the database and update notifications.
-                            final List<AlarmInstance> instanceList =
-                                    AlarmInstance.getInstancesByAlarmId(cr, alarm.id);
-                            for (AlarmInstance instance : instanceList) {
-                                // Make a copy of the existing instance
-                                final AlarmInstance newInstance = new AlarmInstance(instance);
-                                // Copy over minor change data to the instance; we don't know
-                                // exactly which minor field changed, so just copy them all.
-                                newInstance.mVibrate = alarm.vibrate;
-                                newInstance.mRingtone = alarm.alert;
-                                newInstance.mLabel = alarm.label;
-                                // Since we copied the mId of the old instance and the mId is used
-                                // as the primary key in the AlarmInstance table, this will replace
-                                // the existing instance.
-                                AlarmInstance.updateInstance(cr, newInstance);
-                                // Update the notification for this instance.
-                                AlarmNotifications.updateNotification(mAppContext, newInstance);
-                            }
-                            return null;
-                        }
-                        // Otherwise, this is a major update and we're going to re-create the alarm
-                        AlarmStateManager.deleteAllInstances(mAppContext, alarm.id);
-
-                        return alarm.enabled ? setupAlarmInstance(alarm) : null;
-                    }
-
-                    @Override
-                    protected void onPostExecute(AlarmInstance instance) {
-                        if (popToast && instance != null) {
-                            AlarmUtils.popAlarmSetSnackbar(
-                                    mSnackbarAnchor, instance.getAlarmTime().getTimeInMillis());
-                        }
-                    }
-                };
-        updateTask.execute();
-    }
-
-    /**
-     * Deletes an alarm on the background.
-     *
-     * @param alarm The alarm to be deleted.
-     */
-    public void asyncDeleteAlarm(final Alarm alarm) {
-        final AsyncTask<Void, Void, Boolean> deleteTask = new AsyncTask<Void, Void, Boolean>() {
-            @Override
-            protected Boolean doInBackground(Void... parameters) {
-                // Activity may be closed at this point , make sure data is still valid
-                if (alarm == null) {
-                    // Nothing to do here, just return.
-                    return false;
-                }
-                AlarmStateManager.deleteAllInstances(mAppContext, alarm.id);
-                return Alarm.deleteAlarm(mAppContext.getContentResolver(), alarm.id);
-            }
-
-            @Override
-            protected void onPostExecute(Boolean deleted) {
-                if (deleted) {
-                    mDeletedAlarm = alarm;
-                    showUndoBar();
-                }
-            }
-        };
-        deleteTask.execute();
-    }
-
-    /**
-     * Show a toast when an alarm is predismissed.
-     *
-     * @param instance Instance being predismissed.
-     */
-    public void showPredismissToast(AlarmInstance instance) {
-        final String time = DateFormat.getTimeFormat(mAppContext).format(
-                instance.getAlarmTime().getTime());
-        final String text = mAppContext.getString(R.string.alarm_is_dismissed, time);
-        SnackbarManager.show(Snackbar.make(mSnackbarAnchor, text, Snackbar.LENGTH_SHORT));
-    }
-
-    /**
-     * Hides any undo toast.
-     */
-    public void hideUndoBar() {
-        mDeletedAlarm = null;
-        SnackbarManager.dismiss();
-    }
-
-    private void showUndoBar() {
-        final Alarm deletedAlarm = mDeletedAlarm;
-        final Snackbar snackbar = Snackbar.make(mSnackbarAnchor,
-                mAppContext.getString(R.string.alarm_deleted), Snackbar.LENGTH_LONG)
-                .setAction(R.string.alarm_undo, new View.OnClickListener() {
-                    @Override
-                    public void onClick(View v) {
-                        mDeletedAlarm = null;
-                        asyncAddAlarm(deletedAlarm);
-                    }
-                });
-        SnackbarManager.show(snackbar);
-    }
-
-    private AlarmInstance setupAlarmInstance(Alarm alarm) {
-        final ContentResolver cr = mAppContext.getContentResolver();
-        AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance());
-        newInstance = AlarmInstance.addInstance(cr, newInstance);
-        // Register instance to state manager
-        AlarmStateManager.registerInstance(mAppContext, newInstance, true);
-        return newInstance;
-    }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt b/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt
new file mode 100644
index 0000000..9295cbf
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.ContentResolver
+import android.content.Context
+import android.os.AsyncTask
+import android.text.format.DateFormat
+import android.view.ViewGroup
+
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.R
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.widget.toast.SnackbarManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import java.util.Calendar
+
+/**
+ * API for asynchronously mutating a single alarm.
+ */
+// TODO(b/165664115) Replace deprecated AsyncTask calls
+class AlarmUpdateHandler(
+    context: Context,
+    private val mScrollHandler: ScrollHandler?,
+    private val mSnackbarAnchor: ViewGroup?
+) {
+
+    private val mAppContext: Context = context.getApplicationContext()
+
+    // For undo
+    private var mDeletedAlarm: Alarm? = null
+
+    /**
+     * Adds a new alarm on the background.
+     *
+     * @param alarm The alarm to be added.
+     */
+    fun asyncAddAlarm(alarm: Alarm?) {
+        val updateTask: AsyncTask<Void, Void, AlarmInstance> =
+                object : AsyncTask<Void, Void, AlarmInstance>() {
+            override fun doInBackground(vararg parameters: Void): AlarmInstance? {
+                if (alarm != null) {
+                    Events.sendAlarmEvent(R.string.action_create, R.string.label_deskclock)
+                    val cr: ContentResolver = mAppContext.getContentResolver()
+
+                    // Add alarm to db
+                    val newAlarm = Alarm.addAlarm(cr, alarm)
+
+                    // Be ready to scroll to this alarm on UI later.
+                    mScrollHandler?.setSmoothScrollStableId(newAlarm.id)
+
+                    // Create and add instance to db
+                    if (newAlarm.enabled) {
+                        return setupAlarmInstance(newAlarm)
+                    }
+                }
+                return null
+            }
+
+            override fun onPostExecute(instance: AlarmInstance?) {
+                if (instance != null) {
+                    AlarmUtils.popAlarmSetSnackbar(mSnackbarAnchor!!,
+                            instance.alarmTime.timeInMillis)
+                }
+            }
+        }
+        updateTask.execute()
+    }
+
+    /**
+     * Modifies an alarm on the background, and optionally show a toast when done.
+     *
+     * @param alarm The alarm to be modified.
+     * @param popToast whether or not a toast should be displayed when done.
+     * @param minorUpdate if true, don't affect any currently snoozed instances.
+     */
+    fun asyncUpdateAlarm(
+        alarm: Alarm,
+        popToast: Boolean,
+        minorUpdate: Boolean
+    ) {
+        val updateTask: AsyncTask<Void, Void, AlarmInstance> =
+                object : AsyncTask<Void, Void, AlarmInstance>() {
+            override fun doInBackground(vararg parameters: Void): AlarmInstance? {
+                val cr: ContentResolver = mAppContext.getContentResolver()
+
+                // Update alarm
+                Alarm.updateAlarm(cr, alarm)
+                if (minorUpdate) {
+                    // just update the instance in the database and update notifications.
+                    val instanceList = AlarmInstance.getInstancesByAlarmId(cr, alarm.id)
+                    for (instance in instanceList) {
+                        // Make a copy of the existing instance
+                        val newInstance = AlarmInstance(instance)
+                        // Copy over minor change data to the instance; we don't know
+                        // exactly which minor field changed, so just copy them all.
+                        newInstance.mVibrate = alarm.vibrate
+                        newInstance.mRingtone = alarm.alert
+                        newInstance.mLabel = alarm.label
+                        // Since we copied the mId of the old instance and the mId is used
+                        // as the primary key in the AlarmInstance table, this will replace
+                        // the existing instance.
+                        AlarmInstance.updateInstance(cr, newInstance)
+                        // Update the notification for this instance.
+                        AlarmNotifications.updateNotification(mAppContext, newInstance)
+                    }
+                    return null
+                }
+                // Otherwise, this is a major update and we're going to re-create the alarm
+                AlarmStateManager.deleteAllInstances(mAppContext, alarm.id)
+
+                return if (alarm.enabled) setupAlarmInstance(alarm) else null
+            }
+
+            override fun onPostExecute(instance: AlarmInstance?) {
+                if (popToast && instance != null) {
+                    AlarmUtils.popAlarmSetSnackbar(
+                            mSnackbarAnchor!!, instance.alarmTime.timeInMillis)
+                }
+            }
+        }
+        updateTask.execute()
+    }
+
+    /**
+     * Deletes an alarm on the background.
+     *
+     * @param alarm The alarm to be deleted.
+     */
+    fun asyncDeleteAlarm(alarm: Alarm?) {
+        val deleteTask: AsyncTask<Void, Void, Boolean> = object : AsyncTask<Void, Void, Boolean>() {
+            override fun doInBackground(vararg parameters: Void): Boolean {
+                // Activity may be closed at this point , make sure data is still valid
+                if (alarm == null) {
+                    // Nothing to do here, just return.
+                    return false
+                }
+                AlarmStateManager.deleteAllInstances(mAppContext, alarm.id)
+                return Alarm.deleteAlarm(mAppContext.getContentResolver(), alarm.id)
+            }
+
+            override fun onPostExecute(deleted: Boolean) {
+                if (deleted) {
+                    mDeletedAlarm = alarm
+                    showUndoBar()
+                }
+            }
+        }
+        deleteTask.execute()
+    }
+
+    /**
+     * Show a toast when an alarm is predismissed.
+     *
+     * @param instance Instance being predismissed.
+     */
+    fun showPredismissToast(instance: AlarmInstance) {
+        val time: String = DateFormat.getTimeFormat(mAppContext).format(instance.alarmTime.time)
+        val text: String = mAppContext.getString(R.string.alarm_is_dismissed, time)
+        SnackbarManager.show(Snackbar.make(mSnackbarAnchor!!, text, Snackbar.LENGTH_SHORT))
+    }
+
+    /**
+     * Hides any undo toast.
+     */
+    fun hideUndoBar() {
+        mDeletedAlarm = null
+        SnackbarManager.dismiss()
+    }
+
+    private fun showUndoBar() {
+        val deletedAlarm = mDeletedAlarm
+        val snackbar: Snackbar = Snackbar.make(mSnackbarAnchor!!,
+                mAppContext.getString(R.string.alarm_deleted), Snackbar.LENGTH_LONG)
+                .setAction(R.string.alarm_undo, { _ ->
+                    mDeletedAlarm = null
+                    asyncAddAlarm(deletedAlarm)
+                })
+        SnackbarManager.show(snackbar)
+    }
+
+    private fun setupAlarmInstance(alarm: Alarm): AlarmInstance {
+        val cr: ContentResolver = mAppContext.getContentResolver()
+        var newInstance = alarm.createInstanceAfter(Calendar.getInstance())
+        newInstance = AlarmInstance.addInstance(cr, newInstance)
+        // Register instance to state manager
+        AlarmStateManager.registerInstance(mAppContext, newInstance, true)
+        return newInstance
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/ScrollHandler.java b/src/com/android/deskclock/alarms/ScrollHandler.kt
similarity index 79%
rename from src/com/android/deskclock/alarms/ScrollHandler.java
rename to src/com/android/deskclock/alarms/ScrollHandler.kt
index 168bed9..ddcf5f4 100644
--- a/src/com/android/deskclock/alarms/ScrollHandler.java
+++ b/src/com/android/deskclock/alarms/ScrollHandler.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,20 +14,20 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.alarms;
+package com.android.deskclock.alarms
 
 /**
  * API that handles scrolling when an alarm item is expanded/collapsed.
  */
-public interface ScrollHandler {
+interface ScrollHandler {
 
     /**
      * Sets the stable id that view should be scrolled to. The view does not actually scroll yet.
      */
-    void setSmoothScrollStableId(long stableId);
+    fun setSmoothScrollStableId(stableId: Long)
 
     /**
      * Perform smooth scroll to position.
      */
-    void smoothScrollTo(int position);
+    fun smoothScrollTo(position: Int)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/TimePickerDialogFragment.java b/src/com/android/deskclock/alarms/TimePickerDialogFragment.java
deleted file mode 100644
index 33fc757..0000000
--- a/src/com/android/deskclock/alarms/TimePickerDialogFragment.java
+++ /dev/null
@@ -1,141 +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.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 androidx.appcompat.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/TimePickerDialogFragment.kt b/src/com/android/deskclock/alarms/TimePickerDialogFragment.kt
new file mode 100644
index 0000000..d419766
--- /dev/null
+++ b/src/com/android/deskclock/alarms/TimePickerDialogFragment.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.TimePickerDialog
+import android.content.Context
+import android.os.Bundle
+import android.text.format.DateFormat
+import android.widget.TimePicker
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+
+import com.android.deskclock.Utils
+
+import java.util.Calendar
+
+/**
+ * DialogFragment used to show TimePicker.
+ */
+class TimePickerDialogFragment : DialogFragment() {
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val listener = getParentFragment() as OnTimeSetListener
+
+        val now = Calendar.getInstance()
+        val args: Bundle = arguments ?: Bundle.EMPTY
+        val hour: Int = args.getInt(ARG_HOUR, now[Calendar.HOUR_OF_DAY])
+        val minute: Int = args.getInt(ARG_MINUTE, now[Calendar.MINUTE])
+        return if (Utils.isLOrLater) {
+            val context: Context = requireActivity()
+            TimePickerDialog(context, { _, hourOfDay, minuteOfHour ->
+                listener.onTimeSet(this@TimePickerDialogFragment, hourOfDay, minuteOfHour)
+            }, hour, minute, DateFormat.is24HourFormat(context))
+        } else {
+            val builder: AlertDialog.Builder = AlertDialog.Builder(requireActivity())
+            val context: Context = builder.getContext()
+
+            val timePicker = TimePicker(context)
+            timePicker.setCurrentHour(hour)
+            timePicker.setCurrentMinute(minute)
+            timePicker.setIs24HourView(DateFormat.is24HourFormat(context))
+
+            builder.setView(timePicker)
+                    .setPositiveButton(android.R.string.ok, { _, _ ->
+                        listener.onTimeSet(this@TimePickerDialogFragment,
+                                timePicker.getCurrentHour(), timePicker.getCurrentMinute())
+                    }).setNegativeButton(android.R.string.cancel, null /* listener */)
+                    .create()
+        }
+    }
+
+    /**
+     * The callback interface used to indicate the user is done filling in the time (e.g. they
+     * clicked on the 'OK' button).
+     */
+    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
+         */
+        fun onTimeSet(fragment: TimePickerDialogFragment?, hourOfDay: Int, minute: Int)
+    }
+
+    companion object {
+        /**
+         * Tag for timer picker fragment in FragmentManager.
+         */
+        private const val TAG = "TimePickerDialogFragment"
+
+        private const val ARG_HOUR = TAG + "_hour"
+        private const val ARG_MINUTE = TAG + "_minute"
+
+        @JvmStatic
+        fun show(fragment: Fragment) {
+            show(fragment, -1 /* hour */, -1 /* minute */)
+        }
+
+        fun show(parentFragment: Fragment, hourOfDay: Int, minute: Int) {
+            require(parentFragment is OnTimeSetListener) {
+                "Fragment must implement OnTimeSetListener"
+            }
+
+            val manager: FragmentManager = parentFragment.getChildFragmentManager()
+            if (manager == null || manager.isDestroyed()) {
+                return
+            }
+
+            // Make sure the dialog isn't already added.
+            removeTimeEditDialog(manager)
+
+            val fragment = TimePickerDialogFragment()
+
+            val args = Bundle()
+            if (hourOfDay in 0..23) {
+                args.putInt(ARG_HOUR, hourOfDay)
+            }
+            if (minute in 0..59) {
+                args.putInt(ARG_MINUTE, minute)
+            }
+
+            fragment.setArguments(args)
+            fragment.show(manager, TAG)
+        }
+
+        @JvmStatic
+        fun removeTimeEditDialog(manager: FragmentManager?) {
+            manager?.let { manager ->
+                val prev: Fragment? = manager.findFragmentByTag(TAG)
+                prev?.let {
+                    manager.beginTransaction().remove(it).commit()
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.java b/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.java
deleted file mode 100644
index 48748f4..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.java
+++ /dev/null
@@ -1,83 +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.dataadapter;
-
-import android.os.Bundle;
-
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.alarms.AlarmTimeClickHandler;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-public class AlarmItemHolder extends ItemAdapter.ItemHolder<Alarm> {
-
-    private static final java.lang.String EXPANDED_KEY = "expanded";
-    private final AlarmInstance mAlarmInstance;
-    private final AlarmTimeClickHandler mAlarmTimeClickHandler;
-    private boolean mExpanded;
-
-    public AlarmItemHolder(Alarm alarm, AlarmInstance alarmInstance,
-            AlarmTimeClickHandler alarmTimeClickHandler) {
-        super(alarm, alarm.id);
-        mAlarmInstance = alarmInstance;
-        mAlarmTimeClickHandler = alarmTimeClickHandler;
-    }
-
-    @Override
-    public int getItemViewType() {
-        return isExpanded() ?
-                ExpandedAlarmViewHolder.VIEW_TYPE : CollapsedAlarmViewHolder.VIEW_TYPE;
-    }
-
-    public AlarmTimeClickHandler getAlarmTimeClickHandler() {
-        return mAlarmTimeClickHandler;
-    }
-
-    public AlarmInstance getAlarmInstance() {
-        return mAlarmInstance;
-    }
-
-    public void expand() {
-        if (!isExpanded()) {
-            mExpanded = true;
-            notifyItemChanged();
-        }
-    }
-
-    public void collapse() {
-        if (isExpanded()) {
-            mExpanded = false;
-            notifyItemChanged();
-        }
-    }
-
-    public boolean isExpanded() {
-        return mExpanded;
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle bundle) {
-        super.onSaveInstanceState(bundle);
-        bundle.putBoolean(EXPANDED_KEY, mExpanded);
-    }
-
-    @Override
-    public void onRestoreInstanceState(Bundle bundle) {
-        super.onRestoreInstanceState(bundle);
-        mExpanded = bundle.getBoolean(EXPANDED_KEY);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.kt b/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.kt
new file mode 100644
index 0000000..e794d00
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.dataadapter
+
+import android.os.Bundle
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.alarms.AlarmTimeClickHandler
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+
+class AlarmItemHolder(
+    alarm: Alarm,
+    val alarmInstance: AlarmInstance?,
+    val alarmTimeClickHandler: AlarmTimeClickHandler
+) : ItemHolder<Alarm>(alarm, alarm.id) {
+    var isExpanded = false
+        private set
+
+    override fun getItemViewType(): Int {
+        return if (isExpanded) {
+            ExpandedAlarmViewHolder.VIEW_TYPE
+        } else {
+            CollapsedAlarmViewHolder.VIEW_TYPE
+        }
+    }
+
+    fun expand() {
+        if (!isExpanded) {
+            isExpanded = true
+            notifyItemChanged()
+        }
+    }
+
+    fun collapse() {
+        if (isExpanded) {
+            isExpanded = false
+            notifyItemChanged()
+        }
+    }
+
+    override fun onSaveInstanceState(bundle: Bundle) {
+        super.onSaveInstanceState(bundle)
+        bundle.putBoolean(EXPANDED_KEY, isExpanded)
+    }
+
+    override fun onRestoreInstanceState(bundle: Bundle) {
+        super.onRestoreInstanceState(bundle)
+        isExpanded = bundle.getBoolean(EXPANDED_KEY)
+    }
+
+    companion object {
+        private const val EXPANDED_KEY = "expanded"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java
deleted file mode 100644
index d9c4986..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java
+++ /dev/null
@@ -1,120 +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.dataadapter;
-
-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;
-import com.android.deskclock.widget.TextTime;
-
-/**
- * Abstract ViewHolder for alarm time items.
- */
-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 ImageView arrow;
-    public final TextView preemptiveDismissButton;
-
-    public AlarmItemViewHolder(View itemView) {
-        super(itemView);
-
-        clock = (TextTime) itemView.findViewById(R.id.digital_clock);
-        onOff = (CompoundButton) itemView.findViewById(R.id.onoff);
-        arrow = (ImageView) itemView.findViewById(R.id.arrow);
-        preemptiveDismissButton =
-                (TextView) itemView.findViewById(R.id.preemptive_dismiss_button);
-        preemptiveDismissButton.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                final AlarmInstance alarmInstance = getItemHolder().getAlarmInstance();
-                if (alarmInstance != null) {
-                    getItemHolder().getAlarmTimeClickHandler().dismissAlarmInstance(alarmInstance);
-                }
-            }
-        });
-        onOff.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            @Override
-            public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
-                getItemHolder().getAlarmTimeClickHandler().setAlarmEnabled(
-                        getItemHolder().item, checked);
-            }
-        });
-    }
-
-    @Override
-    protected void onBindItemView(final AlarmItemHolder itemHolder) {
-        final Alarm alarm = itemHolder.item;
-        bindOnOffSwitch(alarm);
-        bindClock(alarm);
-        final Context context = itemView.getContext();
-        itemView.setContentDescription(clock.getText() + " " + alarm.getLabelOrDefault(context));
-    }
-
-    protected void bindOnOffSwitch(Alarm alarm) {
-        if (onOff.isChecked() != alarm.enabled) {
-            onOff.setChecked(alarm.enabled);
-        }
-    }
-
-    protected void bindClock(Alarm alarm) {
-        clock.setTime(alarm.hour, alarm.minutes);
-        clock.setAlpha(alarm.enabled ? CLOCK_ENABLED_ALPHA : CLOCK_DISABLED_ALPHA);
-    }
-
-    protected boolean bindPreemptiveDismissButton(Context context, Alarm alarm,
-            AlarmInstance alarmInstance) {
-        final boolean canBind = alarm.canPreemptivelyDismiss() && alarmInstance != null;
-        if (canBind) {
-            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))
-                    : context.getString(R.string.alarm_alert_dismiss_text);
-            preemptiveDismissButton.setText(dismissText);
-            preemptiveDismissButton.setClickable(true);
-        } else {
-            preemptiveDismissButton.setVisibility(View.GONE);
-            preemptiveDismissButton.setClickable(false);
-        }
-        return canBind;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.kt b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.kt
new file mode 100644
index 0000000..e957761
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.dataadapter
+
+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.ItemViewHolder
+import com.android.deskclock.ItemAnimator.OnAnimateChangeListener
+import com.android.deskclock.R
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.widget.TextTime
+
+/**
+ * Abstract ViewHolder for alarm time items.
+ */
+abstract class AlarmItemViewHolder(itemView: View)
+    : ItemViewHolder<AlarmItemHolder>(itemView), OnAnimateChangeListener {
+    val clock: TextTime = itemView.findViewById(R.id.digital_clock)
+    val onOff: CompoundButton = itemView.findViewById(R.id.onoff) as CompoundButton
+    val arrow: ImageView = itemView.findViewById(R.id.arrow) as ImageView
+    val preemptiveDismissButton: TextView =
+            itemView.findViewById(R.id.preemptive_dismiss_button) as TextView
+
+    init {
+        preemptiveDismissButton.setOnClickListener { _ ->
+            val alarmInstance = itemHolder!!.alarmInstance
+            if (alarmInstance != null) {
+                itemHolder!!.alarmTimeClickHandler.dismissAlarmInstance(alarmInstance)
+            }
+        }
+        onOff.setOnCheckedChangeListener { _, checked ->
+            itemHolder!!.alarmTimeClickHandler.setAlarmEnabled(itemHolder!!.item, checked)
+        }
+    }
+
+    override fun onBindItemView(itemHolder: AlarmItemHolder) {
+        val alarm = itemHolder.item
+        bindOnOffSwitch(alarm)
+        bindClock(alarm)
+        val context: Context = itemView.getContext()
+        itemView.setContentDescription(clock.text.toString() + " " +
+                alarm.getLabelOrDefault(context))
+    }
+
+    protected fun bindOnOffSwitch(alarm: Alarm) {
+        if (onOff.isChecked() != alarm.enabled) {
+            onOff.isChecked = alarm.enabled
+        }
+    }
+
+    protected fun bindClock(alarm: Alarm) {
+        clock.setTime(alarm.hour, alarm.minutes)
+        clock.alpha = if (alarm.enabled) CLOCK_ENABLED_ALPHA else CLOCK_DISABLED_ALPHA
+    }
+
+    protected fun bindPreemptiveDismissButton(
+        context: Context,
+        alarm: Alarm,
+        alarmInstance: AlarmInstance?
+    ): Boolean {
+        val canBind = alarm.canPreemptivelyDismiss() && alarmInstance != null
+        if (canBind) {
+            preemptiveDismissButton.visibility = View.VISIBLE
+            val dismissText: String = if (alarm.instanceState == InstancesColumns.SNOOZE_STATE) {
+                context.getString(R.string.alarm_alert_snooze_until,
+                        AlarmUtils.getAlarmText(context, alarmInstance!!, false))
+            } else {
+                context.getString(R.string.alarm_alert_dismiss_text)
+            }
+            preemptiveDismissButton.text = dismissText
+            preemptiveDismissButton.isClickable = true
+        } else {
+            preemptiveDismissButton.visibility = View.GONE
+            preemptiveDismissButton.isClickable = false
+        }
+        return canBind
+    }
+
+    companion object {
+        private const val CLOCK_ENABLED_ALPHA = 1f
+        private const val CLOCK_DISABLED_ALPHA = 0.69f
+
+        const val ANIM_STANDARD_DELAY_MULTIPLIER = 1f / 6f
+        const val ANIM_LONG_DURATION_MULTIPLIER = 2f / 3f
+        const val ANIM_SHORT_DURATION_MULTIPLIER = 1f / 4f
+        const val ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER =
+                1f - ANIM_LONG_DURATION_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER
+        const val ANIM_LONG_DELAY_INCREMENT_MULTIPLIER =
+                1f - ANIM_STANDARD_DELAY_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER
+        const val ANIMATE_REPEAT_DAYS = "ANIMATE_REPEAT_DAYS"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java
deleted file mode 100644
index 70ed1bb..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java
+++ /dev/null
@@ -1,273 +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.dataadapter;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.graphics.Rect;
-import androidx.recyclerview.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.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.
- */
-public final class CollapsedAlarmViewHolder extends AlarmItemViewHolder {
-
-    public static final int VIEW_TYPE = R.layout.alarm_time_collapsed;
-
-    private final TextView alarmLabel;
-    public final TextView daysOfWeek;
-    private final TextView upcomingInstanceLabel;
-    private final View hairLine;
-
-    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);
-        hairLine = itemView.findViewById(R.id.hairline);
-
-        // Expand handler
-        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();
-            }
-        });
-        // Edit time handler
-        clock.setOnClickListener(new View.OnClickListener() {
-            @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
-    protected void onBindItemView(AlarmItemHolder itemHolder) {
-        super.onBindItemView(itemHolder);
-        final Alarm alarm = itemHolder.item;
-        final AlarmInstance alarmInstance = itemHolder.getAlarmInstance();
-        final Context context = itemView.getContext();
-        bindRepeatText(context, alarm);
-        bindReadOnlyLabel(context, alarm);
-        bindUpcomingInstance(context, alarm);
-        bindPreemptiveDismissButton(context, alarm, alarmInstance);
-    }
-
-    private void bindReadOnlyLabel(Context context, Alarm alarm) {
-        if (alarm.label != null && alarm.label.length() != 0) {
-            alarmLabel.setText(alarm.label);
-            alarmLabel.setVisibility(View.VISIBLE);
-            alarmLabel.setContentDescription(context.getString(R.string.label_description)
-                    + " " + alarm.label);
-        } else {
-            alarmLabel.setVisibility(View.GONE);
-        }
-    }
-
-    private void bindRepeatText(Context context, Alarm alarm) {
-        if (alarm.daysOfWeek.isRepeating()) {
-            final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
-            final String daysOfWeekText = alarm.daysOfWeek.toString(context, weekdayOrder);
-            daysOfWeek.setText(daysOfWeekText);
-
-            final String string = alarm.daysOfWeek.toAccessibilityString(context, weekdayOrder);
-            daysOfWeek.setContentDescription(string);
-
-            daysOfWeek.setVisibility(View.VISIBLE);
-        } else {
-            daysOfWeek.setVisibility(View.GONE);
-        }
-    }
-
-    private void bindUpcomingInstance(Context context, Alarm alarm) {
-        if (alarm.daysOfWeek.isRepeating()) {
-            upcomingInstanceLabel.setVisibility(View.GONE);
-        } else {
-            upcomingInstanceLabel.setVisibility(View.VISIBLE);
-            final String labelText = Alarm.isTomorrow(alarm, Calendar.getInstance()) ?
-                    context.getString(R.string.alarm_tomorrow) :
-                    context.getString(R.string.alarm_today);
-            upcomingInstanceLabel.setText(labelText);
-        }
-    }
-
-    @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) {
-            mLayoutInflater = layoutInflater;
-        }
-
-        @Override
-        public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
-            return new CollapsedAlarmViewHolder(mLayoutInflater.inflate(
-                    viewType, parent, false /* attachToRoot */));
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.kt b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.kt
new file mode 100644
index 0000000..9eac514
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.dataadapter
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+
+import java.util.Calendar
+
+/**
+ * A ViewHolder containing views for an alarm item in collapsed stated.
+ */
+class CollapsedAlarmViewHolder private constructor(itemView: View) : AlarmItemViewHolder(itemView) {
+    private val alarmLabel: TextView = itemView.findViewById(R.id.label) as TextView
+    val daysOfWeek: TextView = itemView.findViewById(R.id.days_of_week) as TextView
+    private val upcomingInstanceLabel: TextView =
+            itemView.findViewById(R.id.upcoming_instance_label) as TextView
+    private val hairLine: View = itemView.findViewById(R.id.hairline)
+
+    init {
+        // Expand handler
+        itemView.setOnClickListener { _ ->
+            Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock)
+            itemHolder?.expand()
+        }
+        alarmLabel.setOnClickListener { _ ->
+            Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock)
+            itemHolder?.expand()
+        }
+        arrow.setOnClickListener { _ ->
+            Events.sendAlarmEvent(R.string.action_expand, R.string.label_deskclock)
+            itemHolder?.expand()
+        }
+        // Edit time handler
+        clock.setOnClickListener { _ ->
+            itemHolder!!.alarmTimeClickHandler.onClockClicked(itemHolder!!.item)
+            Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock)
+            itemHolder?.expand()
+        }
+
+        itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO)
+    }
+
+    override fun onBindItemView(itemHolder: AlarmItemHolder) {
+        super.onBindItemView(itemHolder)
+        val alarm = itemHolder.item
+        val alarmInstance = itemHolder.alarmInstance
+        val context: Context = itemView.getContext()
+        bindRepeatText(context, alarm)
+        bindReadOnlyLabel(context, alarm)
+        bindUpcomingInstance(context, alarm)
+        bindPreemptiveDismissButton(context, alarm, alarmInstance)
+    }
+
+    private fun bindReadOnlyLabel(context: Context, alarm: Alarm) {
+        if (!alarm.label.isNullOrEmpty()) {
+            alarmLabel.text = alarm.label
+            alarmLabel.visibility = View.VISIBLE
+            alarmLabel.setContentDescription(context.getString(R.string.label_description)
+                    .toString() + " " + alarm.label)
+        } else {
+            alarmLabel.visibility = View.GONE
+        }
+    }
+
+    private fun bindRepeatText(context: Context, alarm: Alarm) {
+        if (alarm.daysOfWeek.isRepeating) {
+            val weekdayOrder = DataModel.dataModel.weekdayOrder
+            val daysOfWeekText = alarm.daysOfWeek.toString(context, weekdayOrder)
+            daysOfWeek.text = daysOfWeekText
+
+            val string = alarm.daysOfWeek.toAccessibilityString(context, weekdayOrder)
+            daysOfWeek.setContentDescription(string)
+
+            daysOfWeek.visibility = View.VISIBLE
+        } else {
+            daysOfWeek.visibility = View.GONE
+        }
+    }
+
+    private fun bindUpcomingInstance(context: Context, alarm: Alarm) {
+        if (alarm.daysOfWeek.isRepeating) {
+            upcomingInstanceLabel.visibility = View.GONE
+        } else {
+            upcomingInstanceLabel.visibility = View.VISIBLE
+            val labelText: String = if (Alarm.isTomorrow(alarm, Calendar.getInstance())) {
+                context.getString(R.string.alarm_tomorrow)
+            } else {
+                context.getString(R.string.alarm_today)
+            }
+            upcomingInstanceLabel.text = labelText
+        }
+    }
+
+    override fun onAnimateChange(
+        payloads: List<Any>?,
+        fromLeft: Int,
+        fromTop: Int,
+        fromRight: Int,
+        fromBottom: Int,
+        duration: Long
+    ): Animator? {
+        /* There are no possible partial animations for collapsed view holders. */
+        return null
+    }
+
+    override fun onAnimateChange(
+        oldHolder: ViewHolder,
+        newHolder: ViewHolder,
+        duration: Long
+    ): Animator? {
+        if (oldHolder !is AlarmItemViewHolder ||
+                newHolder !is AlarmItemViewHolder) {
+            return null
+        }
+
+        val isCollapsing = this == newHolder
+        setChangingViewsAlpha(if (isCollapsing) 0f else 1f)
+
+        val changeAnimatorSet: Animator = if (isCollapsing) {
+            createCollapsingAnimator(oldHolder, duration)
+        } else {
+            createExpandingAnimator(newHolder, duration)
+        }
+        changeAnimatorSet.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animator: Animator) {
+                clock.visibility = View.VISIBLE
+                onOff.visibility = View.VISIBLE
+                arrow.visibility = View.VISIBLE
+                arrow.setTranslationY(0f)
+                setChangingViewsAlpha(1f)
+                arrow.jumpDrawablesToCurrentState()
+            }
+        })
+        return changeAnimatorSet
+    }
+
+    private fun createExpandingAnimator(newHolder: AlarmItemViewHolder, duration: Long): Animator {
+        clock.visibility = View.INVISIBLE
+        onOff.visibility = View.INVISIBLE
+        arrow.visibility = View.INVISIBLE
+
+        val alphaAnimatorSet = 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((duration * ANIM_SHORT_DURATION_MULTIPLIER).toLong())
+
+        val oldView: View = itemView
+        val newView: View = newHolder.itemView
+        val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView)
+                .setDuration(duration)
+        boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(alphaAnimatorSet, boundsAnimator)
+        return animatorSet
+    }
+
+    private fun createCollapsingAnimator(oldHolder: AlarmItemViewHolder, duration: Long): Animator {
+        val alphaAnimatorSet = 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))
+        val standardDelay = (duration * ANIM_STANDARD_DELAY_MULTIPLIER).toLong()
+        alphaAnimatorSet.setDuration(standardDelay)
+        alphaAnimatorSet.setStartDelay(duration - standardDelay)
+
+        val oldView: View = oldHolder.itemView
+        val newView: View = itemView
+        val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView)
+                .setDuration(duration)
+        boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+        val oldArrow: View = oldHolder.arrow
+        val oldArrowRect = Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight())
+        val newArrowRect = Rect(0, 0, arrow.getWidth(), arrow.getHeight())
+        (newView as ViewGroup).offsetDescendantRectToMyCoords(arrow, newArrowRect)
+        (oldView as ViewGroup).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect)
+        val arrowTranslationY: Float = (oldArrowRect.bottom - newArrowRect.bottom).toFloat()
+        arrow.setTranslationY(arrowTranslationY)
+        arrow.visibility = View.VISIBLE
+        clock.visibility = View.VISIBLE
+        onOff.visibility = View.VISIBLE
+
+        val arrowAnimation: Animator = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
+                .setDuration(duration)
+        arrowAnimation.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(alphaAnimatorSet, boundsAnimator, arrowAnimation)
+        animatorSet.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationStart(animator: Animator) {
+                AnimatorUtils.startDrawableAnimation(arrow)
+            }
+        })
+        return animatorSet
+    }
+
+    private fun setChangingViewsAlpha(alpha: Float) {
+        alarmLabel.alpha = alpha
+        daysOfWeek.alpha = alpha
+        upcomingInstanceLabel.alpha = alpha
+        hairLine.alpha = alpha
+        preemptiveDismissButton.alpha = alpha
+    }
+
+    class Factory(private val layoutInflater: LayoutInflater) : ItemViewHolder.Factory {
+        override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+            return CollapsedAlarmViewHolder(layoutInflater.inflate(
+                    viewType, parent, false /* attachToRoot */))
+        }
+    }
+
+    companion object {
+        @JvmField
+        val VIEW_TYPE: Int = R.layout.alarm_time_collapsed
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java
deleted file mode 100644
index 59a26e0..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java
+++ /dev/null
@@ -1,531 +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.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.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Vibrator;
-import androidx.core.content.ContextCompat;
-import androidx.recyclerview.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.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.data.DataModel;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.uidata.UiDataModel;
-
-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 state.
- */
-public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
-    public static final int VIEW_TYPE = R.layout.alarm_time_expanded;
-
-    public final CheckBox repeat;
-    private final TextView editLabel;
-    public final LinearLayout repeatDays;
-    private final CompoundButton[] dayButtons = new CompoundButton[7];
-    public final CheckBox vibrate;
-    public final TextView ringtone;
-    public final TextView delete;
-    private final View hairLine;
-
-    private final boolean mHasVibrator;
-
-    private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator) {
-        super(itemView);
-
-        mHasVibrator = hasVibrator;
-
-        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.
-        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();
-            }
-        });
-        // Edit time handler
-        clock.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                getAlarmTimeClickHandler().onClockClicked(getItemHolder().item);
-            }
-        });
-        // Edit label handler
-        editLabel.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                getAlarmTimeClickHandler().onEditLabelClicked(getItemHolder().item);
-            }
-        });
-        // Vibrator checkbox handler
-        vibrate.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                getAlarmTimeClickHandler().setAlarmVibrationEnabled(getItemHolder().item,
-                        ((CheckBox) v).isChecked());
-            }
-        });
-        // Ringtone editor handler
-        ringtone.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                getAlarmTimeClickHandler().onRingtoneClicked(context, getItemHolder().item);
-            }
-        });
-        // Delete alarm handler
-        delete.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                getAlarmTimeClickHandler().onDeleteClicked(getItemHolder());
-                v.announceForAccessibility(context.getString(R.string.alarm_deleted));
-            }
-        });
-        // Repeat checkbox handler
-        repeat.setOnClickListener(new View.OnClickListener() {
-            @Override
-            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 < dayButtons.length; i++) {
-            final int buttonIndex = i;
-            dayButtons[i].setOnClickListener(new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    final boolean isChecked = ((CompoundButton) view).isChecked();
-                    getAlarmTimeClickHandler().setDayOfWeekEnabled(getItemHolder().item,
-                            isChecked, buttonIndex);
-                }
-            });
-        }
-
-        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(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().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 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, Context context) {
-        final List<Integer> weekdays = DataModel.getDataModel().getWeekdayOrder().getCalendarDays();
-        for (int i = 0; i < weekdays.size(); i++) {
-            final CompoundButton dayButton = dayButtons[i];
-            if (alarm.daysOfWeek.isBitOn(weekdays.get(i))) {
-                dayButton.setChecked(true);
-                dayButton.setTextColor(ThemeUtils.resolveColor(context,
-                        android.R.attr.windowBackground));
-            } else {
-                dayButton.setChecked(false);
-                dayButton.setTextColor(Color.WHITE);
-            }
-        }
-        if (alarm.daysOfWeek.isRepeating()) {
-            repeat.setChecked(true);
-            repeatDays.setVisibility(View.VISIBLE);
-        } else {
-            repeat.setChecked(false);
-            repeatDays.setVisibility(View.GONE);
-        }
-    }
-
-    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) {
-        if (!mHasVibrator) {
-            vibrate.setVisibility(View.INVISIBLE);
-        } else {
-            vibrate.setVisibility(View.VISIBLE);
-            vibrate.setChecked(alarm.vibrate);
-        }
-    }
-
-    private AlarmTimeClickHandler getAlarmTimeClickHandler() {
-        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 mLayoutInflater;
-        private final boolean mHasVibrator;
-
-        public Factory(Context context) {
-            mLayoutInflater = LayoutInflater.from(context);
-            mHasVibrator = ((Vibrator) context.getSystemService(VIBRATOR_SERVICE)).hasVibrator();
-        }
-
-        @Override
-        public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
-            final View itemView = mLayoutInflater.inflate(viewType, parent, false);
-            return new ExpandedAlarmViewHolder(itemView, mHasVibrator);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.kt b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.kt
new file mode 100644
index 0000000..f6baef9
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.kt
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.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.Context.VIBRATOR_SERVICE
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Vibrator
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.TRANSLATION_Y
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+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.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * A ViewHolder containing views for an alarm item in expanded state.
+ */
+class ExpandedAlarmViewHolder private constructor(itemView: View, private val mHasVibrator: Boolean)
+    : AlarmItemViewHolder(itemView) {
+    val repeat: CheckBox = itemView.findViewById(R.id.repeat_onoff) as CheckBox
+    private val editLabel: TextView = itemView.findViewById(R.id.edit_label) as TextView
+    val repeatDays: LinearLayout = itemView.findViewById(R.id.repeat_days) as LinearLayout
+    private val dayButtons: Array<CompoundButton?> = arrayOfNulls<CompoundButton>(7)
+    val vibrate: CheckBox = itemView.findViewById(R.id.vibrate_onoff) as CheckBox
+    val ringtone: TextView = itemView.findViewById(R.id.choose_ringtone) as TextView
+    val delete: TextView = itemView.findViewById(R.id.delete) as TextView
+    private val hairLine: View = itemView.findViewById(R.id.hairline)
+
+    init {
+        val context: Context = itemView.getContext()
+        itemView.setBackground(LayerDrawable(arrayOf(
+                ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded),
+                ThemeUtils.resolveDrawable(context, R.attr.selectableItemBackground)
+        )))
+
+        // Build button for each day.
+        val inflater: LayoutInflater = LayoutInflater.from(context)
+        val weekdays = DataModel.dataModel.weekdayOrder.calendarDays
+        for (i in 0..6) {
+            val dayButtonFrame: View = inflater.inflate(R.layout.day_button, repeatDays,
+                    false /* attachToRoot */)
+            val dayButton: CompoundButton =
+                    dayButtonFrame.findViewById(R.id.day_button_box) as CompoundButton
+            val weekday = weekdays[i]
+            dayButton.text = UiDataModel.uiDataModel.getShortWeekday(weekday)
+            dayButton.setContentDescription(UiDataModel.uiDataModel.getLongWeekday(weekday))
+            repeatDays.addView(dayButtonFrame)
+            dayButtons[i] = dayButton
+        }
+
+        // Cannot set in xml since we need compat functionality for API < 21
+        val labelIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_label)
+        editLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(labelIcon, null, null, null)
+        val deleteIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_delete_small)
+        delete.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null)
+
+        // Collapse handler
+        itemView.setOnClickListener { _ ->
+            Events.sendAlarmEvent(R.string.action_collapse_implied, R.string.label_deskclock)
+            itemHolder?.collapse()
+        }
+        arrow.setOnClickListener { _ ->
+            Events.sendAlarmEvent(R.string.action_collapse, R.string.label_deskclock)
+            itemHolder?.collapse()
+        }
+        // Edit time handler
+        clock.setOnClickListener { _ ->
+            alarmTimeClickHandler.onClockClicked(itemHolder!!.item)
+        }
+        // Edit label handler
+        editLabel.setOnClickListener { _ ->
+            alarmTimeClickHandler.onEditLabelClicked(itemHolder!!.item)
+        }
+        // Vibrator checkbox handler
+        vibrate.setOnClickListener { view ->
+            alarmTimeClickHandler.setAlarmVibrationEnabled(itemHolder!!.item,
+                    (view as CheckBox).isChecked)
+        }
+        // Ringtone editor handler
+        ringtone.setOnClickListener { _ ->
+            alarmTimeClickHandler.onRingtoneClicked(context, itemHolder!!.item)
+        }
+        // Delete alarm handler
+        delete.setOnClickListener { view ->
+            alarmTimeClickHandler.onDeleteClicked(itemHolder!!)
+            view.announceForAccessibility(context.getString(R.string.alarm_deleted))
+        }
+        // Repeat checkbox handler
+        repeat.setOnClickListener { view ->
+            val checked: Boolean = (view as CheckBox).isChecked
+            alarmTimeClickHandler.setAlarmRepeatEnabled(itemHolder!!.item, checked)
+            itemHolder?.notifyItemChanged(ANIMATE_REPEAT_DAYS)
+        }
+        // Day buttons handler
+        for (i in dayButtons.indices) {
+            dayButtons[i]?.setOnClickListener { view ->
+                val isChecked: Boolean = (view as CompoundButton).isChecked
+                alarmTimeClickHandler.setDayOfWeekEnabled(itemHolder!!.item, isChecked, i)
+            }
+        }
+        itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO)
+    }
+
+    override fun onBindItemView(itemHolder: AlarmItemHolder) {
+        super.onBindItemView(itemHolder)
+
+        val alarm = itemHolder.item
+        val alarmInstance = itemHolder.alarmInstance
+        val context: Context = itemView.getContext()
+        bindEditLabel(context, alarm)
+        bindDaysOfWeekButtons(alarm, context)
+        bindVibrator(alarm)
+        bindRingtone(context, alarm)
+        bindPreemptiveDismissButton(context, alarm, alarmInstance)
+    }
+
+    private fun bindRingtone(context: Context, alarm: Alarm) {
+        val title = DataModel.dataModel.getRingtoneTitle(alarm.alert!!)
+        ringtone.text = title
+
+        val description: String = context.getString(R.string.ringtone_description)
+        ringtone.setContentDescription("$description $title")
+
+        val silent: Boolean = Utils.RINGTONE_SILENT == alarm.alert
+        val icon: Drawable? = Utils.getVectorDrawable(context,
+                if (silent) R.drawable.ic_ringtone_silent else R.drawable.ic_ringtone)
+        ringtone.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
+    }
+
+    private fun bindDaysOfWeekButtons(alarm: Alarm, context: Context) {
+        val weekdays = DataModel.dataModel.weekdayOrder.calendarDays
+        for (i in weekdays.indices) {
+            val dayButton: CompoundButton? = dayButtons[i]
+            dayButton?.let {
+                if (alarm.daysOfWeek.isBitOn(weekdays[i])) {
+                    dayButton.isChecked = true
+                    dayButton.setTextColor(ThemeUtils.resolveColor(context,
+                            android.R.attr.windowBackground))
+                } else {
+                    dayButton.isChecked = false
+                    dayButton.setTextColor(Color.WHITE)
+                }
+            }
+        }
+        if (alarm.daysOfWeek.isRepeating) {
+            repeat.isChecked = true
+            repeatDays.visibility = View.VISIBLE
+        } else {
+            repeat.isChecked = false
+            repeatDays.visibility = View.GONE
+        }
+    }
+
+    private fun bindEditLabel(context: Context, alarm: Alarm) {
+        editLabel.text = alarm.label
+        editLabel.contentDescription = if (!alarm.label.isNullOrEmpty()) {
+            context.getString(R.string.label_description).toString() + " " + alarm.label
+        } else {
+            context.getString(R.string.no_label_specified)
+        }
+    }
+
+    private fun bindVibrator(alarm: Alarm) {
+        if (!mHasVibrator) {
+            vibrate.visibility = View.INVISIBLE
+        } else {
+            vibrate.visibility = View.VISIBLE
+            vibrate.isChecked = alarm.vibrate
+        }
+    }
+
+    private val alarmTimeClickHandler: AlarmTimeClickHandler
+        get() = itemHolder!!.alarmTimeClickHandler
+
+    override fun onAnimateChange(
+        payloads: List<Any>?,
+        fromLeft: Int,
+        fromTop: Int,
+        fromRight: Int,
+        fromBottom: Int,
+        duration: Long
+    ): Animator? {
+        if (payloads == null || payloads.isEmpty() || !payloads.contains(ANIMATE_REPEAT_DAYS)) {
+            return null
+        }
+
+        val isExpansion = repeatDays.getVisibility() == View.VISIBLE
+        val height: Int = repeatDays.getHeight()
+        setTranslationY(if (isExpansion) {
+            -height.toFloat()
+        } else {
+            0f
+        }, if (isExpansion) {
+            -height.toFloat()
+        } else {
+            height.toFloat()
+        })
+        repeatDays.visibility = View.VISIBLE
+        repeatDays.alpha = if (isExpansion) 0f else 1f
+
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(AnimatorUtils.getBoundsAnimator(itemView,
+                fromLeft, fromTop, fromRight, fromBottom,
+                itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()),
+                ObjectAnimator.ofFloat(repeatDays, View.ALPHA, if (isExpansion) 1f else 0f),
+                ObjectAnimator.ofFloat(repeatDays, TRANSLATION_Y, if (isExpansion) {
+                    0f
+                } else {
+                    -height.toFloat()
+                }),
+                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(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animator: Animator) {
+                setTranslationY(0f, 0f)
+                repeatDays.alpha = 1f
+                repeatDays.visibility = if (isExpansion) View.VISIBLE else View.GONE
+                itemView.requestLayout()
+            }
+        })
+        animatorSet.duration = duration
+        animatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+        return animatorSet
+    }
+
+    private fun setTranslationY(repeatDaysTranslationY: Float, translationY: Float) {
+        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 fun onAnimateChange(
+        oldHolder: ViewHolder,
+        newHolder: ViewHolder,
+        duration: Long
+    ): Animator? {
+        if (oldHolder !is AlarmItemViewHolder ||
+                newHolder !is AlarmItemViewHolder) {
+            return null
+        }
+
+        val isExpanding = this == newHolder
+        AnimatorUtils.setBackgroundAlpha(itemView, if (isExpanding) 0 else 255)
+        setChangingViewsAlpha(if (isExpanding) 0f else 1f)
+
+        val changeAnimatorSet: Animator = if (isExpanding) {
+            createExpandingAnimator(oldHolder, duration)
+        } else {
+            createCollapsingAnimator(newHolder, duration)
+        }
+        changeAnimatorSet.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animator: Animator) {
+                AnimatorUtils.setBackgroundAlpha(itemView, 255)
+                clock.visibility = View.VISIBLE
+                onOff.visibility = View.VISIBLE
+                arrow.visibility = View.VISIBLE
+                arrow.setTranslationY(0f)
+                setChangingViewsAlpha(1f)
+                arrow.jumpDrawablesToCurrentState()
+            }
+        })
+        return changeAnimatorSet
+    }
+
+    private fun createCollapsingAnimator(newHolder: AlarmItemViewHolder, duration: Long): Animator {
+        arrow.visibility = View.INVISIBLE
+        clock.visibility = View.INVISIBLE
+        onOff.visibility = View.INVISIBLE
+
+        val daysVisible = repeatDays.getVisibility() == View.VISIBLE
+        val numberOfItems = countNumberOfItems()
+
+        val oldView: View = itemView
+        val newView: View = newHolder.itemView
+
+        val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(oldView,
+                PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 255, 0))
+        backgroundAnimator.duration = duration
+
+        val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView)
+        boundsAnimator.duration = duration
+        boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+        val shortDuration = (duration * ANIM_SHORT_DURATION_MULTIPLIER).toLong()
+        val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 0f)
+                .setDuration(shortDuration)
+        val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 0f)
+                .setDuration(shortDuration)
+        val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 0f)
+                .setDuration(shortDuration)
+        val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 0f)
+                .setDuration(shortDuration)
+        val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 0f)
+                .setDuration(shortDuration)
+        val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton,
+                View.ALPHA, 0f).setDuration(shortDuration)
+        val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 0f)
+                .setDuration(shortDuration)
+        val hairLineAnimation: Animator = 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.
+        var startDelay = 0L
+        val delayIncrement = (duration * ANIM_LONG_DELAY_INCREMENT_MULTIPLIER).toLong() /
+                (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)
+
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(backgroundAnimator, boundsAnimator, repeatAnimation,
+                repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
+                deleteAnimation, hairLineAnimation, dismissAnimation)
+        return animatorSet
+    }
+
+    private fun createExpandingAnimator(oldHolder: AlarmItemViewHolder, duration: Long): Animator {
+        val oldView: View = oldHolder.itemView
+        val newView: View = itemView
+        val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView)
+        boundsAnimator.duration = duration
+        boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+        val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(newView,
+                PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255))
+        backgroundAnimator.duration = duration
+
+        val oldArrow: View = oldHolder.arrow
+        val oldArrowRect = Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight())
+        val newArrowRect = Rect(0, 0, arrow.getWidth(), arrow.getHeight())
+        (newView as ViewGroup).offsetDescendantRectToMyCoords(arrow, newArrowRect)
+        (oldView as ViewGroup).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect)
+        val arrowTranslationY: Float = (oldArrowRect.bottom - newArrowRect.bottom).toFloat()
+
+        arrow.setTranslationY(arrowTranslationY)
+        arrow.visibility = View.VISIBLE
+        clock.visibility = View.VISIBLE
+        onOff.visibility = View.VISIBLE
+
+        val longDuration = (duration * ANIM_LONG_DURATION_MULTIPLIER).toLong()
+        val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 1f)
+                .setDuration(longDuration)
+        val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 1f)
+                .setDuration(longDuration)
+        val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 1f)
+                .setDuration(longDuration)
+        val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton,
+                View.ALPHA, 1f).setDuration(longDuration)
+        val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 1f)
+                .setDuration(longDuration)
+        val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 1f)
+                .setDuration(longDuration)
+        val hairLineAnimation: Animator = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f)
+                .setDuration(longDuration)
+        val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 1f)
+                .setDuration(longDuration)
+        val arrowAnimation: Animator = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
+                .setDuration(duration)
+        arrowAnimation.interpolator = 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.
+        var startDelay = (duration * ANIM_STANDARD_DELAY_MULTIPLIER).toLong()
+        val numberOfItems = countNumberOfItems()
+        val delayIncrement = (duration * ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER).toLong() /
+                (numberOfItems - 1)
+        repeatAnimation.setStartDelay(startDelay)
+        startDelay += delayIncrement
+        val 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)
+
+        val animatorSet = AnimatorSet()
+        animatorSet.playTogether(backgroundAnimator, repeatAnimation, boundsAnimator,
+                repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
+                deleteAnimation, hairLineAnimation, dismissAnimation, arrowAnimation)
+        animatorSet.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationStart(animator: Animator) {
+                AnimatorUtils.startDrawableAnimation(arrow)
+            }
+        })
+        return animatorSet
+    }
+
+    private fun countNumberOfItems(): Int {
+        // Always between 4 and 6 items.
+        var numberOfItems = 4
+        if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
+            numberOfItems++
+        }
+        if (repeatDays.getVisibility() == View.VISIBLE) {
+            numberOfItems++
+        }
+        return numberOfItems
+    }
+
+    private fun setChangingViewsAlpha(alpha: Float) {
+        repeat.alpha = alpha
+        editLabel.alpha = alpha
+        repeatDays.alpha = alpha
+        vibrate.alpha = alpha
+        ringtone.alpha = alpha
+        hairLine.alpha = alpha
+        delete.alpha = alpha
+        preemptiveDismissButton.alpha = alpha
+    }
+
+    class Factory(context: Context) : ItemViewHolder.Factory {
+        private val mLayoutInflater: LayoutInflater = LayoutInflater.from(context)
+        private val mHasVibrator: Boolean =
+                (context.getSystemService(VIBRATOR_SERVICE) as Vibrator).hasVibrator()
+
+        override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+            val itemView: View = mLayoutInflater.inflate(viewType, parent, false)
+            return ExpandedAlarmViewHolder(itemView, mHasVibrator)
+        }
+    }
+
+    companion object {
+        @JvmField
+        val VIEW_TYPE: Int = R.layout.alarm_time_expanded
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/Controller.java b/src/com/android/deskclock/controller/Controller.java
deleted file mode 100644
index 76f839a..0000000
--- a/src/com/android/deskclock/controller/Controller.java
+++ /dev/null
@@ -1,118 +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.controller;
-
-import android.app.Activity;
-import android.content.Context;
-import androidx.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() {}
-
-    public static Controller getController() {
-        return sController;
-    }
-
-    public void setContext(Context context) {
-        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() {
-        enforceMainLooper();
-        if (mShortcutController != null) {
-            mShortcutController.updateShortcuts();
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/Controller.kt b/src/com/android/deskclock/controller/Controller.kt
new file mode 100644
index 0000000..0b7c82c
--- /dev/null
+++ b/src/com/android/deskclock/controller/Controller.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.Activity
+import android.content.Context
+import androidx.annotation.StringRes
+
+import com.android.deskclock.Utils
+import com.android.deskclock.events.EventTracker
+
+/**
+ * Interactions with Android framework components responsible for part of the user experience are
+ * handled via this singleton.
+ */
+class Controller private constructor() {
+    private var mContext: Context? = null
+
+    /** The controller that dispatches app events to event trackers.  */
+    private lateinit var mEventController: EventController
+
+    /** The controller that interacts with voice interaction sessions on M+.  */
+    private lateinit var mVoiceController: VoiceController
+
+    /** The controller that creates and updates launcher shortcuts on N MR1+  */
+    private var mShortcutController: ShortcutController? = null
+
+    fun setContext(context: Context) {
+        if (mContext != context) {
+            mContext = context.getApplicationContext()
+            mEventController = EventController()
+            mVoiceController = VoiceController()
+            if (Utils.isNMR1OrLater) {
+                mShortcutController = ShortcutController(mContext!!)
+            }
+        }
+    }
+
+    //
+    // Event Tracking
+    //
+
+    /**
+     * @param eventTracker to be registered for tracking application events
+     */
+    fun addEventTracker(eventTracker: EventTracker) {
+        Utils.enforceMainLooper()
+        mEventController.addEventTracker(eventTracker)
+    }
+
+    /**
+     * @param eventTracker to be unregistered from tracking application events
+     */
+    fun removeEventTracker(eventTracker: EventTracker) {
+        Utils.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
+     */
+    fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int) {
+        mEventController.sendEvent(category, action, label)
+    }
+
+    //
+    // Voice Interaction
+    //
+
+    fun notifyVoiceSuccess(activity: Activity, message: String) {
+        mVoiceController.notifyVoiceSuccess(activity, message)
+    }
+
+    fun notifyVoiceFailure(activity: Activity, message: String) {
+        mVoiceController.notifyVoiceFailure(activity, message)
+    }
+
+    //
+    // Shortcuts
+    //
+
+    fun updateShortcuts() {
+        Utils.enforceMainLooper()
+        mShortcutController?.updateShortcuts()
+    }
+
+    companion object {
+        private val sController = Controller()
+
+        @JvmStatic
+        fun getController() = sController
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/EventController.java b/src/com/android/deskclock/controller/EventController.java
deleted file mode 100644
index 457756a..0000000
--- a/src/com/android/deskclock/controller/EventController.java
+++ /dev/null
@@ -1,43 +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.controller;
-
-import androidx.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/EventController.kt b/src/com/android/deskclock/controller/EventController.kt
new file mode 100644
index 0000000..d2f648f
--- /dev/null
+++ b/src/com/android/deskclock/controller/EventController.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.annotation.StringRes
+
+import com.android.deskclock.events.EventTracker
+
+import java.util.ArrayList
+
+internal class EventController {
+    private val mEventTrackers: MutableCollection<EventTracker> = ArrayList()
+
+    fun addEventTracker(eventTracker: EventTracker) {
+        mEventTrackers.add(eventTracker)
+    }
+
+    fun removeEventTracker(eventTracker: EventTracker) {
+        mEventTrackers.remove(eventTracker)
+    }
+
+    fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int) {
+        mEventTrackers.forEach { eventTracker ->
+            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
deleted file mode 100644
index 8eba32f..0000000
--- a/src/com/android/deskclock/controller/ShortcutController.java
+++ /dev/null
@@ -1,182 +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.controller;
-
-import android.annotation.TargetApi;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.UserManager;
-import android.provider.AlarmClock;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.HandleApiCalls;
-import com.android.deskclock.HandleShortcuts;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.ScreensaverActivity;
-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.events.ShortcutEventTracker;
-import com.android.deskclock.stopwatch.StopwatchService;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-@TargetApi(Build.VERSION_CODES.N_MR1)
-class ShortcutController {
-
-    private final Context mContext;
-    private final ComponentName mComponentName;
-    private final ShortcutManager mShortcutManager;
-    private final UserManager mUserManager;
-
-    ShortcutController(Context context) {
-        mContext = context;
-        mComponentName = new ComponentName(mContext, DeskClock.class);
-        mShortcutManager = mContext.getSystemService(ShortcutManager.class);
-        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
-        Controller.getController().addEventTracker(new ShortcutEventTracker(mContext));
-        DataModel.getDataModel().addStopwatchListener(new StopwatchWatcher());
-    }
-
-    void updateShortcuts() {
-        if (!mUserManager.isUserUnlocked()) {
-            LogUtils.i("Skipping shortcut update because user is locked.");
-            return;
-        }
-        try {
-            final ShortcutInfo alarm = createNewAlarmShortcut();
-            final ShortcutInfo timer = createNewTimerShortcut();
-            final ShortcutInfo stopwatch = createStopwatchShortcut();
-            final ShortcutInfo screensaver = createScreensaverShortcut();
-            mShortcutManager.setDynamicShortcuts(
-                    Arrays.asList(alarm, timer, stopwatch, screensaver));
-        } catch (IllegalStateException e) {
-            LogUtils.wtf(e);
-        }
-    }
-
-    private ShortcutInfo createNewAlarmShortcut() {
-        final Intent intent = new Intent(AlarmClock.ACTION_SET_ALARM)
-                .setClass(mContext, HandleApiCalls.class)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
-        final String setAlarmShortcut = UiDataModel.getUiDataModel()
-                .getShortcutId(R.string.category_alarm, R.string.action_create);
-        return new ShortcutInfo.Builder(mContext, setAlarmShortcut)
-                .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_new_alarm))
-                .setActivity(mComponentName)
-                .setShortLabel(mContext.getString(R.string.shortcut_new_alarm_short))
-                .setLongLabel(mContext.getString(R.string.shortcut_new_alarm_long))
-                .setIntent(intent)
-                .setRank(0)
-                .build();
-    }
-
-    private ShortcutInfo createNewTimerShortcut() {
-        final Intent intent = new Intent(AlarmClock.ACTION_SET_TIMER)
-                .setClass(mContext, HandleApiCalls.class)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
-        final String setTimerShortcut = UiDataModel.getUiDataModel()
-                .getShortcutId(R.string.category_timer, R.string.action_create);
-        return new ShortcutInfo.Builder(mContext, setTimerShortcut)
-                .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_new_timer))
-                .setActivity(mComponentName)
-                .setShortLabel(mContext.getString(R.string.shortcut_new_timer_short))
-                .setLongLabel(mContext.getString(R.string.shortcut_new_timer_long))
-                .setIntent(intent)
-                .setRank(1)
-                .build();
-    }
-
-    private ShortcutInfo createStopwatchShortcut() {
-        final @StringRes int action = DataModel.getDataModel().getStopwatch().isRunning()
-                ? R.string.action_pause : R.string.action_start;
-        final String shortcutId = UiDataModel.getUiDataModel()
-                .getShortcutId(R.string.category_stopwatch, action);
-        final ShortcutInfo.Builder shortcut = new ShortcutInfo.Builder(mContext, shortcutId)
-                .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_stopwatch))
-                .setActivity(mComponentName)
-                .setRank(2);
-        final Intent intent;
-        if (DataModel.getDataModel().getStopwatch().isRunning()) {
-            intent = new Intent(StopwatchService.ACTION_PAUSE_STOPWATCH)
-                    .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
-            shortcut.setShortLabel(mContext.getString(R.string.shortcut_pause_stopwatch_short))
-                    .setLongLabel(mContext.getString(R.string.shortcut_pause_stopwatch_long));
-        } else {
-            intent = new Intent(StopwatchService.ACTION_START_STOPWATCH)
-                    .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
-            shortcut.setShortLabel(mContext.getString(R.string.shortcut_start_stopwatch_short))
-                    .setLongLabel(mContext.getString(R.string.shortcut_start_stopwatch_long));
-        }
-        intent.setClass(mContext, HandleShortcuts.class)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        return shortcut
-                .setIntent(intent)
-                .build();
-    }
-
-    private ShortcutInfo createScreensaverShortcut() {
-        final Intent intent = new Intent(Intent.ACTION_MAIN)
-                .setClass(mContext, ScreensaverActivity.class)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
-        final String screensaverShortcut = UiDataModel.getUiDataModel()
-                .getShortcutId(R.string.category_screensaver, R.string.action_show);
-        return new ShortcutInfo.Builder(mContext, screensaverShortcut)
-                .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_screensaver))
-                .setActivity(mComponentName)
-                .setShortLabel((mContext.getString(R.string.shortcut_start_screensaver_short)))
-                .setLongLabel((mContext.getString(R.string.shortcut_start_screensaver_long)))
-                .setIntent(intent)
-                .setRank(3)
-                .build();
-    }
-
-    private class StopwatchWatcher implements StopwatchListener {
-
-        @Override
-        public void stopwatchUpdated(Stopwatch before, Stopwatch after) {
-            if (!mUserManager.isUserUnlocked()) {
-                LogUtils.i("Skipping stopwatch shortcut update because user is locked.");
-                return;
-            }
-            try {
-                mShortcutManager.updateShortcuts(
-                        Collections.singletonList(createStopwatchShortcut()));
-            } catch (IllegalStateException e) {
-                LogUtils.wtf(e);
-            }
-        }
-
-        @Override
-        public void lapAdded(Lap lap) {
-        }
-    }
-}
diff --git a/src/com/android/deskclock/controller/ShortcutController.kt b/src/com/android/deskclock/controller/ShortcutController.kt
new file mode 100644
index 0000000..fd2cf5d
--- /dev/null
+++ b/src/com/android/deskclock/controller/ShortcutController.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.UserManager
+import android.provider.AlarmClock
+import androidx.annotation.StringRes
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.HandleApiCalls
+import com.android.deskclock.HandleShortcuts
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.ScreensaverActivity
+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.events.ShortcutEventTracker
+import com.android.deskclock.stopwatch.StopwatchService
+import com.android.deskclock.uidata.UiDataModel
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+internal class ShortcutController(val context: Context) {
+    private val mComponentName = ComponentName(context, DeskClock::class.java)
+    private val mShortcutManager = context.getSystemService(ShortcutManager::class.java)
+    private val mUserManager = context.getSystemService(Context.USER_SERVICE) as UserManager
+
+    init {
+        Controller.getController().addEventTracker(ShortcutEventTracker(context))
+        DataModel.dataModel.addStopwatchListener(StopwatchWatcher())
+    }
+
+    fun updateShortcuts() {
+        if (!mUserManager.isUserUnlocked()) {
+            LogUtils.i("Skipping shortcut update because user is locked.")
+            return
+        }
+        try {
+            val alarm: ShortcutInfo = createNewAlarmShortcut()
+            val timer: ShortcutInfo = createNewTimerShortcut()
+            val stopwatch: ShortcutInfo = createStopwatchShortcut()
+            val screensaver: ShortcutInfo = createScreensaverShortcut()
+            mShortcutManager.setDynamicShortcuts(listOf(alarm, timer, stopwatch, screensaver))
+        } catch (e: IllegalStateException) {
+            LogUtils.wtf(e)
+        }
+    }
+
+    private fun createNewAlarmShortcut(): ShortcutInfo {
+        val intent: Intent = Intent(AlarmClock.ACTION_SET_ALARM)
+                .setClass(context, HandleApiCalls::class.java)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+        val setAlarmShortcut = UiDataModel.uiDataModel
+                .getShortcutId(R.string.category_alarm, R.string.action_create)
+        return ShortcutInfo.Builder(context, setAlarmShortcut)
+                .setIcon(Icon.createWithResource(context, R.drawable.shortcut_new_alarm))
+                .setActivity(mComponentName)
+                .setShortLabel(context.getString(R.string.shortcut_new_alarm_short))
+                .setLongLabel(context.getString(R.string.shortcut_new_alarm_long))
+                .setIntent(intent)
+                .setRank(0)
+                .build()
+    }
+
+    private fun createNewTimerShortcut(): ShortcutInfo {
+        val intent: Intent = Intent(AlarmClock.ACTION_SET_TIMER)
+                .setClass(context, HandleApiCalls::class.java)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+        val setTimerShortcut = UiDataModel.uiDataModel
+                .getShortcutId(R.string.category_timer, R.string.action_create)
+        return ShortcutInfo.Builder(context, setTimerShortcut)
+                .setIcon(Icon.createWithResource(context, R.drawable.shortcut_new_timer))
+                .setActivity(mComponentName)
+                .setShortLabel(context.getString(R.string.shortcut_new_timer_short))
+                .setLongLabel(context.getString(R.string.shortcut_new_timer_long))
+                .setIntent(intent)
+                .setRank(1)
+                .build()
+    }
+
+    private fun createStopwatchShortcut(): ShortcutInfo {
+        @StringRes val action: Int = if (DataModel.dataModel.stopwatch.isRunning) {
+            R.string.action_pause
+        } else {
+            R.string.action_start
+        }
+        val shortcutId = UiDataModel.uiDataModel
+                .getShortcutId(R.string.category_stopwatch, action)
+        val shortcut: ShortcutInfo.Builder = ShortcutInfo.Builder(context, shortcutId)
+                .setIcon(Icon.createWithResource(context, R.drawable.shortcut_stopwatch))
+                .setActivity(mComponentName)
+                .setRank(2)
+        val intent: Intent
+        if (DataModel.dataModel.stopwatch.isRunning) {
+            intent = Intent(StopwatchService.ACTION_PAUSE_STOPWATCH)
+                    .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+            shortcut.setShortLabel(context.getString(R.string.shortcut_pause_stopwatch_short))
+                    .setLongLabel(context.getString(R.string.shortcut_pause_stopwatch_long))
+        } else {
+            intent = Intent(StopwatchService.ACTION_START_STOPWATCH)
+                    .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+            shortcut.setShortLabel(context.getString(R.string.shortcut_start_stopwatch_short))
+                    .setLongLabel(context.getString(R.string.shortcut_start_stopwatch_long))
+        }
+        intent.setClass(context, HandleShortcuts::class.java)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        return shortcut
+                .setIntent(intent)
+                .build()
+    }
+
+    private fun createScreensaverShortcut(): ShortcutInfo {
+        val intent: Intent = Intent(Intent.ACTION_MAIN)
+                .setClass(context, ScreensaverActivity::class.java)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+        val screensaverShortcut = UiDataModel.uiDataModel
+                .getShortcutId(R.string.category_screensaver, R.string.action_show)
+        return ShortcutInfo.Builder(context, screensaverShortcut)
+                .setIcon(Icon.createWithResource(context, R.drawable.shortcut_screensaver))
+                .setActivity(mComponentName)
+                .setShortLabel(context.getString(R.string.shortcut_start_screensaver_short))
+                .setLongLabel(context.getString(R.string.shortcut_start_screensaver_long))
+                .setIntent(intent)
+                .setRank(3)
+                .build()
+    }
+
+    private inner class StopwatchWatcher : StopwatchListener {
+
+        override fun stopwatchUpdated(before: Stopwatch, after: Stopwatch) {
+            if (!mUserManager.isUserUnlocked()) {
+                LogUtils.i("Skipping stopwatch shortcut update because user is locked.")
+                return
+            }
+            try {
+                mShortcutManager.updateShortcuts(listOf(createStopwatchShortcut()))
+            } catch (e: IllegalStateException) {
+                LogUtils.wtf(e)
+            }
+        }
+
+        override fun lapAdded(lap: Lap) {}
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/VoiceController.java b/src/com/android/deskclock/controller/VoiceController.java
deleted file mode 100644
index 1eae560..0000000
--- a/src/com/android/deskclock/controller/VoiceController.java
+++ /dev/null
@@ -1,68 +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.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/controller/VoiceController.kt b/src/com/android/deskclock/controller/VoiceController.kt
new file mode 100644
index 0000000..b5fb146
--- /dev/null
+++ b/src/com/android/deskclock/controller/VoiceController.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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)
+internal class VoiceController {
+    /**
+     * If the `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
+     */
+    fun notifyVoiceSuccess(activity: Activity, message: String) {
+        if (!Utils.isMOrLater) {
+            return
+        }
+
+        val voiceInteractor: VoiceInteractor? = activity.getVoiceInteractor()
+        voiceInteractor?.let {
+            val prompt = Prompt(message)
+            it.submitRequest(CompleteVoiceRequest(prompt, null))
+        }
+    }
+
+    /**
+     * If the `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
+     */
+    fun notifyVoiceFailure(activity: Activity, message: String) {
+        if (!Utils.isMOrLater) {
+            return
+        }
+
+        val voiceInteractor: VoiceInteractor? = activity.getVoiceInteractor()
+        voiceInteractor?.let {
+            val prompt = Prompt(message)
+            it.submitRequest(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
deleted file mode 100644
index da50fe5..0000000
--- a/src/com/android/deskclock/data/AlarmModel.java
+++ /dev/null
@@ -1,97 +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.data;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-
-import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
-import com.android.deskclock.provider.Alarm;
-
-/**
- * All alarm data will eventually be accessed via this model.
- */
-final class AlarmModel {
-
-    /** 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;
-
-    AlarmModel(Context context, SettingsModel settingsModel) {
-        mSettingsModel = settingsModel;
-
-        // Clear caches affected by system settings when system settings change.
-        final ContentResolver cr = context.getContentResolver();
-        final ContentObserver observer = new SystemAlarmAlertChangeObserver();
-        cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer);
-    }
-
-    Uri getDefaultAlarmRingtoneUri() {
-        if (mDefaultAlarmRingtoneUri == null) {
-            mDefaultAlarmRingtoneUri = mSettingsModel.getDefaultAlarmRingtoneUri();
-        }
-
-        return mDefaultAlarmRingtoneUri;
-    }
-
-    void setDefaultAlarmRingtoneUri(Uri uri) {
-        // Never set the silent ringtone as default; new alarms should always make sound by default.
-        if (!Alarm.NO_RINGTONE_URI.equals(uri)) {
-            mSettingsModel.setDefaultAlarmRingtoneUri(uri);
-            mDefaultAlarmRingtoneUri = uri;
-        }
-    }
-
-    long getAlarmCrescendoDuration() {
-        return mSettingsModel.getAlarmCrescendoDuration();
-    }
-
-    AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
-        return mSettingsModel.getAlarmVolumeButtonBehavior();
-    }
-
-    int getAlarmTimeout() {
-        return mSettingsModel.getAlarmTimeout();
-    }
-
-    int getSnoozeLength() {
-        return mSettingsModel.getSnoozeLength();
-    }
-
-    /**
-     * 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);
-            mDefaultAlarmRingtoneUri = null;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/AlarmModel.kt b/src/com/android/deskclock/data/AlarmModel.kt
new file mode 100644
index 0000000..df9fe20
--- /dev/null
+++ b/src/com/android/deskclock/data/AlarmModel.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ContentResolver
+import android.content.Context
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+
+/**
+ * All alarm data will eventually be accessed via this model.
+ */
+internal class AlarmModel(
+    context: Context,
+    /** The model from which settings are fetched.  */
+    private val mSettingsModel: SettingsModel
+) {
+
+    /** The uri of the default ringtone to play for new alarms; mirrors last selection.  */
+    private var mDefaultAlarmRingtoneUri: Uri? = null
+
+    init {
+        // Clear caches affected by system settings when system settings change.
+        val cr: ContentResolver = context.getContentResolver()
+        val observer: ContentObserver = SystemAlarmAlertChangeObserver()
+        cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer)
+    }
+
+    // Never set the silent ringtone as default; new alarms should always make sound by default.
+    var defaultAlarmRingtoneUri: Uri
+        get() {
+            if (mDefaultAlarmRingtoneUri == null) {
+                mDefaultAlarmRingtoneUri = mSettingsModel.defaultAlarmRingtoneUri
+            }
+
+            return mDefaultAlarmRingtoneUri!!
+        }
+        set(uri) {
+            // Never set the silent ringtone as default; new alarms should always make sound by default.
+            if (!AlarmSettingColumns.NO_RINGTONE_URI.equals(uri)) {
+                mSettingsModel.defaultAlarmRingtoneUri = uri
+                mDefaultAlarmRingtoneUri = uri
+            }
+        }
+
+    val alarmCrescendoDuration: Long
+        get() = mSettingsModel.alarmCrescendoDuration
+
+    val alarmVolumeButtonBehavior: DataModel.AlarmVolumeButtonBehavior
+        get() = mSettingsModel.alarmVolumeButtonBehavior
+
+    val alarmTimeout: Int
+        get() = mSettingsModel.alarmTimeout
+
+    val snoozeLength: Int
+        get() = mSettingsModel.snoozeLength
+
+    /**
+     * This receiver is notified when system settings change. Cached information built on
+     * those system settings must be cleared.
+     */
+    private inner class SystemAlarmAlertChangeObserver
+        : ContentObserver(Handler(Looper.myLooper()!!)) {
+        override fun onChange(selfChange: Boolean) {
+            super.onChange(selfChange)
+            mDefaultAlarmRingtoneUri = null
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/City.java b/src/com/android/deskclock/data/City.java
deleted file mode 100644
index 7a41d08..0000000
--- a/src/com/android/deskclock/data/City.java
+++ /dev/null
@@ -1,220 +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.data;
-
-import java.text.Collator;
-import java.util.Comparator;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * A read-only domain object representing a city of the world and associated time information. It
- * also contains static comparators that can be instantiated to order cities in common sort orders.
- */
-public final class City {
-
-    /** A unique identifier for the city. */
-    private final String mId;
-
-    /** An optional numeric index used to order cities for display; -1 if no such index exists. */
-    private final int mIndex;
-
-    /** An index string used to order cities for display. */
-    private final String mIndexString;
-
-    /** The display name of the city. */
-    private final String mName;
-
-    /** The phonetic name of the city used to order cities for display. */
-    private final String mPhoneticName;
-
-    /** The TimeZone corresponding to the city. */
-    private final TimeZone mTimeZone;
-
-    /** A cached upper case form of the {@link #mName} used in case-insensitive name comparisons. */
-    private String mNameUpperCase;
-
-    /**
-     * A cached upper case form of the {@link #mName} used in case-insensitive name comparisons
-     * which ignore {@link #removeSpecialCharacters(String)} special characters.
-     */
-    private String mNameUpperCaseNoSpecialCharacters;
-
-    City(String id, int index, String indexString, String name, String phoneticName, TimeZone tz) {
-        mId = id;
-        mIndex = index;
-        mIndexString = indexString;
-        mName = name;
-        mPhoneticName = phoneticName;
-        mTimeZone = tz;
-    }
-
-    public String getId() { return mId; }
-    public int getIndex() { return mIndex; }
-    public String getName() { return mName; }
-    public TimeZone getTimeZone() { return mTimeZone; }
-    public String getIndexString() { return mIndexString; }
-    public String getPhoneticName() { return mPhoneticName; }
-
-    /**
-     * @return the city name converted to upper case
-     */
-    public String getNameUpperCase() {
-        if (mNameUpperCase == null) {
-            mNameUpperCase = mName.toUpperCase();
-        }
-        return mNameUpperCase;
-    }
-
-    /**
-     * @return the city name converted to upper case with all special characters removed
-     */
-    private String getNameUpperCaseNoSpecialCharacters() {
-        if (mNameUpperCaseNoSpecialCharacters == null) {
-            mNameUpperCaseNoSpecialCharacters = removeSpecialCharacters(getNameUpperCase());
-        }
-        return mNameUpperCaseNoSpecialCharacters;
-    }
-
-    /**
-     * @param upperCaseQueryNoSpecialCharacters search term with all special characters removed
-     *      to match against the upper case city name
-     * @return {@code true} iff the name of this city starts with the given query
-     */
-    public boolean matches(String upperCaseQueryNoSpecialCharacters) {
-        // By removing all special characters, prefix matching becomes more liberal and it is easier
-        // to locate the desired city. e.g. "St. Lucia" is matched by "StL", "St.L", "St L", "St. L"
-        return getNameUpperCaseNoSpecialCharacters().startsWith(upperCaseQueryNoSpecialCharacters);
-    }
-
-    @Override
-    public String toString() {
-        return String.format(Locale.US,
-                "City {id=%s, index=%d, indexString=%s, name=%s, phonetic=%s, tz=%s}",
-                mId, mIndex, mIndexString, mName, mPhoneticName, mTimeZone.getID());
-    }
-
-    /**
-     * Strips out any characters considered optional for matching purposes. These include spaces,
-     * dashes, periods and apostrophes.
-     *
-     * @param token a city name or search term
-     * @return the given {@code token} without any characters considered optional when matching
-     */
-    public static String removeSpecialCharacters(String token) {
-        return token.replaceAll("[ -.']", "");
-    }
-
-    /**
-     * Orders by:
-     *
-     * <ol>
-     *     <li>UTC offset of {@link #getTimeZone() timezone}</li>
-     *     <li>{@link #getIndex() numeric index}</li>
-     *     <li>{@link #getIndexString()} alphabetic index}</li>
-     *     <li>{@link #getPhoneticName() phonetic name}</li>
-     * </ol>
-     */
-    public static final class UtcOffsetComparator implements Comparator<City> {
-
-        private final Comparator<City> mDelegate1 = new UtcOffsetIndexComparator();
-
-        private final Comparator<City> mDelegate2 = new NameComparator();
-
-        public int compare(City c1, City c2) {
-            int result = mDelegate1.compare(c1, c2);
-
-            if (result == 0) {
-                result = mDelegate2.compare(c1, c2);
-            }
-
-            return result;
-        }
-    }
-
-    /**
-     * Orders by:
-     *
-     * <ol>
-     *     <li>UTC offset of {@link #getTimeZone() timezone}</li>
-     * </ol>
-     */
-    public static final class UtcOffsetIndexComparator implements Comparator<City> {
-
-        // Snapshot the current time when the Comparator is created to obtain consistent offsets.
-        private final long now = System.currentTimeMillis();
-
-        public int compare(City c1, City c2) {
-            final int utcOffset1 = c1.getTimeZone().getOffset(now);
-            final int utcOffset2 = c2.getTimeZone().getOffset(now);
-            return Integer.compare(utcOffset1, utcOffset2);
-        }
-    }
-
-    /**
-     * This comparator sorts using the city fields that influence natural name sort order:
-     *
-     * <ol>
-     *     <li>{@link #getIndex() numeric index}</li>
-     *     <li>{@link #getIndexString()} alphabetic index}</li>
-     *     <li>{@link #getPhoneticName() phonetic name}</li>
-     * </ol>
-     */
-    public static final class NameComparator implements Comparator<City> {
-
-        private final Comparator<City> mDelegate = new NameIndexComparator();
-
-        // Locale-sensitive comparator for phonetic names.
-        private final Collator mNameCollator = Collator.getInstance();
-
-        @Override
-        public int compare(City c1, City c2) {
-            int result = mDelegate.compare(c1, c2);
-
-            if (result == 0) {
-                result = mNameCollator.compare(c1.getPhoneticName(), c2.getPhoneticName());
-            }
-
-            return result;
-        }
-    }
-
-    /**
-     * Orders by:
-     *
-     * <ol>
-     *     <li>{@link #getIndex() numeric index}</li>
-     *     <li>{@link #getIndexString()} alphabetic index}</li>
-     * </ol>
-     */
-    public static final class NameIndexComparator implements Comparator<City> {
-
-        // Locale-sensitive comparator for index strings.
-        private final Collator mNameCollator = Collator.getInstance();
-
-        @Override
-        public int compare(City c1, City c2) {
-            int result = Integer.compare(c1.getIndex(), c2.getIndex());
-
-            if (result == 0) {
-                result = mNameCollator.compare(c1.getIndexString(), c2.getIndexString());
-            }
-
-            return result;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/City.kt b/src/com/android/deskclock/data/City.kt
new file mode 100644
index 0000000..4a8abf3
--- /dev/null
+++ b/src/com/android/deskclock/data/City.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.text.Collator
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * A read-only domain object representing a city of the world and associated time information. It
+ * also contains static comparators that can be instantiated to order cities in common sort orders.
+ */
+class City internal constructor(
+    /** A unique identifier for the city.  */
+    val id: String?,
+    /** An optional numeric index used to order cities for display; -1 if no such index exists.  */
+    val index: Int,
+    /** An index string used to order cities for display.  */
+    val indexString: String?,
+    /** The display name of the city.  */
+    val name: String,
+    /** The phonetic name of the city used to order cities for display.  */
+    val phoneticName: String,
+    /** The TimeZone corresponding to the city.  */
+    val timeZone: TimeZone
+) {
+
+    /** A cached upper case form of the [.mName] used in case-insensitive name comparisons.  */
+    private var mNameUpperCase: String? = null
+
+    /**
+     * A cached upper case form of the [.mName] used in case-insensitive name comparisons
+     * which ignore [.removeSpecialCharacters] special characters.
+     */
+    private var mNameUpperCaseNoSpecialCharacters: String? = null
+
+    /**
+     * @return the city name converted to upper case
+     */
+    val nameUpperCase: String
+        get() {
+            if (mNameUpperCase == null) {
+                mNameUpperCase = name.toUpperCase()
+            }
+            return mNameUpperCase!!
+        }
+
+    /**
+     * @return the city name converted to upper case with all special characters removed
+     */
+    private val nameUpperCaseNoSpecialCharacters: String
+        get() {
+            if (mNameUpperCaseNoSpecialCharacters == null) {
+                mNameUpperCaseNoSpecialCharacters = removeSpecialCharacters(nameUpperCase)
+            }
+            return mNameUpperCaseNoSpecialCharacters!!
+        }
+
+    /**
+     * @param upperCaseQueryNoSpecialCharacters search term with all special characters removed
+     * to match against the upper case city name
+     * @return `true` iff the name of this city starts with the given query
+     */
+    fun matches(upperCaseQueryNoSpecialCharacters: String): Boolean {
+        // By removing all special characters, prefix matching becomes more liberal and it is easier
+        // to locate the desired city. e.g. "St. Lucia" is matched by "StL", "St.L", "St L", "St. L"
+        return nameUpperCaseNoSpecialCharacters.startsWith(upperCaseQueryNoSpecialCharacters)
+    }
+
+    override fun toString(): String {
+        return String.format(Locale.US,
+                "City {id=%s, index=%d, indexString=%s, name=%s, phonetic=%s, tz=%s}",
+                id, index, indexString, name, phoneticName, timeZone.id)
+    }
+
+    /**
+     * Orders by:
+     *
+     *  1. UTC offset of [timezone][.getTimeZone]
+     *  1. [numeric index][.getIndex]
+     *  1. [.getIndexString] alphabetic index}
+     *  1. [phonetic name][.getPhoneticName]
+     */
+    class UtcOffsetComparator : Comparator<City> {
+        private val mDelegate1: Comparator<City> = UtcOffsetIndexComparator()
+        private val mDelegate2: Comparator<City> = NameComparator()
+
+        override fun compare(c1: City, c2: City): Int {
+            var result = mDelegate1.compare(c1, c2)
+
+            if (result == 0) {
+                result = mDelegate2.compare(c1, c2)
+            }
+
+            return result
+        }
+    }
+
+    /**
+     * Orders by:
+     *
+     *  1. UTC offset of [timezone][.getTimeZone]
+     */
+    class UtcOffsetIndexComparator : Comparator<City> {
+        // Snapshot the current time when the Comparator is created to obtain consistent offsets.
+        private val now = System.currentTimeMillis()
+
+        override fun compare(c1: City, c2: City): Int {
+            val utcOffset1 = c1.timeZone.getOffset(now)
+            val utcOffset2 = c2.timeZone.getOffset(now)
+            return utcOffset1.compareTo(utcOffset2)
+        }
+    }
+
+    /**
+     * This comparator sorts using the city fields that influence natural name sort order:
+     *
+     *  1. [numeric index][.getIndex]
+     *  1. [.getIndexString] alphabetic index}
+     *  1. [phonetic name][.getPhoneticName]
+     */
+    class NameComparator : Comparator<City> {
+        private val mDelegate: Comparator<City> = NameIndexComparator()
+
+        // Locale-sensitive comparator for phonetic names.
+        private val mNameCollator = Collator.getInstance()
+
+        override fun compare(c1: City, c2: City): Int {
+            var result = mDelegate.compare(c1, c2)
+
+            if (result == 0) {
+                result = mNameCollator.compare(c1.phoneticName, c2.phoneticName)
+            }
+
+            return result
+        }
+    }
+
+    /**
+     * Orders by:
+     *
+     *  1. [numeric index][.getIndex]
+     *  1. [.getIndexString] alphabetic index}
+     */
+    class NameIndexComparator : Comparator<City> {
+        // Locale-sensitive comparator for index strings.
+        private val mNameCollator = Collator.getInstance()
+
+        override fun compare(c1: City, c2: City): Int {
+            var result = c1.index.compareTo(c2.index)
+
+            if (result == 0) {
+                result = mNameCollator.compare(c1.indexString, c2.indexString)
+            }
+
+            return result
+        }
+    }
+
+    companion object {
+        /**
+         * Strips out any characters considered optional for matching purposes. These include spaces,
+         * dashes, periods and apostrophes.
+         *
+         * @param token a city name or search term
+         * @return the given `token` without any characters considered optional when matching
+         */
+        @JvmStatic
+        fun removeSpecialCharacters(token: String): String {
+            return token.replace("[ -.']".toRegex(), "")
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityDAO.java b/src/com/android/deskclock/data/CityDAO.java
deleted file mode 100644
index c75298a..0000000
--- a/src/com/android/deskclock/data/CityDAO.java
+++ /dev/null
@@ -1,166 +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.data;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import androidx.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.ArrayMap;
-
-import com.android.deskclock.R;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * This class encapsulates the transfer of data between {@link City} domain objects and their
- * permanent storage in {@link Resources} and {@link SharedPreferences}.
- */
-final class CityDAO {
-
-    /** Regex to match numeric index values when parsing city names. */
-    private static final Pattern NUMERIC_INDEX_REGEX = Pattern.compile("\\d+");
-
-    /** Key to a preference that stores the number of selected cities. */
-    private static final String NUMBER_OF_CITIES = "number_of_cities";
-
-    /** Prefix for a key to a preference that stores the id of a selected city. */
-    private static final String CITY_ID = "city_id_";
-
-    private CityDAO() {}
-
-    /**
-     * @param cityMap maps city ids to city instances
-     * @return the list of city ids selected for display by the user
-     */
-    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);
-
-        for (int i = 0; i < size; i++) {
-            final String id = prefs.getString(CITY_ID + i, null);
-            final City city = cityMap.get(id);
-            if (city != null) {
-                selectedCities.add(city);
-            }
-        }
-
-        return selectedCities;
-    }
-
-    /**
-     * @param cities the collection of cities selected for display by the user
-     */
-    static void setSelectedCities(SharedPreferences prefs, Collection<City> cities) {
-        final SharedPreferences.Editor editor = prefs.edit();
-        editor.putInt(NUMBER_OF_CITIES, cities.size());
-
-        int count = 0;
-        for (City city : cities) {
-            editor.putString(CITY_ID + count, city.getId());
-            count++;
-        }
-
-        editor.apply();
-    }
-
-    /**
-     * @return the domain of cities from which the user may choose a world clock
-     */
-    static Map<String, City> getCities(Context context) {
-        final Resources resources = context.getResources();
-        final TypedArray cityStrings = resources.obtainTypedArray(R.array.city_ids);
-        final int citiesCount = cityStrings.length();
-
-        final Map<String, City> cities = new ArrayMap<>(citiesCount);
-        try {
-            for (int i = 0; i < citiesCount; ++i) {
-                // Attempt to locate the resource id defining the city as a string.
-                final int cityResourceId = cityStrings.getResourceId(i, 0);
-                if (cityResourceId == 0) {
-                    final String message = String.format(Locale.ENGLISH,
-                            "Unable to locate city resource id for index %d", i);
-                    throw new IllegalStateException(message);
-                }
-
-                final String id = resources.getResourceEntryName(cityResourceId);
-                final String cityString = cityStrings.getString(i);
-                if (cityString == null) {
-                    final String message = String.format("Unable to locate city with id %s", id);
-                    throw new IllegalStateException(message);
-                }
-
-                // Attempt to parse the time zone from the city entry.
-                final String[] cityParts = cityString.split("[|]");
-                if (cityParts.length != 2) {
-                    final String message = String.format(
-                            "Error parsing malformed city %s", cityString);
-                    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);
-                }
-            }
-        } finally {
-            cityStrings.recycle();
-        }
-
-        return Collections.unmodifiableMap(cities);
-    }
-
-    /**
-     * @param id unique identifier for city
-     * @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 tzId the string id of the timezone a given city is located in
-     */
-    @VisibleForTesting
-    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
-        // if one is not explicitly provided.
-        final String indexString = TextUtils.isEmpty(parts[0])
-                ? name.substring(0, 1) : parts[0];
-        final String phoneticName = parts.length == 3 ? parts[2] : name;
-
-        final Matcher matcher = NUMERIC_INDEX_REGEX.matcher(indexString);
-        final int index = matcher.find() ? Integer.parseInt(matcher.group()) : -1;
-
-        return new City(id, index, indexString, name, phoneticName, tz);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityDAO.kt b/src/com/android/deskclock/data/CityDAO.kt
new file mode 100644
index 0000000..4f38b73
--- /dev/null
+++ b/src/com/android/deskclock/data/CityDAO.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.text.TextUtils
+import android.util.ArrayMap
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.R
+
+import java.util.Locale
+import java.util.regex.Pattern
+import java.util.TimeZone
+
+/**
+ * This class encapsulates the transfer of data between [City] domain objects and their
+ * permanent storage in [Resources] and [SharedPreferences].
+ */
+internal object CityDAO {
+    /** Regex to match numeric index values when parsing city names.  */
+    private val NUMERIC_INDEX_REGEX = Pattern.compile("\\d+")
+
+    /** Key to a preference that stores the number of selected cities.  */
+    private const val NUMBER_OF_CITIES = "number_of_cities"
+
+    /** Prefix for a key to a preference that stores the id of a selected city.  */
+    private const val CITY_ID = "city_id_"
+
+    /**
+     * @param cityMap maps city ids to city instances
+     * @return the list of city ids selected for display by the user
+     */
+    fun getSelectedCities(prefs: SharedPreferences, cityMap: Map<String, City>): List<City> {
+        val size: Int = prefs.getInt(NUMBER_OF_CITIES, 0)
+        val selectedCities: MutableList<City> = ArrayList(size)
+
+        for (i in 0 until size) {
+            val id: String? = prefs.getString(CITY_ID + i, null)
+            val city = cityMap[id]
+            if (city != null) {
+                selectedCities.add(city)
+            }
+        }
+
+        return selectedCities
+    }
+
+    /**
+     * @param cities the collection of cities selected for display by the user
+     */
+    fun setSelectedCities(prefs: SharedPreferences, cities: Collection<City>) {
+        val editor: SharedPreferences.Editor = prefs.edit()
+        editor.putInt(NUMBER_OF_CITIES, cities.size)
+
+        for ((count, city) in cities.withIndex()) {
+            editor.putString(CITY_ID + count, city.id)
+        }
+
+        editor.apply()
+    }
+
+    /**
+     * @return the domain of cities from which the user may choose a world clock
+     */
+    fun getCities(context: Context): Map<String, City> {
+        val resources: Resources = context.getResources()
+        val cityStrings: TypedArray = resources.obtainTypedArray(R.array.city_ids)
+        val citiesCount: Int = cityStrings.length()
+
+        val cities: MutableMap<String, City> = ArrayMap(citiesCount)
+        try {
+            for (i in 0 until citiesCount) {
+                // Attempt to locate the resource id defining the city as a string.
+                val cityResourceId: Int = cityStrings.getResourceId(i, 0)
+                if (cityResourceId == 0) {
+                    val message = String.format(Locale.ENGLISH,
+                            "Unable to locate city resource id for index %d", i)
+                    throw IllegalStateException(message)
+                }
+
+                val id: String = resources.getResourceEntryName(cityResourceId)
+                val cityString: String? = cityStrings.getString(i)
+                if (cityString == null) {
+                    val message = String.format("Unable to locate city with id %s", id)
+                    throw IllegalStateException(message)
+                }
+
+                // Attempt to parse the time zone from the city entry.
+                val cityParts = cityString.split("[|]".toRegex()).toTypedArray()
+                if (cityParts.size != 2) {
+                    val message = String.format(
+                            "Error parsing malformed city %s", cityString)
+                    throw IllegalStateException(message)
+                }
+
+                val city = createCity(id, cityParts[0], cityParts[1])
+                // Skip cities whose timezone cannot be resolved.
+                if (city != null) {
+                    cities[id] = city
+                }
+            }
+        } finally {
+            cityStrings.recycle()
+        }
+
+        return cities
+    }
+
+    /**
+     * @param id unique identifier for city
+     * @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 tzId the string id of the timezone a given city is located in
+     */
+    @VisibleForTesting
+    fun createCity(id: String?, formattedName: String, tzId: String?): City? {
+        val tz = TimeZone.getTimeZone(tzId)
+        // If the time zone lookup fails, GMT is returned. No cities actually map to GMT.
+        if ("GMT" == tz.id) {
+            return null
+        }
+
+        val parts = formattedName.split("[=:]".toRegex()).toTypedArray()
+        val name = parts[1]
+        // Extract index string from input, use the first character of city name as the index string
+        // if one is not explicitly provided.
+        val indexString = if (TextUtils.isEmpty(parts[0])) name.substring(0, 1) else parts[0]
+        val phoneticName = if (parts.size == 3) parts[2] else name
+
+        val matcher = NUMERIC_INDEX_REGEX.matcher(indexString)
+        val index = if (matcher.find()) matcher.group().toInt() else -1
+
+        return City(id, index, indexString, name, phoneticName, tz)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityListener.java b/src/com/android/deskclock/data/CityListener.kt
similarity index 75%
rename from src/com/android/deskclock/data/CityListener.java
rename to src/com/android/deskclock/data/CityListener.kt
index 91f66b3..da6f089 100644
--- a/src/com/android/deskclock/data/CityListener.java
+++ b/src/com/android/deskclock/data/CityListener.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.data;
-
-import java.util.List;
+package com.android.deskclock.data
 
 /**
  * The interface through which interested parties are notified of changes to the world cities list.
  */
-public interface CityListener {
-    void citiesChanged(List<City> oldCities, List<City> newCities);
+interface CityListener {
+    fun citiesChanged(oldCities: List<City>, newCities: List<City>)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityModel.java b/src/com/android/deskclock/data/CityModel.java
deleted file mode 100644
index 9f45875..0000000
--- a/src/com/android/deskclock/data/CityModel.java
+++ /dev/null
@@ -1,275 +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.data;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel.CitySort;
-import com.android.deskclock.settings.SettingsActivity;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimeZone;
-
-/**
- * All {@link City} data is accessed via this model.
- */
-final class CityModel {
-
-    private final Context mContext;
-
-    private final SharedPreferences mPrefs;
-
-    /** The model from which settings are fetched. */
-    private final SettingsModel mSettingsModel;
-
-    /**
-     * Retain a hard reference to the shared preference observer to prevent it from being garbage
-     * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
-     */
-    @SuppressWarnings("FieldCanBeLocal")
-    private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
-
-    /** Clears data structures containing data that is locale-sensitive. */
-    @SuppressWarnings("FieldCanBeLocal")
-    private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
-    /** List of listeners to invoke upon world city list change */
-    private final List<CityListener> mCityListeners = new ArrayList<>();
-
-    /** Maps city ID to city instance. */
-    private Map<String, City> mCityMap;
-
-    /** List of city instances in display order. */
-    private List<City> mAllCities;
-
-    /** List of selected city instances in display order. */
-    private List<City> mSelectedCities;
-
-    /** List of unselected city instances in display order. */
-    private List<City> mUnselectedCities;
-
-    /** A city instance representing the home timezone of the user. */
-    private City mHomeCity;
-
-    CityModel(Context context, SharedPreferences prefs, SettingsModel settingsModel) {
-        mContext = context;
-        mPrefs = prefs;
-        mSettingsModel = settingsModel;
-
-        // Clear caches affected by locale when locale changes.
-        final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
-        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
-
-        // Clear caches affected by preferences when preferences change.
-        prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
-    }
-
-    void addCityListener(CityListener cityListener) {
-        mCityListeners.add(cityListener);
-    }
-
-    void removeCityListener(CityListener cityListener) {
-        mCityListeners.remove(cityListener);
-    }
-
-    /**
-     * @return a list of all cities in their display order
-     */
-    List<City> getAllCities() {
-        if (mAllCities == null) {
-            // Create a set of selections to identify the unselected cities.
-            final List<City> selected = new ArrayList<>(getSelectedCities());
-
-            // Sort the selected cities alphabetically by name.
-            Collections.sort(selected, new City.NameComparator());
-
-            // Combine selected and unselected cities into a single list.
-            final List<City> allCities = new ArrayList<>(getCityMap().size());
-            allCities.addAll(selected);
-            allCities.addAll(getUnselectedCities());
-            mAllCities = Collections.unmodifiableList(allCities);
-        }
-
-        return mAllCities;
-    }
-
-    /**
-     * @return a city representing the user's home timezone
-     */
-    City getHomeCity() {
-        if (mHomeCity == null) {
-            final String name = mContext.getString(R.string.home_label);
-            final TimeZone timeZone = mSettingsModel.getHomeTimeZone();
-            mHomeCity = new City(null, -1, null, name, name, timeZone);
-        }
-
-        return mHomeCity;
-    }
-
-    /**
-     * @return a list of cities not selected for display
-     */
-    List<City> getUnselectedCities() {
-        if (mUnselectedCities == null) {
-            // Create a set of selections to identify the unselected cities.
-            final List<City> selected = new ArrayList<>(getSelectedCities());
-            final Set<City> selectedSet = Utils.newArraySet(selected);
-
-            final Collection<City> all = getCityMap().values();
-            final List<City> unselected = new ArrayList<>(all.size() - selectedSet.size());
-            for (City city : all) {
-                if (!selectedSet.contains(city)) {
-                    unselected.add(city);
-                }
-            }
-
-            // Sort the unselected cities according by the user's preferred sort.
-            Collections.sort(unselected, getCitySortComparator());
-            mUnselectedCities = Collections.unmodifiableList(unselected);
-        }
-
-        return mUnselectedCities;
-    }
-
-    /**
-     * @return a list of cities selected for display
-     */
-    List<City> getSelectedCities() {
-        if (mSelectedCities == null) {
-            final List<City> selectedCities = CityDAO.getSelectedCities(mPrefs, getCityMap());
-            Collections.sort(selectedCities, new City.UtcOffsetComparator());
-            mSelectedCities = Collections.unmodifiableList(selectedCities);
-        }
-
-        return mSelectedCities;
-    }
-
-    /**
-     * @param cities the new collection of cities selected for display by the user
-     */
-    void setSelectedCities(Collection<City> cities) {
-        final List<City> oldCities = getAllCities();
-        CityDAO.setSelectedCities(mPrefs, cities);
-
-        // Clear caches affected by this update.
-        mAllCities = null;
-        mSelectedCities = null;
-        mUnselectedCities = null;
-
-        // Broadcast the change to the selected cities for the benefit of widgets.
-        fireCitiesChanged(oldCities, getAllCities());
-    }
-
-    /**
-     * @return a comparator used to locate index positions
-     */
-    Comparator<City> getCityIndexComparator() {
-        final CitySort citySort = mSettingsModel.getCitySort();
-        switch (citySort) {
-            case NAME: return new City.NameIndexComparator();
-            case UTC_OFFSET: return new City.UtcOffsetIndexComparator();
-        }
-        throw new IllegalStateException("unexpected city sort: " + citySort);
-    }
-
-    /**
-     * @return the order in which cities are sorted
-     */
-    CitySort getCitySort() {
-        return mSettingsModel.getCitySort();
-    }
-
-    /**
-     * Adjust the order in which cities are sorted.
-     */
-    void toggleCitySort() {
-        mSettingsModel.toggleCitySort();
-
-        // Clear caches affected by this update.
-        mAllCities = null;
-        mUnselectedCities = null;
-    }
-
-    private Map<String, City> getCityMap() {
-        if (mCityMap == null) {
-            mCityMap = CityDAO.getCities(mContext);
-        }
-
-        return mCityMap;
-    }
-
-    private Comparator<City> getCitySortComparator() {
-        final CitySort citySort = mSettingsModel.getCitySort();
-        switch (citySort) {
-            case NAME: return new City.NameComparator();
-            case UTC_OFFSET: return new City.UtcOffsetComparator();
-        }
-        throw new IllegalStateException("unexpected city sort: " + citySort);
-    }
-
-    private void fireCitiesChanged(List<City> oldCities, List<City> newCities) {
-        mContext.sendBroadcast(new Intent(DataModel.ACTION_WORLD_CITIES_CHANGED));
-        for (CityListener cityListener : mCityListeners) {
-            cityListener.citiesChanged(oldCities, newCities);
-        }
-    }
-
-    /**
-     * 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) {
-            mCityMap = null;
-            mHomeCity = null;
-            mAllCities = null;
-            mSelectedCities = null;
-            mUnselectedCities = null;
-        }
-    }
-
-    /**
-     * This receiver is notified when shared preferences change. Cached information built on
-     * preferences must be cleared.
-     */
-    private final class PreferenceListener implements OnSharedPreferenceChangeListener {
-        @Override
-        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
-            switch (key) {
-                case SettingsActivity.KEY_HOME_TZ:
-                    mHomeCity = null;
-                case SettingsActivity.KEY_AUTO_HOME_CLOCK:
-                    final List<City> cities = getAllCities();
-                    fireCitiesChanged(cities, cities);
-                    break;
-            }
-        }
-    }
-}
diff --git a/src/com/android/deskclock/data/CityModel.kt b/src/com/android/deskclock/data/CityModel.kt
new file mode 100644
index 0000000..258f179
--- /dev/null
+++ b/src/com/android/deskclock/data/CityModel.kt
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.City.NameComparator
+import com.android.deskclock.data.City.NameIndexComparator
+import com.android.deskclock.data.City.UtcOffsetComparator
+import com.android.deskclock.data.City.UtcOffsetIndexComparator
+import com.android.deskclock.data.DataModel.CitySort
+import com.android.deskclock.settings.SettingsActivity
+
+import java.util.Collections
+
+/**
+ * All [City] data is accessed via this model.
+ */
+internal class CityModel(
+    private val context: Context,
+    private val prefs: SharedPreferences,
+    /** The model from which settings are fetched.  */
+    private val settingsModel: SettingsModel
+) {
+
+    /**
+     * Retain a hard reference to the shared preference observer to prevent it from being garbage
+     * collected. See [SharedPreferences.registerOnSharedPreferenceChangeListener] for detail.
+     */
+    private val mPreferenceListener: OnSharedPreferenceChangeListener = PreferenceListener()
+
+    /** Clears data structures containing data that is locale-sensitive.  */
+    private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+    /** List of listeners to invoke upon world city list change  */
+    private val mCityListeners: MutableList<CityListener> = ArrayList()
+
+    /** Maps city ID to city instance.  */
+    private var mCityMap: Map<String, City>? = null
+
+    /** List of city instances in display order.  */
+    private var mAllCities: List<City>? = null
+
+    /** List of selected city instances in display order.  */
+    private var mSelectedCities: List<City>? = null
+
+    /** List of unselected city instances in display order.  */
+    private var mUnselectedCities: List<City>? = null
+
+    /** A city instance representing the home timezone of the user.  */
+    private var mHomeCity: City? = null
+
+    init {
+        // Clear caches affected by locale when locale changes.
+        val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+        context.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+
+        // Clear caches affected by preferences when preferences change.
+        prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener)
+    }
+
+    fun addCityListener(cityListener: CityListener) {
+        mCityListeners.add(cityListener)
+    }
+
+    fun removeCityListener(cityListener: CityListener) {
+        mCityListeners.remove(cityListener)
+    }
+
+    /**
+     * @return a list of all cities in their display order
+     */
+    val allCities: List<City>
+        get() {
+            if (mAllCities == null) {
+                // Create a set of selections to identify the unselected cities.
+                val selected: List<City> = selectedCities.toMutableList()
+
+                // Sort the selected cities alphabetically by name.
+                Collections.sort(selected, NameComparator())
+
+                // Combine selected and unselected cities into a single list.
+                val allCities: MutableList<City> = ArrayList(cityMap.size)
+                allCities.addAll(selected)
+                allCities.addAll(unselectedCities)
+                mAllCities = allCities
+            }
+
+            return mAllCities!!
+        }
+
+    /**
+     * @return a city representing the user's home timezone
+     */
+    val homeCity: City
+        get() {
+            if (mHomeCity == null) {
+                val name: String = context.getString(R.string.home_label)
+                val timeZone = settingsModel.homeTimeZone
+                mHomeCity = City(null, -1, null, name, name, timeZone)
+            }
+
+            return mHomeCity!!
+        }
+
+    /**
+     * @return a list of cities not selected for display
+     */
+    val unselectedCities: List<City>
+        get() {
+            if (mUnselectedCities == null) {
+                // Create a set of selections to identify the unselected cities.
+                val selected: List<City> = selectedCities.toMutableList()
+                val selectedSet: Set<City> = Utils.newArraySet(selected)
+
+                val all = cityMap.values
+                val unselected: MutableList<City> = ArrayList(all.size - selectedSet.size)
+                for (city in all) {
+                    if (!selectedSet.contains(city)) {
+                        unselected.add(city)
+                    }
+                }
+
+                // Sort the unselected cities according by the user's preferred sort.
+                Collections.sort(unselected, citySortComparator)
+                mUnselectedCities = unselected
+            }
+
+            return mUnselectedCities!!
+        }
+
+    /**
+     * @return a list of cities selected for display
+     */
+    val selectedCities: List<City>
+        get() {
+            if (mSelectedCities == null) {
+                val selectedCities = CityDAO.getSelectedCities(prefs, cityMap)
+                Collections.sort(selectedCities, UtcOffsetComparator())
+                mSelectedCities = selectedCities
+            }
+
+            return mSelectedCities!!
+        }
+
+    /**
+     * @param cities the new collection of cities selected for display by the user
+     */
+    fun setSelectedCities(cities: Collection<City>) {
+        val oldCities = allCities
+        CityDAO.setSelectedCities(prefs, cities)
+
+        // Clear caches affected by this update.
+        mAllCities = null
+        mSelectedCities = null
+        mUnselectedCities = null
+
+        // Broadcast the change to the selected cities for the benefit of widgets.
+        fireCitiesChanged(oldCities, allCities)
+    }
+
+    /**
+     * @return a comparator used to locate index positions
+     */
+    val cityIndexComparator: Comparator<City>
+        get() = when (settingsModel.citySort) {
+            CitySort.NAME -> NameIndexComparator()
+            CitySort.UTC_OFFSET -> UtcOffsetIndexComparator()
+        }
+
+    /**
+     * @return the order in which cities are sorted
+     */
+    val citySort: CitySort
+        get() = settingsModel.citySort
+
+    /**
+     * Adjust the order in which cities are sorted.
+     */
+    fun toggleCitySort() {
+        settingsModel.toggleCitySort()
+
+        // Clear caches affected by this update.
+        mAllCities = null
+        mUnselectedCities = null
+    }
+
+    private val cityMap: Map<String, City>
+        get() {
+            if (mCityMap == null) {
+                mCityMap = CityDAO.getCities(context)
+            }
+
+            return mCityMap!!
+        }
+
+    private val citySortComparator: Comparator<City>
+        get() = when (settingsModel.citySort) {
+            CitySort.NAME -> NameComparator()
+            CitySort.UTC_OFFSET -> UtcOffsetComparator()
+        }
+
+    private fun fireCitiesChanged(oldCities: List<City>, newCities: List<City>) {
+        context.sendBroadcast(Intent(DataModel.ACTION_WORLD_CITIES_CHANGED))
+        for (cityListener in mCityListeners) {
+            cityListener.citiesChanged(oldCities, newCities)
+        }
+    }
+
+    /**
+     * Cached information that is locale-sensitive must be cleared in response to locale changes.
+     */
+    private inner class LocaleChangedReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            mCityMap = null
+            mHomeCity = null
+            mAllCities = null
+            mSelectedCities = null
+            mUnselectedCities = null
+        }
+    }
+
+    /**
+     * This receiver is notified when shared preferences change. Cached information built on
+     * preferences must be cleared.
+     */
+    private inner class PreferenceListener : OnSharedPreferenceChangeListener {
+        override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
+            when (key) {
+                SettingsActivity.KEY_HOME_TZ -> {
+                    mHomeCity = null
+                    val cities = allCities
+                    fireCitiesChanged(cities, cities)
+                }
+                SettingsActivity.KEY_AUTO_HOME_CLOCK -> {
+                    val cities = allCities
+                    fireCitiesChanged(cities, cities)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CustomRingtone.java b/src/com/android/deskclock/data/CustomRingtone.java
deleted file mode 100644
index d243027..0000000
--- a/src/com/android/deskclock/data/CustomRingtone.java
+++ /dev/null
@@ -1,63 +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.net.Uri;
-import androidx.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/CustomRingtone.kt b/src/com/android/deskclock/data/CustomRingtone.kt
new file mode 100644
index 0000000..db4bcfd
--- /dev/null
+++ b/src/com/android/deskclock/data/CustomRingtone.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+/**
+ * A read-only domain object representing a custom ringtone chosen from the file system.
+ */
+class CustomRingtone internal constructor(
+    /** The unique identifier of the custom ringtone.  */
+    val id: Long,
+    /** The uri that allows playback of the ringtone.  */
+    private val mUri: Uri,
+    /** The title describing the file at the given uri; typically the file name.  */
+    val title: String?,
+    /** `true` iff the application has permission to read the content of `mUri uri`.  */
+    private val mHasPermissions: Boolean
+) : Comparable<CustomRingtone> {
+
+    val uri: Uri
+        get() = mUri
+
+    fun hasPermissions(): Boolean = mHasPermissions
+
+    fun setHasPermissions(hasPermissions: Boolean): CustomRingtone =
+            if (mHasPermissions == hasPermissions) {
+                this
+            } else {
+                CustomRingtone(id, mUri, title, hasPermissions)
+            }
+
+    override fun compareTo(other: CustomRingtone): Int {
+        return String.CASE_INSENSITIVE_ORDER.compare(title, other.title)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CustomRingtoneDAO.java b/src/com/android/deskclock/data/CustomRingtoneDAO.java
deleted file mode 100644
index 827acb5..0000000
--- a/src/com/android/deskclock/data/CustomRingtoneDAO.java
+++ /dev/null
@@ -1,107 +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.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/CustomRingtoneDAO.kt b/src/com/android/deskclock/data/CustomRingtoneDAO.kt
new file mode 100644
index 0000000..dc9b11e
--- /dev/null
+++ b/src/com/android/deskclock/data/CustomRingtoneDAO.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+/**
+ * This class encapsulates the transfer of data between [CustomRingtone] domain objects and
+ * their permanent storage in [SharedPreferences].
+ */
+internal object CustomRingtoneDAO {
+    /** Key to a preference that stores the set of all custom ringtone ids.  */
+    private const val RINGTONE_IDS = "ringtone_ids"
+
+    /** Key to a preference that stores the next unused ringtone id.  */
+    private const val NEXT_RINGTONE_ID = "next_ringtone_id"
+
+    /** Prefix for a key to a preference that stores the URI associated with the ringtone id.  */
+    private const val RINGTONE_URI = "ringtone_uri_"
+
+    /** Prefix for a key to a preference that stores the title associated with the ringtone id.  */
+    private const val RINGTONE_TITLE = "ringtone_title_"
+
+    /**
+     * @param uri points to an audio file located on the file system
+     * @param title the title of the audio content at the given `uri`
+     * @return the newly added custom ringtone
+     */
+    fun addCustomRingtone(prefs: SharedPreferences, uri: Uri, title: String?): CustomRingtone {
+        val id: Long = prefs.getLong(NEXT_RINGTONE_ID, 0)
+        val ids = getRingtoneIds(prefs)
+        ids.add(id.toString())
+
+        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 CustomRingtone(id, uri, title, true)
+    }
+
+    /**
+     * @param id identifies the ringtone to be removed
+     */
+    fun removeCustomRingtone(prefs: SharedPreferences, id: Long) {
+        val ids = getRingtoneIds(prefs)
+        ids.remove(id.toString())
+
+        val editor: SharedPreferences.Editor = prefs.edit()
+        editor.apply {
+            remove(RINGTONE_URI + id)
+            remove(RINGTONE_TITLE + id)
+            if (ids.isEmpty()) {
+                remove(RINGTONE_IDS)
+                remove(NEXT_RINGTONE_ID)
+            } else {
+                putStringSet(RINGTONE_IDS, ids)
+            }
+            apply()
+        }
+    }
+
+    /**
+     * @return a list of all known custom ringtones
+     */
+    fun getCustomRingtones(prefs: SharedPreferences): MutableList<CustomRingtone> {
+        val ids: Set<String> = prefs.getStringSet(RINGTONE_IDS, emptySet<String>())!!
+        val ringtones: MutableList<CustomRingtone> = ArrayList(ids.size)
+
+        for (id in ids) {
+            val idLong = id.toLong()
+            val uri: Uri = Uri.parse(prefs.getString(RINGTONE_URI + id, null))
+            val title: String? = prefs.getString(RINGTONE_TITLE + id, null)
+            ringtones.add(CustomRingtone(idLong, uri, title, true))
+        }
+
+        return ringtones
+    }
+
+    private fun getRingtoneIds(prefs: SharedPreferences): MutableSet<String> {
+        return prefs.getStringSet(RINGTONE_IDS, mutableSetOf<String>())!!
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/DataModel.java b/src/com/android/deskclock/data/DataModel.java
deleted file mode 100644
index 1b43232..0000000
--- a/src/com/android/deskclock/data/DataModel.java
+++ /dev/null
@@ -1,1076 +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.data;
-
-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 androidx.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.
- */
-public final class DataModel {
-
-    /** Indicates the display style of clocks. */
-    public enum ClockStyle {ANALOG, DIGITAL}
-
-    /** 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";
-
-    /** The single instance of this data model that exists for the life of the application. */
-    private static final DataModel sDataModel = new DataModel();
-
-    private Handler mHandler;
-
-    private Context mContext;
-
-    /** The model from which settings are fetched. */
-    private SettingsModel mSettingsModel;
-
-    /** The model from which city data are fetched. */
-    private CityModel mCityModel;
-
-    /** The model from which timer data are fetched. */
-    private TimerModel mTimerModel;
-
-    /** The model from which alarm data are fetched. */
-    private AlarmModel mAlarmModel;
-
-    /** 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;
-    }
-
-    private DataModel() {}
-
-    /**
-     * Initializes the data model with the context and shared preferences to be used.
-     */
-    public void init(Context context, SharedPreferences prefs) {
-        if (mContext != context) {
-            mContext = context.getApplicationContext();
-
-            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);
-        }
-    }
-
-    /**
-     * Convenience for {@code run(runnable, 0)}, i.e. waits indefinitely.
-     */
-    public void run(Runnable runnable) {
-        try {
-            run(runnable, 0 /* waitMillis */);
-        } catch (InterruptedException ignored) {
-        }
-    }
-
-    /**
-     * Updates all timers and the stopwatch after the device has shutdown and restarted.
-     */
-    public void updateAfterReboot() {
-        enforceMainLooper();
-        mTimerModel.updateTimersAfterReboot();
-        mStopwatchModel.setStopwatch(getStopwatch().updateAfterReboot());
-    }
-
-    /**
-     * Updates all timers and the stopwatch after the device's time has changed.
-     */
-    public void updateAfterTimeSet() {
-        enforceMainLooper();
-        mTimerModel.updateTimersAfterTimeSet();
-        mStopwatchModel.setStopwatch(getStopwatch().updateAfterTimeSet());
-    }
-
-    /**
-     * Posts a runnable to the main thread and blocks until the runnable executes. Used to access
-     * the data model from the main thread.
-     */
-    public void run(Runnable runnable, long waitMillis) throws InterruptedException {
-        if (Looper.myLooper() == Looper.getMainLooper()) {
-            runnable.run();
-            return;
-        }
-
-        final ExecutedRunnable er = new ExecutedRunnable(runnable);
-        getHandler().post(er);
-
-        // Wait for the data to arrive, if it has not.
-        synchronized (er) {
-            if (!er.isExecuted()) {
-                er.wait(waitMillis);
-            }
-        }
-    }
-
-    /**
-     * @return a handler associated with the main thread
-     */
-    private synchronized Handler getHandler() {
-        if (mHandler == null) {
-            mHandler = new Handler(Looper.getMainLooper());
-        }
-        return mHandler;
-    }
-
-    //
-    // Application
-    //
-
-    /**
-     * @param inForeground {@code true} to indicate the application is open in the foreground
-     */
-    public void setApplicationInForeground(boolean inForeground) {
-        enforceMainLooper();
-
-        if (mNotificationModel.isApplicationInForeground() != inForeground) {
-            mNotificationModel.setApplicationInForeground(inForeground);
-
-            // Refresh all notifications in response to a change in app open state.
-            mTimerModel.updateNotification();
-            mTimerModel.updateMissedNotification();
-            mStopwatchModel.updateNotification();
-            mSilentSettingsModel.updateSilentState();
-        }
-    }
-
-    /**
-     * @return {@code true} when the application is open in the foreground; {@code false} otherwise
-     */
-    public boolean isApplicationInForeground() {
-        enforceMainLooper();
-        return mNotificationModel.isApplicationInForeground();
-    }
-
-    /**
-     * Called when the notifications may be stale or absent from the notification manager and must
-     * be rebuilt. e.g. after upgrading the application
-     */
-    public void updateAllNotifications() {
-        enforceMainLooper();
-        mTimerModel.updateNotification();
-        mTimerModel.updateMissedNotification();
-        mStopwatchModel.updateNotification();
-    }
-
-    //
-    // Cities
-    //
-
-    /**
-     * @return a list of all cities in their display order
-     */
-    public List<City> getAllCities() {
-        enforceMainLooper();
-        return mCityModel.getAllCities();
-    }
-
-    /**
-     * @return a city representing the user's home timezone
-     */
-    public City getHomeCity() {
-        enforceMainLooper();
-        return mCityModel.getHomeCity();
-    }
-
-    /**
-     * @return a list of cities not selected for display
-     */
-    public List<City> getUnselectedCities() {
-        enforceMainLooper();
-        return mCityModel.getUnselectedCities();
-    }
-
-    /**
-     * @return a list of cities selected for display
-     */
-    public List<City> getSelectedCities() {
-        enforceMainLooper();
-        return mCityModel.getSelectedCities();
-    }
-
-    /**
-     * @param cities the new collection of cities selected for display by the user
-     */
-    public void setSelectedCities(Collection<City> cities) {
-        enforceMainLooper();
-        mCityModel.setSelectedCities(cities);
-    }
-
-    /**
-     * @return a comparator used to locate index positions
-     */
-    public Comparator<City> getCityIndexComparator() {
-        enforceMainLooper();
-        return mCityModel.getCityIndexComparator();
-    }
-
-    /**
-     * @return the order in which cities are sorted
-     */
-    public CitySort getCitySort() {
-        enforceMainLooper();
-        return mCityModel.getCitySort();
-    }
-
-    /**
-     * Adjust the order in which cities are sorted.
-     */
-    public void toggleCitySort() {
-        enforceMainLooper();
-        mCityModel.toggleCitySort();
-    }
-
-    /**
-     * @param cityListener listener to be notified when the world city list changes
-     */
-    public void addCityListener(CityListener cityListener) {
-        enforceMainLooper();
-        mCityModel.addCityListener(cityListener);
-    }
-
-    /**
-     * @param cityListener listener that no longer needs to be notified of world city list changes
-     */
-    public void removeCityListener(CityListener cityListener) {
-        enforceMainLooper();
-        mCityModel.removeCityListener(cityListener);
-    }
-
-    //
-    // Timers
-    //
-
-    /**
-     * @param timerListener to be notified when timers are added, updated and removed
-     */
-    public void addTimerListener(TimerListener timerListener) {
-        enforceMainLooper();
-        mTimerModel.addTimerListener(timerListener);
-    }
-
-    /**
-     * @param timerListener to no longer be notified when timers are added, updated and removed
-     */
-    public void removeTimerListener(TimerListener timerListener) {
-        enforceMainLooper();
-        mTimerModel.removeTimerListener(timerListener);
-    }
-
-    /**
-     * @return a list of timers for display
-     */
-    public List<Timer> getTimers() {
-        enforceMainLooper();
-        return mTimerModel.getTimers();
-    }
-
-    /**
-     * @return a list of expired timers for display
-     */
-    public List<Timer> getExpiredTimers() {
-        enforceMainLooper();
-        return mTimerModel.getExpiredTimers();
-    }
-
-    /**
-     * @param timerId identifies the timer to return
-     * @return the timer with the given {@code timerId}
-     */
-    public Timer getTimer(int timerId) {
-        enforceMainLooper();
-        return mTimerModel.getTimer(timerId);
-    }
-
-    /**
-     * @return the timer that last expired and is still expired now; {@code null} if no timers are
-     *      expired
-     */
-    public Timer getMostRecentExpiredTimer() {
-        enforceMainLooper();
-        return mTimerModel.getMostRecentExpiredTimer();
-    }
-
-    /**
-     * @param length the length of the timer in milliseconds
-     * @param label describes the purpose of the timer
-     * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
-     * @return the newly added timer
-     */
-    public Timer addTimer(long length, String label, boolean deleteAfterUse) {
-        enforceMainLooper();
-        return mTimerModel.addTimer(length, label, deleteAfterUse);
-    }
-
-    /**
-     * @param timer the timer to be removed
-     */
-    public void removeTimer(Timer timer) {
-        enforceMainLooper();
-        mTimerModel.removeTimer(timer);
-    }
-
-    /**
-     * @param timer the timer to be started
-     */
-    public void startTimer(Timer timer) {
-        startTimer(null, timer);
-    }
-
-    /**
-     * @param service used to start foreground notifications for expired timers
-     * @param timer the timer to be started
-     */
-    public void startTimer(Service service, Timer timer) {
-        enforceMainLooper();
-        final Timer started = timer.start();
-        mTimerModel.updateTimer(started);
-        if (timer.getRemainingTime() <= 0) {
-            if (service != null) {
-                expireTimer(service, started);
-            } else {
-                mContext.startService(TimerService.createTimerExpiredIntent(mContext, started));
-            }
-        }
-    }
-
-    /**
-     * @param timer the timer to be paused
-     */
-    public void pauseTimer(Timer timer) {
-        enforceMainLooper();
-        mTimerModel.updateTimer(timer.pause());
-    }
-
-    /**
-     * @param service used to start foreground notifications for expired timers
-     * @param timer the timer to be expired
-     */
-    public void expireTimer(Service service, Timer timer) {
-        enforceMainLooper();
-        mTimerModel.expireTimer(service, timer);
-    }
-
-    /**
-     * @param timer the timer to be reset
-     * @return the reset {@code timer}
-     */
-    public Timer resetTimer(Timer timer) {
-        enforceMainLooper();
-        return mTimerModel.resetTimer(timer, false /* allowDelete */, 0 /* eventLabelId */);
-    }
-
-    /**
-     * If the given {@code timer} is expired and marked for deletion after use then this method
-     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
-     * to exist.
-     *
-     * @param timer the timer to be reset
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     * @return the reset {@code timer} or {@code null} if the timer was deleted
-     */
-    public Timer resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) {
-        enforceMainLooper();
-        return mTimerModel.resetTimer(timer, true /* allowDelete */, eventLabelId);
-    }
-
-    /**
-     * Resets all expired timers.
-     *
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     */
-    public void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) {
-        enforceMainLooper();
-        mTimerModel.resetOrDeleteExpiredTimers(eventLabelId);
-    }
-
-    /**
-     * Resets all unexpired timers.
-     *
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     */
-    public void resetUnexpiredTimers(@StringRes int eventLabelId) {
-        enforceMainLooper();
-        mTimerModel.resetUnexpiredTimers(eventLabelId);
-    }
-
-    /**
-     * Resets all missed timers.
-     *
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     */
-    public void resetMissedTimers(@StringRes int eventLabelId) {
-        enforceMainLooper();
-        mTimerModel.resetMissedTimers(eventLabelId);
-    }
-
-    /**
-     * @param timer the timer to which a minute should be added to the remaining time
-     */
-    public void addTimerMinute(Timer timer) {
-        enforceMainLooper();
-        mTimerModel.updateTimer(timer.addMinute());
-    }
-
-    /**
-     * @param timer the timer to which the new {@code label} belongs
-     * @param label the new label to store for the {@code timer}
-     */
-    public void setTimerLabel(Timer timer, String label) {
-        enforceMainLooper();
-        mTimerModel.updateTimer(timer.setLabel(label));
-    }
-
-    /**
-     * @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) {
-        enforceMainLooper();
-        mTimerModel.updateTimer(timer.setLength(length));
-    }
-
-    /**
-     * @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) {
-        enforceMainLooper();
-
-        final Timer updated = timer.setRemainingTime(remainingTime);
-        mTimerModel.updateTimer(updated);
-        if (timer.isRunning() && timer.getRemainingTime() <= 0) {
-            mContext.startService(TimerService.createTimerExpiredIntent(mContext, updated));
-        }
-    }
-
-    /**
-     * Updates the timer notifications to be current.
-     */
-    public void updateTimerNotification() {
-        enforceMainLooper();
-        mTimerModel.updateNotification();
-    }
-
-    /**
-     * @return the uri of the default ringtone to play for all timers when no user selection exists
-     */
-    public Uri getDefaultTimerRingtoneUri() {
-        enforceMainLooper();
-        return mTimerModel.getDefaultTimerRingtoneUri();
-    }
-
-    /**
-     * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
-     */
-    public boolean isTimerRingtoneSilent() {
-        enforceMainLooper();
-        return mTimerModel.isTimerRingtoneSilent();
-    }
-
-    /**
-     * @return the uri of the ringtone to play for all timers
-     */
-    public Uri getTimerRingtoneUri() {
-        enforceMainLooper();
-        return mTimerModel.getTimerRingtoneUri();
-    }
-
-    /**
-     * @param uri the uri of the ringtone to play for all timers
-     */
-    public void setTimerRingtoneUri(Uri uri) {
-        enforceMainLooper();
-        mTimerModel.setTimerRingtoneUri(uri);
-    }
-
-    /**
-     * @return the title of the ringtone that is played for all timers
-     */
-    public String getTimerRingtoneTitle() {
-        enforceMainLooper();
-        return mTimerModel.getTimerRingtoneTitle();
-    }
-
-    /**
-     * @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() {
-        enforceMainLooper();
-        return mTimerModel.getTimerVibrate();
-    }
-
-    /**
-     * @param enabled whether vibrate is enabled for all timers.
-     */
-    public void setTimerVibrate(boolean enabled) {
-        enforceMainLooper();
-        mTimerModel.setTimerVibrate(enabled);
-    }
-
-    //
-    // Alarms
-    //
-
-    /**
-     * @return the uri of the ringtone to which all new alarms default
-     */
-    public Uri getDefaultAlarmRingtoneUri() {
-        enforceMainLooper();
-        return mAlarmModel.getDefaultAlarmRingtoneUri();
-    }
-
-    /**
-     * @param uri the uri of the ringtone to which future new alarms will default
-     */
-    public void setDefaultAlarmRingtoneUri(Uri uri) {
-        enforceMainLooper();
-        mAlarmModel.setDefaultAlarmRingtoneUri(uri);
-    }
-
-    /**
-     * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
-     *      {@code 0} implies no crescendo should be applied
-     */
-    public long getAlarmCrescendoDuration() {
-        enforceMainLooper();
-        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();
-    }
-
-    //
-    // Stopwatch
-    //
-
-    /**
-     * @param stopwatchListener to be notified when stopwatch changes or laps are added
-     */
-    public void addStopwatchListener(StopwatchListener stopwatchListener) {
-        enforceMainLooper();
-        mStopwatchModel.addStopwatchListener(stopwatchListener);
-    }
-
-    /**
-     * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
-     */
-    public void removeStopwatchListener(StopwatchListener stopwatchListener) {
-        enforceMainLooper();
-        mStopwatchModel.removeStopwatchListener(stopwatchListener);
-    }
-
-    /**
-     * @return the current state of the stopwatch
-     */
-    public Stopwatch getStopwatch() {
-        enforceMainLooper();
-        return mStopwatchModel.getStopwatch();
-    }
-
-    /**
-     * @return the stopwatch after being started
-     */
-    public Stopwatch startStopwatch() {
-        enforceMainLooper();
-        return mStopwatchModel.setStopwatch(getStopwatch().start());
-    }
-
-    /**
-     * @return the stopwatch after being paused
-     */
-    public Stopwatch pauseStopwatch() {
-        enforceMainLooper();
-        return mStopwatchModel.setStopwatch(getStopwatch().pause());
-    }
-
-    /**
-     * @return the stopwatch after being reset
-     */
-    public Stopwatch resetStopwatch() {
-        enforceMainLooper();
-        return mStopwatchModel.setStopwatch(getStopwatch().reset());
-    }
-
-    /**
-     * @return the laps recorded for this stopwatch
-     */
-    public List<Lap> getLaps() {
-        enforceMainLooper();
-        return mStopwatchModel.getLaps();
-    }
-
-    /**
-     * @return a newly recorded lap completed now; {@code null} if no more laps can be added
-     */
-    public Lap addLap() {
-        enforceMainLooper();
-        return mStopwatchModel.addLap();
-    }
-
-    /**
-     * @return {@code true} iff more laps can be recorded
-     */
-    public boolean canAddMoreLaps() {
-        enforceMainLooper();
-        return mStopwatchModel.canAddMoreLaps();
-    }
-
-    /**
-     * @return the longest lap time of all recorded laps and the current lap
-     */
-    public long getLongestLapTime() {
-        enforceMainLooper();
-        return mStopwatchModel.getLongestLapTime();
-    }
-
-    /**
-     * @param time a point in time after the end of the last lap
-     * @return the elapsed time between the given {@code time} and the end of the previous lap
-     */
-    public long getCurrentLapTime(long time) {
-        enforceMainLooper();
-        return mStopwatchModel.getCurrentLapTime(time);
-    }
-
-    //
-    // 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
-    //
-
-    /**
-     * @param widgetClass indicates the type of widget being counted
-     * @param count the number of widgets of the given type
-     * @param eventCategoryId identifies the category of event to send
-     */
-    public void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
-        enforceMainLooper();
-        mWidgetModel.updateWidgetCount(widgetClass, count, eventCategoryId);
-    }
-
-    //
-    // Settings
-    //
-
-    /**
-     * @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() {
-        enforceMainLooper();
-        return mSettingsModel.getClockStyle();
-    }
-
-    /**
-     * @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() {
-        enforceMainLooper();
-        return mSettingsModel.getScreensaverClockStyle();
-    }
-
-    /**
-     * @return {@code true} if the screen saver should be dimmed for lower contrast at night
-     */
-    public boolean getScreensaverNightModeOn() {
-        enforceMainLooper();
-        return mSettingsModel.getScreensaverNightModeOn();
-    }
-
-    /**
-     * @return {@code true} if the users wants to automatically show a clock for their home timezone
-     *      when they have travelled outside of that timezone
-     */
-    public boolean getShowHomeClock() {
-        enforceMainLooper();
-        return mSettingsModel.getShowHomeClock();
-    }
-
-    /**
-     * @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 {
-
-        private final Runnable mDelegate;
-        private boolean mExecuted;
-
-        private ExecutedRunnable(Runnable delegate) {
-            this.mDelegate = delegate;
-        }
-
-        @Override
-        public void run() {
-            mDelegate.run();
-
-            synchronized (this) {
-                mExecuted = true;
-                notifyAll();
-            }
-        }
-
-        private boolean isExecuted() {
-            return mExecuted;
-        }
-    }
-}
diff --git a/src/com/android/deskclock/data/DataModel.kt b/src/com/android/deskclock/data/DataModel.kt
new file mode 100644
index 0000000..72e4189
--- /dev/null
+++ b/src/com/android/deskclock/data/DataModel.kt
@@ -0,0 +1,1075 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Service
+import android.content.Context
+import android.content.Context.AUDIO_SERVICE
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.SharedPreferences
+import android.media.AudioManager
+import android.media.AudioManager.FLAG_SHOW_UI
+import android.media.AudioManager.STREAM_ALARM
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+import android.provider.Settings.ACTION_SOUND_SETTINGS
+import android.view.View
+import androidx.annotation.Keep
+import androidx.annotation.StringRes
+
+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 kotlin.Comparator
+import kotlin.math.roundToInt
+
+/**
+ * All application-wide data is accessible through this singleton.
+ */
+class DataModel private constructor() {
+
+    /** Indicates the display style of clocks.  */
+    enum class ClockStyle {
+        ANALOG, DIGITAL
+    }
+
+    /** Indicates the preferred sort order of cities.  */
+    enum class CitySort {
+        NAME, UTC_OFFSET
+    }
+
+    /** Indicates the preferred behavior of hardware volume buttons when firing alarms.  */
+    enum class AlarmVolumeButtonBehavior {
+        NOTHING, SNOOZE, DISMISS
+    }
+
+    /** Indicates the reason alarms may not fire or may fire silently.  */
+    enum class SilentSetting(
+        @field:StringRes @get:StringRes val labelResId: Int,
+        @field:StringRes @get:StringRes val actionResId: Int,
+        private val mActionEnabled: Predicate<Context>,
+        private val mActionListener: View.OnClickListener?
+    ) {
+
+        DO_NOT_DISTURB(R.string.alarms_blocked_by_dnd,
+                0,
+                Predicate.FALSE as Predicate<Context>,
+                mActionListener = null),
+        MUTED_VOLUME(R.string.alarm_volume_muted,
+                R.string.unmute_alarm_volume,
+                Predicate.TRUE as Predicate<Context>,
+                UnmuteAlarmVolumeListener()),
+        SILENT_RINGTONE(R.string.silent_default_alarm_ringtone,
+                R.string.change_setting_action,
+                ChangeSoundActionPredicate(),
+                ChangeSoundSettingsListener()),
+        BLOCKED_NOTIFICATIONS(R.string.app_notifications_blocked,
+                R.string.change_setting_action,
+                Predicate.TRUE as Predicate<Context>,
+                ChangeAppNotificationSettingsListener());
+
+        val actionListener: View.OnClickListener?
+            get() = mActionListener
+
+        fun isActionEnabled(context: Context): Boolean {
+            return labelResId != 0 && mActionEnabled.apply(context)
+        }
+
+        private class UnmuteAlarmVolumeListener : View.OnClickListener {
+            override fun onClick(v: View) {
+                // 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.
+                val context: Context = v.context
+                val am: AudioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager
+                val index = (am.getStreamMaxVolume(STREAM_ALARM) * 11f / 16f).roundToInt()
+                am.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI)
+            }
+        }
+
+        private class ChangeSoundSettingsListener : View.OnClickListener {
+            override fun onClick(v: View) {
+                val context: Context = v.context
+                context.startActivity(Intent(ACTION_SOUND_SETTINGS)
+                        .addFlags(FLAG_ACTIVITY_NEW_TASK))
+            }
+        }
+
+        private class ChangeSoundActionPredicate : Predicate<Context> {
+            override fun apply(context: Context): Boolean {
+                val intent = Intent(ACTION_SOUND_SETTINGS)
+                return intent.resolveActivity(context.packageManager) != null
+            }
+        }
+
+        private class ChangeAppNotificationSettingsListener : View.OnClickListener {
+            override fun onClick(v: View) {
+                val context: Context = v.context
+                if (Utils.isLOrLater) {
+                    try {
+                        // Attempt to open the notification settings for this app.
+                        context.startActivity(
+                                Intent("android.settings.APP_NOTIFICATION_SETTINGS")
+                                        .putExtra("app_package", context.packageName)
+                                        .putExtra("app_uid", context.applicationInfo.uid)
+                                        .addFlags(FLAG_ACTIVITY_NEW_TASK))
+                        return
+                    } catch (ignored: Exception) {
+                        // best attempt only; recovery code below
+                    }
+                }
+
+                // Fall back to opening the app settings page.
+                context.startActivity(Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+                        .setData(Uri.fromParts("package", context.packageName, null))
+                        .addFlags(FLAG_ACTIVITY_NEW_TASK))
+            }
+        }
+    }
+
+    private var mHandler: Handler? = null
+    private var mContext: Context? = null
+
+    /** The model from which settings are fetched.  */
+    private var mSettingsModel: SettingsModel? = null
+
+    /** The model from which city data are fetched.  */
+    private var mCityModel: CityModel? = null
+
+    /** The model from which timer data are fetched.  */
+    private var mTimerModel: TimerModel? = null
+
+    /** The model from which alarm data are fetched.  */
+    private var mAlarmModel: AlarmModel? = null
+
+    /** The model from which widget data are fetched.  */
+    private var mWidgetModel: WidgetModel? = null
+
+    /** The model from which data about settings that silence alarms are fetched.  */
+    private var mSilentSettingsModel: SilentSettingsModel? = null
+
+    /** The model from which stopwatch data are fetched.  */
+    private var mStopwatchModel: StopwatchModel? = null
+
+    /** The model from which notification data are fetched.  */
+    private var mNotificationModel: NotificationModel? = null
+
+    /** The model from which time data are fetched.  */
+    private var mTimeModel: TimeModel? = null
+
+    /** The model from which ringtone data are fetched.  */
+    private var mRingtoneModel: RingtoneModel? = null
+
+    /**
+     * Initializes the data model with the context and shared preferences to be used.
+     */
+    fun init(context: Context, prefs: SharedPreferences) {
+        if (mContext !== context) {
+            mContext = context.applicationContext
+            mTimeModel = TimeModel(mContext!!)
+            mWidgetModel = WidgetModel(prefs)
+            mNotificationModel = NotificationModel()
+            mRingtoneModel = RingtoneModel(mContext!!, prefs)
+            mSettingsModel = SettingsModel(mContext!!, prefs, mTimeModel!!)
+            mCityModel = CityModel(mContext!!, prefs, mSettingsModel!!)
+            mAlarmModel = AlarmModel(mContext!!, mSettingsModel!!)
+            mSilentSettingsModel = SilentSettingsModel(mContext!!, mNotificationModel!!)
+            mStopwatchModel = StopwatchModel(mContext!!, prefs, mNotificationModel!!)
+            mTimerModel = TimerModel(mContext!!, prefs, mSettingsModel!!, mRingtoneModel!!,
+                    mNotificationModel!!)
+        }
+    }
+
+    /**
+     * Convenience for `run(runnable, 0)`, i.e. waits indefinitely.
+     */
+    fun run(runnable: Runnable) {
+        try {
+            run(runnable, 0 /* waitMillis */)
+        } catch (ignored: InterruptedException) {
+        }
+    }
+
+    /**
+     * Updates all timers and the stopwatch after the device has shutdown and restarted.
+     */
+    fun updateAfterReboot() {
+        Utils.enforceMainLooper()
+        mTimerModel!!.updateTimersAfterReboot()
+        mStopwatchModel!!.setStopwatch(stopwatch.updateAfterReboot())
+    }
+
+    /**
+     * Updates all timers and the stopwatch after the device's time has changed.
+     */
+    fun updateAfterTimeSet() {
+        Utils.enforceMainLooper()
+        mTimerModel!!.updateTimersAfterTimeSet()
+        mStopwatchModel!!.setStopwatch(stopwatch.updateAfterTimeSet())
+    }
+
+    /**
+     * Posts a runnable to the main thread and blocks until the runnable executes. Used to access
+     * the data model from the main thread.
+     */
+    @Throws(InterruptedException::class)
+    fun run(runnable: Runnable, waitMillis: Long) {
+        if (Looper.myLooper() === Looper.getMainLooper()) {
+            runnable.run()
+            return
+        }
+
+        val er = ExecutedRunnable(runnable)
+        handler.post(er)
+
+        // Wait for the data to arrive, if it has not.
+        synchronized(er) {
+            if (!er.isExecuted) {
+                er.wait(waitMillis)
+            }
+        }
+    }
+
+    /**
+     * @return a handler associated with the main thread
+     */
+    @get:Synchronized
+    private val handler: Handler
+        get() {
+            if (mHandler == null) {
+                mHandler = Handler(Looper.getMainLooper())
+            }
+            return mHandler!!
+        }
+
+    //
+    // Application
+    //
+
+    var isApplicationInForeground: Boolean
+        /**
+         * @return `true` when the application is open in the foreground; `false` otherwise
+         */
+        get() {
+            Utils.enforceMainLooper()
+            return mNotificationModel!!.isApplicationInForeground
+        }
+        /**
+         * @param inForeground `true` to indicate the application is open in the foreground
+         */
+        set(inForeground) {
+            Utils.enforceMainLooper()
+            if (mNotificationModel!!.isApplicationInForeground != inForeground) {
+                mNotificationModel!!.isApplicationInForeground = inForeground
+
+                // Refresh all notifications in response to a change in app open state.
+                mTimerModel!!.updateNotification()
+                mTimerModel!!.updateMissedNotification()
+                mStopwatchModel!!.updateNotification()
+                mSilentSettingsModel!!.updateSilentState()
+            }
+        }
+
+    /**
+     * Called when the notifications may be stale or absent from the notification manager and must
+     * be rebuilt. e.g. after upgrading the application
+     */
+    fun updateAllNotifications() {
+        Utils.enforceMainLooper()
+        mTimerModel!!.updateNotification()
+        mTimerModel!!.updateMissedNotification()
+        mStopwatchModel!!.updateNotification()
+    }
+
+    //
+    // Cities
+    //
+
+    /**
+     * @return a list of all cities in their display order
+     */
+    val allCities: List<City>
+        get() {
+            Utils.enforceMainLooper()
+            return mCityModel!!.allCities
+        }
+
+    /**
+     * @return a city representing the user's home timezone
+     */
+    val homeCity: City
+        get() {
+            Utils.enforceMainLooper()
+            return mCityModel!!.homeCity
+        }
+
+    /**
+     * @return a list of cities not selected for display
+     */
+    val unselectedCities: List<City>
+        get() {
+            Utils.enforceMainLooper()
+            return mCityModel!!.unselectedCities
+        }
+
+    var selectedCities: Collection<City>
+        /**
+         * @return a list of cities selected for display
+         */
+        get() {
+            Utils.enforceMainLooper()
+            return mCityModel!!.selectedCities
+        }
+        /**
+         * @param cities the new collection of cities selected for display by the user
+         */
+        set(cities) {
+            Utils.enforceMainLooper()
+            mCityModel?.setSelectedCities(cities)
+        }
+
+    /**
+     * @return a comparator used to locate index positions
+     */
+    val cityIndexComparator: Comparator<City>
+        get() {
+            Utils.enforceMainLooper()
+            return mCityModel!!.cityIndexComparator
+        }
+
+    /**
+     * @return the order in which cities are sorted
+     */
+    val citySort: CitySort
+        get() {
+            Utils.enforceMainLooper()
+            return mCityModel!!.citySort
+        }
+
+    /**
+     * Adjust the order in which cities are sorted.
+     */
+    fun toggleCitySort() {
+        Utils.enforceMainLooper()
+        mCityModel?.toggleCitySort()
+    }
+
+    /**
+     * @param cityListener listener to be notified when the world city list changes
+     */
+    fun addCityListener(cityListener: CityListener) {
+        Utils.enforceMainLooper()
+        mCityModel?.addCityListener(cityListener)
+    }
+
+    /**
+     * @param cityListener listener that no longer needs to be notified of world city list changes
+     */
+    fun removeCityListener(cityListener: CityListener) {
+        Utils.enforceMainLooper()
+        mCityModel?.removeCityListener(cityListener)
+    }
+
+    //
+    // Timers
+    //
+
+    /**
+     * @param timerListener to be notified when timers are added, updated and removed
+     */
+    fun addTimerListener(timerListener: TimerListener) {
+        Utils.enforceMainLooper()
+        mTimerModel?.addTimerListener(timerListener)
+    }
+
+    /**
+     * @param timerListener to no longer be notified when timers are added, updated and removed
+     */
+    fun removeTimerListener(timerListener: TimerListener) {
+        Utils.enforceMainLooper()
+        mTimerModel?.removeTimerListener(timerListener)
+    }
+
+    /**
+     * @return a list of timers for display
+     */
+    val timers: List<Timer>
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.timers
+        }
+
+    /**
+     * @return a list of expired timers for display
+     */
+    val expiredTimers: List<Timer>
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.expiredTimers
+        }
+
+    /**
+     * @param timerId identifies the timer to return
+     * @return the timer with the given `timerId`
+     */
+    fun getTimer(timerId: Int): Timer? {
+        Utils.enforceMainLooper()
+        return mTimerModel?.getTimer(timerId)
+    }
+
+    /**
+     * @return the timer that last expired and is still expired now; `null` if no timers are
+     * expired
+     */
+    val mostRecentExpiredTimer: Timer?
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel?.mostRecentExpiredTimer
+        }
+
+    /**
+     * @param length the length of the timer in milliseconds
+     * @param label describes the purpose of the timer
+     * @param deleteAfterUse `true` indicates the timer should be deleted when it is reset
+     * @return the newly added timer
+     */
+    fun addTimer(length: Long, label: String?, deleteAfterUse: Boolean): Timer {
+        Utils.enforceMainLooper()
+        return mTimerModel!!.addTimer(length, label, deleteAfterUse)
+    }
+
+    /**
+     * @param timer the timer to be removed
+     */
+    fun removeTimer(timer: Timer) {
+        Utils.enforceMainLooper()
+        mTimerModel?.removeTimer(timer)
+    }
+
+    /**
+     * @param timer the timer to be started
+     */
+    fun startTimer(timer: Timer) {
+        startTimer(null, timer)
+    }
+
+    /**
+     * @param service used to start foreground notifications for expired timers
+     * @param timer the timer to be started
+     */
+    fun startTimer(service: Service?, timer: Timer) {
+        Utils.enforceMainLooper()
+        val started = timer.start()
+        mTimerModel?.updateTimer(started)
+        if (timer.remainingTime <= 0) {
+            if (service != null) {
+                expireTimer(service, started)
+            } else {
+                mContext!!.startService(TimerService.createTimerExpiredIntent(mContext!!, started))
+            }
+        }
+    }
+
+    /**
+     * @param timer the timer to be paused
+     */
+    fun pauseTimer(timer: Timer) {
+        Utils.enforceMainLooper()
+        mTimerModel?.updateTimer(timer.pause())
+    }
+
+    /**
+     * @param service used to start foreground notifications for expired timers
+     * @param timer the timer to be expired
+     */
+    fun expireTimer(service: Service?, timer: Timer) {
+        Utils.enforceMainLooper()
+        mTimerModel?.expireTimer(service, timer)
+    }
+
+    /**
+     * @param timer the timer to be reset
+     * @return the reset `timer`
+     */
+    @Keep
+    fun resetTimer(timer: Timer): Timer? {
+        Utils.enforceMainLooper()
+        return mTimerModel?.resetTimer(timer, false /* allowDelete */, 0 /* eventLabelId */)
+    }
+
+    /**
+     * If the given `timer` is expired and marked for deletion after use then this method
+     * removes the timer. The timer is otherwise transitioned to the reset state and continues
+     * to exist.
+     *
+     * @param timer the timer to be reset
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     * @return the reset `timer` or `null` if the timer was deleted
+     */
+    fun resetOrDeleteTimer(timer: Timer, @StringRes eventLabelId: Int): Timer? {
+        Utils.enforceMainLooper()
+        return mTimerModel?.resetTimer(timer, true /* allowDelete */, eventLabelId)
+    }
+
+    /**
+     * Resets all expired timers.
+     *
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     */
+    fun resetOrDeleteExpiredTimers(@StringRes eventLabelId: Int) {
+        Utils.enforceMainLooper()
+        mTimerModel?.resetOrDeleteExpiredTimers(eventLabelId)
+    }
+
+    /**
+     * Resets all unexpired timers.
+     *
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     */
+    fun resetUnexpiredTimers(@StringRes eventLabelId: Int) {
+        Utils.enforceMainLooper()
+        mTimerModel?.resetUnexpiredTimers(eventLabelId)
+    }
+
+    /**
+     * Resets all missed timers.
+     *
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     */
+    fun resetMissedTimers(@StringRes eventLabelId: Int) {
+        Utils.enforceMainLooper()
+        mTimerModel?.resetMissedTimers(eventLabelId)
+    }
+
+    /**
+     * @param timer the timer to which a minute should be added to the remaining time
+     */
+    fun addTimerMinute(timer: Timer) {
+        Utils.enforceMainLooper()
+        mTimerModel?.updateTimer(timer.addMinute())
+    }
+
+    /**
+     * @param timer the timer to which the new `label` belongs
+     * @param label the new label to store for the `timer`
+     */
+    fun setTimerLabel(timer: Timer, label: String?) {
+        Utils.enforceMainLooper()
+        mTimerModel?.updateTimer(timer.setLabel(label))
+    }
+
+    /**
+     * @param timer the timer whose `length` to change
+     * @param length the new length of the timer in milliseconds
+     */
+    fun setTimerLength(timer: Timer, length: Long) {
+        Utils.enforceMainLooper()
+        mTimerModel?.updateTimer(timer.setLength(length))
+    }
+
+    /**
+     * @param timer the timer whose `remainingTime` to change
+     * @param remainingTime the new remaining time of the timer in milliseconds
+     */
+    fun setRemainingTime(timer: Timer, remainingTime: Long) {
+        Utils.enforceMainLooper()
+
+        val updated = timer.setRemainingTime(remainingTime)
+        mTimerModel?.updateTimer(updated)
+        if (timer.isRunning && timer.remainingTime <= 0) {
+            mContext?.startService(TimerService.createTimerExpiredIntent(mContext!!, updated))
+        }
+    }
+
+    /**
+     * Updates the timer notifications to be current.
+     */
+    fun updateTimerNotification() {
+        Utils.enforceMainLooper()
+        mTimerModel?.updateNotification()
+    }
+
+    /**
+     * @return the uri of the default ringtone to play for all timers when no user selection exists
+     */
+    val defaultTimerRingtoneUri: Uri
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.defaultTimerRingtoneUri
+        }
+
+    /**
+     * @return `true` iff the ringtone to play for all timers is the silent ringtone
+     */
+    val isTimerRingtoneSilent: Boolean
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.isTimerRingtoneSilent
+        }
+
+    var timerRingtoneUri: Uri
+        /**
+         * @return the uri of the ringtone to play for all timers
+         */
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.timerRingtoneUri
+        }
+        /**
+         * @param uri the uri of the ringtone to play for all timers
+         */
+        set(uri) {
+            Utils.enforceMainLooper()
+            mTimerModel!!.timerRingtoneUri = uri
+        }
+
+    /**
+     * @return the title of the ringtone that is played for all timers
+     */
+    val timerRingtoneTitle: String
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.timerRingtoneTitle
+        }
+
+    /**
+     * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+     * `0` implies no crescendo should be applied
+     */
+    val timerCrescendoDuration: Long
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.timerCrescendoDuration
+        }
+
+    var timerVibrate: Boolean
+        /**
+         * @return whether vibrate is enabled for all timers.
+         */
+        get() {
+            Utils.enforceMainLooper()
+            return mTimerModel!!.timerVibrate
+        }
+        /**
+         * @param enabled whether vibrate is enabled for all timers.
+         */
+        set(enabled) {
+            Utils.enforceMainLooper()
+            mTimerModel!!.timerVibrate = enabled
+        }
+
+    //
+    // Alarms
+    //
+
+    var defaultAlarmRingtoneUri: Uri
+        /**
+         * @return the uri of the ringtone to which all new alarms default
+         */
+        get() {
+            Utils.enforceMainLooper()
+            return mAlarmModel!!.defaultAlarmRingtoneUri
+        }
+        /**
+         * @param uri the uri of the ringtone to which future new alarms will default
+         */
+        set(uri) {
+            Utils.enforceMainLooper()
+            mAlarmModel!!.defaultAlarmRingtoneUri = uri
+        }
+
+    /**
+     * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
+     * `0` implies no crescendo should be applied
+     */
+    val alarmCrescendoDuration: Long
+        get() {
+            Utils.enforceMainLooper()
+            return mAlarmModel!!.alarmCrescendoDuration
+        }
+
+    /**
+     * @return the behavior to execute when volume buttons are pressed while firing an alarm
+     */
+    val alarmVolumeButtonBehavior: AlarmVolumeButtonBehavior
+        get() {
+            Utils.enforceMainLooper()
+            return mAlarmModel!!.alarmVolumeButtonBehavior
+        }
+
+    /**
+     * @return the number of minutes an alarm may ring before it has timed out and becomes missed
+     */
+    val alarmTimeout: Int
+        get() = mAlarmModel!!.alarmTimeout
+
+    /**
+     * @return the number of minutes an alarm will remain snoozed before it rings again
+     */
+    val snoozeLength: Int
+        get() = mAlarmModel!!.snoozeLength
+
+    //
+    // Stopwatch
+    //
+
+    /**
+     * @param stopwatchListener to be notified when stopwatch changes or laps are added
+     */
+    fun addStopwatchListener(stopwatchListener: StopwatchListener) {
+        Utils.enforceMainLooper()
+        mStopwatchModel?.addStopwatchListener(stopwatchListener)
+    }
+
+    /**
+     * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
+     */
+    fun removeStopwatchListener(stopwatchListener: StopwatchListener) {
+        Utils.enforceMainLooper()
+        mStopwatchModel?.removeStopwatchListener(stopwatchListener)
+    }
+
+    /**
+     * @return the current state of the stopwatch
+     */
+    val stopwatch: Stopwatch
+        get() {
+            Utils.enforceMainLooper()
+            return mStopwatchModel!!.stopwatch
+        }
+
+    /**
+     * @return the stopwatch after being started
+     */
+    fun startStopwatch(): Stopwatch {
+        Utils.enforceMainLooper()
+        return mStopwatchModel!!.setStopwatch(stopwatch.start())
+    }
+
+    /**
+     * @return the stopwatch after being paused
+     */
+    fun pauseStopwatch(): Stopwatch {
+        Utils.enforceMainLooper()
+        return mStopwatchModel!!.setStopwatch(stopwatch.pause())
+    }
+
+    /**
+     * @return the stopwatch after being reset
+     */
+    fun resetStopwatch(): Stopwatch {
+        Utils.enforceMainLooper()
+        return mStopwatchModel!!.setStopwatch(stopwatch.reset())
+    }
+
+    /**
+     * @return the laps recorded for this stopwatch
+     */
+    val laps: List<Lap>
+        get() {
+            Utils.enforceMainLooper()
+            return mStopwatchModel!!.laps
+        }
+
+    /**
+     * @return a newly recorded lap completed now; `null` if no more laps can be added
+     */
+    fun addLap(): Lap? {
+        Utils.enforceMainLooper()
+        return mStopwatchModel!!.addLap()
+    }
+
+    /**
+     * @return `true` iff more laps can be recorded
+     */
+    fun canAddMoreLaps(): Boolean {
+        Utils.enforceMainLooper()
+        return mStopwatchModel!!.canAddMoreLaps()
+    }
+
+    /**
+     * @return the longest lap time of all recorded laps and the current lap
+     */
+    val longestLapTime: Long
+        get() {
+            Utils.enforceMainLooper()
+            return mStopwatchModel!!.longestLapTime
+        }
+
+    /**
+     * @param time a point in time after the end of the last lap
+     * @return the elapsed time between the given `time` and the end of the previous lap
+     */
+    fun getCurrentLapTime(time: Long): Long {
+        Utils.enforceMainLooper()
+        return mStopwatchModel!!.getCurrentLapTime(time)
+    }
+
+    //
+    // Time
+    // (Time settings/values are accessible from any Thread so no Thread-enforcement exists.)
+    //
+
+    /**
+     * @return the current time in milliseconds
+     */
+    fun currentTimeMillis(): Long {
+        return mTimeModel!!.currentTimeMillis()
+    }
+
+    /**
+     * @return milliseconds since boot, including time spent in sleep
+     */
+    fun elapsedRealtime(): Long {
+        return mTimeModel!!.elapsedRealtime()
+    }
+
+    /**
+     * @return `true` if 24 hour time format is selected; `false` otherwise
+     */
+    fun is24HourFormat(): Boolean {
+        return mTimeModel!!.is24HourFormat()
+    }
+
+    /**
+     * @return a new calendar object initialized to the [.currentTimeMillis]
+     */
+    val calendar: Calendar
+        get() = mTimeModel!!.calendar
+
+    //
+    // Ringtones
+    //
+
+    /**
+     * Ringtone titles are cached because loading them is expensive. This method
+     * **must** 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.
+     */
+    fun loadRingtoneTitles() {
+        Utils.enforceNotMainLooper()
+        mRingtoneModel?.loadRingtoneTitles()
+    }
+
+    /**
+     * Recheck the permission to read each custom ringtone.
+     */
+    fun loadRingtonePermissions() {
+        Utils.enforceNotMainLooper()
+        mRingtoneModel?.loadRingtonePermissions()
+    }
+
+    /**
+     * @param uri the uri of a ringtone
+     * @return the title of the ringtone with the `uri`; `null` if it cannot be fetched
+     */
+    fun getRingtoneTitle(uri: Uri): String? {
+        Utils.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 `uri`
+     * @return the ringtone instance created for the audio file
+     */
+    fun addCustomRingtone(uri: Uri, title: String?): CustomRingtone? {
+        Utils.enforceMainLooper()
+        return mRingtoneModel?.addCustomRingtone(uri, title)
+    }
+
+    /**
+     * @param uri identifies the ringtone to remove
+     */
+    fun removeCustomRingtone(uri: Uri) {
+        Utils.enforceMainLooper()
+        mRingtoneModel?.removeCustomRingtone(uri)
+    }
+
+    /**
+     * @return all available custom ringtones
+     */
+    val customRingtones: List<CustomRingtone>
+        get() {
+            Utils.enforceMainLooper()
+            return mRingtoneModel!!.customRingtones
+        }
+
+    //
+    // Widgets
+    //
+
+    /**
+     * @param widgetClass indicates the type of widget being counted
+     * @param count the number of widgets of the given type
+     * @param eventCategoryId identifies the category of event to send
+     */
+    fun updateWidgetCount(widgetClass: Class<*>?, count: Int, @StringRes eventCategoryId: Int) {
+        Utils.enforceMainLooper()
+        mWidgetModel!!.updateWidgetCount(widgetClass!!, count, eventCategoryId)
+    }
+
+    //
+    // Settings
+    //
+
+    /**
+     * @param silentSettingsListener to be notified when alarm-silencing settings change
+     */
+    fun addSilentSettingsListener(silentSettingsListener: OnSilentSettingsListener) {
+        Utils.enforceMainLooper()
+        mSilentSettingsModel?.addSilentSettingsListener(silentSettingsListener)
+    }
+
+    /**
+     * @param silentSettingsListener to no longer be notified when alarm-silencing settings change
+     */
+    fun removeSilentSettingsListener(silentSettingsListener: OnSilentSettingsListener) {
+        Utils.enforceMainLooper()
+        mSilentSettingsModel?.removeSilentSettingsListener(silentSettingsListener)
+    }
+
+    /**
+     * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
+     */
+    val globalIntentId: Int
+        get() = mSettingsModel!!.globalIntentId
+
+    /**
+     * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
+     */
+    fun updateGlobalIntentId() {
+        Utils.enforceMainLooper()
+        mSettingsModel!!.updateGlobalIntentId()
+    }
+
+    /**
+     * @return the style of clock to display in the clock application
+     */
+    val clockStyle: ClockStyle
+        get() {
+            Utils.enforceMainLooper()
+            return mSettingsModel!!.clockStyle
+        }
+
+    var displayClockSeconds: Boolean
+        /**
+         * @return the style of clock to display in the clock application
+         */
+        get() {
+            Utils.enforceMainLooper()
+            return mSettingsModel!!.displayClockSeconds
+        }
+        /**
+         * @param displaySeconds whether or not to display seconds for main clock
+         */
+        set(displaySeconds) {
+            Utils.enforceMainLooper()
+            mSettingsModel!!.displayClockSeconds = displaySeconds
+        }
+
+    /**
+     * @return the style of clock to display in the clock screensaver
+     */
+    val screensaverClockStyle: ClockStyle
+        get() {
+            Utils.enforceMainLooper()
+            return mSettingsModel!!.screensaverClockStyle
+        }
+
+    /**
+     * @return `true` if the screen saver should be dimmed for lower contrast at night
+     */
+    val screensaverNightModeOn: Boolean
+        get() {
+            Utils.enforceMainLooper()
+            return mSettingsModel!!.screensaverNightModeOn
+        }
+
+    /**
+     * @return `true` if the users wants to automatically show a clock for their home timezone
+     * when they have travelled outside of that timezone
+     */
+    val showHomeClock: Boolean
+        get() {
+            Utils.enforceMainLooper()
+            return mSettingsModel!!.showHomeClock
+        }
+
+    /**
+     * @return the display order of the weekdays, which can start with [Calendar.SATURDAY],
+     * [Calendar.SUNDAY] or [Calendar.MONDAY]
+     */
+    val weekdayOrder: Weekdays.Order
+        get() {
+            Utils.enforceMainLooper()
+            return mSettingsModel!!.weekdayOrder
+        }
+
+    var isRestoreBackupFinished: Boolean
+        /**
+         * @return `true` if the restore process (of backup and restore) has completed
+         */
+        get() = mSettingsModel!!.isRestoreBackupFinished
+        /**
+         * @param finished `true` means the restore process (of backup and restore) has completed
+         */
+        set(finished) {
+            mSettingsModel!!.isRestoreBackupFinished = finished
+        }
+
+    /**
+     * @return a description of the time zones available for selection
+     */
+    val timeZones: TimeZones
+        get() {
+            Utils.enforceMainLooper()
+            return mSettingsModel!!.timeZones
+        }
+
+    /**
+     * Used to execute a delegate runnable and track its completion.
+     */
+    private class ExecutedRunnable(private val mDelegate: Runnable) : Runnable, java.lang.Object() {
+        var isExecuted = false
+        override fun run() {
+            mDelegate.run()
+            synchronized(this) {
+                isExecuted = true
+                notifyAll()
+            }
+        }
+    }
+
+    companion object {
+        const val ACTION_WORLD_CITIES_CHANGED = "com.android.deskclock.WORLD_CITIES_CHANGED"
+
+        /** The single instance of this data model that exists for the life of the application.  */
+        val sDataModel = DataModel()
+
+        @get:JvmStatic
+        @get:Keep
+        val dataModel
+            get() = sDataModel
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Lap.java b/src/com/android/deskclock/data/Lap.java
deleted file mode 100644
index 1c42fd8..0000000
--- a/src/com/android/deskclock/data/Lap.java
+++ /dev/null
@@ -1,42 +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.data;
-
-/**
- * A read-only domain object representing a stopwatch lap.
- */
-public final class Lap {
-
-    /** The 1-based position of the lap. */
-    private final int mLapNumber;
-
-    /** Elapsed time in ms since the lap was last started. */
-    private final long mLapTime;
-
-    /** Elapsed time in ms accumulated for all laps up to and including this one. */
-    private final long mAccumulatedTime;
-
-    Lap(int lapNumber, long lapTime, long accumulatedTime) {
-        mLapNumber = lapNumber;
-        mLapTime = lapTime;
-        mAccumulatedTime = accumulatedTime;
-    }
-
-    public int getLapNumber() { return mLapNumber; }
-    public long getLapTime() { return mLapTime; }
-    public long getAccumulatedTime() { return mAccumulatedTime; }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Lap.kt b/src/com/android/deskclock/data/Lap.kt
new file mode 100644
index 0000000..bb109ed
--- /dev/null
+++ b/src/com/android/deskclock/data/Lap.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+/**
+ * A read-only domain object representing a stopwatch lap.
+ */
+data class Lap(
+    /** The 1-based position of the lap.  */
+    val lapNumber: Int,
+    /** Elapsed time in ms since the lap was last started.  */
+    val lapTime: Long,
+    /** Elapsed time in ms accumulated for all laps up to and including this one.  */
+    val accumulatedTime: Long
+)
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/NotificationModel.java b/src/com/android/deskclock/data/NotificationModel.java
deleted file mode 100644
index b0b00f3..0000000
--- a/src/com/android/deskclock/data/NotificationModel.java
+++ /dev/null
@@ -1,115 +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.data;
-
-/**
- * Data that must be coordinated across all notifications is accessed via this model.
- */
-final class NotificationModel {
-
-    private boolean mApplicationInForeground;
-
-    /**
-     * @param inForeground {@code true} to indicate the application is open in the foreground
-     */
-    void setApplicationInForeground(boolean inForeground) {
-        mApplicationInForeground = inForeground;
-    }
-
-    /**
-     * @return {@code true} while the application is open in the foreground
-     */
-    boolean isApplicationInForeground() {
-        return mApplicationInForeground;
-    }
-
-    //
-    // Notification IDs
-    //
-    // Used elsewhere:
-    // Integer.MAX_VALUE - 4
-    // Integer.MAX_VALUE - 5
-    // Integer.MAX_VALUE - 7
-    //
-
-    /**
-     * @return a value that identifies the stopwatch notification
-     */
-    int getStopwatchNotificationId() {
-        return Integer.MAX_VALUE - 1;
-    }
-
-    /**
-     * @return a value that identifies the notification for running/paused timers
-     */
-    int getUnexpiredTimerNotificationId() {
-        return Integer.MAX_VALUE - 2;
-    }
-
-    /**
-     * @return a value that identifies the notification for expired timers
-     */
-    int getExpiredTimerNotificationId() {
-        return Integer.MAX_VALUE - 3;
-    }
-
-    /**
-     * @return a value that identifies the notification for missed timers
-     */
-    int getMissedTimerNotificationId() {
-        return Integer.MAX_VALUE - 6;
-    }
-
-    //
-    // Notification Group keys
-    //
-    // Used elsewhere:
-    // "1"
-    // "4"
-
-    /**
-     * @return the group key for the stopwatch notification
-     */
-    String getStopwatchNotificationGroupKey() {
-        return "3";
-    }
-
-    /**
-     * @return the group key for the timer notification
-     */
-    String getTimerNotificationGroupKey() {
-        return "2";
-    }
-
-    //
-    // Notification Sort keys
-    //
-
-    /**
-     * @return the sort key for the timer notification
-     */
-    String getTimerNotificationSortKey() {
-        return "0";
-    }
-
-    /**
-     * @return the sort key for the missed timer notification
-     */
-    String getTimerNotificationMissedSortKey() {
-        return "1";
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/NotificationModel.kt b/src/com/android/deskclock/data/NotificationModel.kt
new file mode 100644
index 0000000..f4c0faa
--- /dev/null
+++ b/src/com/android/deskclock/data/NotificationModel.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+/**
+ * Data that must be coordinated across all notifications is accessed via this model.
+ */
+internal class NotificationModel {
+    /**
+     * @return `true` while the application is open in the foreground
+     */
+    /**
+     * @param inForeground `true` to indicate the application is open in the foreground
+     */
+    var isApplicationInForeground = false
+
+    //
+    // Notification IDs
+    //
+    // Used elsewhere:
+    // Integer.MAX_VALUE - 4
+    // Integer.MAX_VALUE - 5
+    // Integer.MAX_VALUE - 7
+    //
+
+    /**
+     * @return a value that identifies the stopwatch notification
+     */
+    val stopwatchNotificationId: Int
+        get() = Int.MAX_VALUE - 1
+
+    /**
+     * @return a value that identifies the notification for running/paused timers
+     */
+    val unexpiredTimerNotificationId: Int
+        get() = Int.MAX_VALUE - 2
+
+    /**
+     * @return a value that identifies the notification for expired timers
+     */
+    val expiredTimerNotificationId: Int
+        get() = Int.MAX_VALUE - 3
+
+    /**
+     * @return a value that identifies the notification for missed timers
+     */
+    val missedTimerNotificationId: Int
+        get() = Int.MAX_VALUE - 6
+
+    //
+    // Notification Group keys
+    //
+    // Used elsewhere:
+    // "1"
+    // "4"
+
+    /**
+     * @return the group key for the stopwatch notification
+     */
+    val stopwatchNotificationGroupKey: String
+        get() = "3"
+
+    /**
+     * @return the group key for the timer notification
+     */
+    val timerNotificationGroupKey: String
+        get() = "2"
+
+    //
+    // Notification Sort keys
+    //
+
+    /**
+     * @return the sort key for the timer notification
+     */
+    val timerNotificationSortKey: String
+        get() = "0"
+
+    /**
+     * @return the sort key for the missed timer notification
+     */
+    val timerNotificationMissedSortKey: String
+        get() = "1"
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/OnSilentSettingsListener.java b/src/com/android/deskclock/data/OnSilentSettingsListener.kt
similarity index 72%
rename from src/com/android/deskclock/data/OnSilentSettingsListener.java
rename to src/com/android/deskclock/data/OnSilentSettingsListener.kt
index 783b6e0..47d5336 100644
--- a/src/com/android/deskclock/data/OnSilentSettingsListener.java
+++ b/src/com/android/deskclock/data/OnSilentSettingsListener.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.data;
+package com.android.deskclock.data
+
+import com.android.deskclock.data.DataModel.SilentSetting
 
 /**
  * The interface through which interested parties are notified of changes to device settings that
  * silence firing alarms.
  */
-public interface OnSilentSettingsListener {
-    void onSilentSettingsChange(DataModel.SilentSetting before, DataModel.SilentSetting after);
+interface OnSilentSettingsListener {
+    fun onSilentSettingsChange(before: SilentSetting?, after: SilentSetting?)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/RingtoneModel.java b/src/com/android/deskclock/data/RingtoneModel.java
deleted file mode 100644
index 90b7f91..0000000
--- a/src/com/android/deskclock/data/RingtoneModel.java
+++ /dev/null
@@ -1,231 +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.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/RingtoneModel.kt b/src/com/android/deskclock/data/RingtoneModel.kt
new file mode 100644
index 0000000..42349d6
--- /dev/null
+++ b/src/com/android/deskclock/data/RingtoneModel.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AudioManager.STREAM_ALARM
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.media.RingtoneManager.TITLE_COLUMN_INDEX
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+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.ClockContract.AlarmSettingColumns
+
+/**
+ * All ringtone data is accessed via this model.
+ */
+internal class RingtoneModel(private val mContext: Context, private val mPrefs: SharedPreferences) {
+    /** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive.  */
+    private val mRingtoneTitles: MutableMap<Uri, String?> = ArrayMap(16)
+
+    /** Clears data structures containing data that is locale-sensitive.  */
+    private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+    /** A mutable copy of the custom ringtones.  */
+    private var mCustomRingtones: MutableList<CustomRingtone>? = null
+
+    init {
+        // Clear caches affected by system settings when system settings change.
+        val cr: ContentResolver = mContext.getContentResolver()
+        val observer: ContentObserver = SystemAlarmAlertChangeObserver()
+        cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer)
+
+        // Clear caches affected by locale when locale changes.
+        val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+    }
+
+    fun addCustomRingtone(uri: Uri, title: String?): CustomRingtone? {
+        // If the uri is already present in an existing ringtone, do nothing.
+        val existing = getCustomRingtone(uri)
+        if (existing != null) {
+            return existing
+        }
+
+        val ringtone = CustomRingtoneDAO.addCustomRingtone(mPrefs, uri, title)
+        mutableCustomRingtones.add(ringtone)
+        mutableCustomRingtones.sort()
+        return ringtone
+    }
+
+    fun removeCustomRingtone(uri: Uri) {
+        val ringtones = mutableCustomRingtones
+        for (ringtone in ringtones) {
+            if (ringtone.uri.equals(uri)) {
+                CustomRingtoneDAO.removeCustomRingtone(mPrefs, ringtone.id)
+                ringtones.remove(ringtone)
+                break
+            }
+        }
+    }
+
+    private fun getCustomRingtone(uri: Uri): CustomRingtone? {
+        for (ringtone in mutableCustomRingtones) {
+            if (ringtone.uri.equals(uri)) {
+                return ringtone
+            }
+        }
+
+        return null
+    }
+
+    val customRingtones: List<CustomRingtone>
+        get() = mutableCustomRingtones
+
+    @SuppressLint("NewApi")
+    fun loadRingtonePermissions() {
+        val ringtones = mutableCustomRingtones
+        if (ringtones.isEmpty()) {
+            return
+        }
+
+        val uriPermissions: List<UriPermission> =
+                mContext.getContentResolver().getPersistedUriPermissions()
+        val permissions: MutableSet<Uri?> = ArraySet(uriPermissions.size)
+        for (uriPermission in uriPermissions) {
+            permissions.add(uriPermission.getUri())
+        }
+
+        val i = ringtones.listIterator()
+        while (i.hasNext()) {
+            val ringtone = i.next()
+            i.set(ringtone.setHasPermissions(permissions.contains(ringtone.uri)))
+        }
+    }
+
+    fun loadRingtoneTitles() {
+        // Early return if the cache is already primed.
+        if (mRingtoneTitles.isNotEmpty()) {
+            return
+        }
+
+        val ringtoneManager = RingtoneManager(mContext)
+        ringtoneManager.setType(STREAM_ALARM)
+
+        // Cache a title for each system ringtone.
+        try {
+            val cursor: Cursor? = ringtoneManager.getCursor()
+            cursor?.let {
+                cursor.moveToFirst()
+                while (!cursor.isAfterLast()) {
+                    val ringtoneTitle: String = cursor.getString(TITLE_COLUMN_INDEX)
+                    val ringtoneUri: Uri = ringtoneManager.getRingtoneUri(cursor.getPosition())
+                    mRingtoneTitles[ringtoneUri] = ringtoneTitle
+                    cursor.moveToNext()
+                }
+            }
+        } catch (ignored: Throwable) {
+            // best attempt only
+            LogUtils.e("Error loading ringtone title cache", ignored)
+        }
+    }
+
+    fun getRingtoneTitle(uri: Uri): String? {
+        // Special case: no ringtone has a title of "Silent".
+        if (AlarmSettingColumns.NO_RINGTONE_URI.equals(uri)) {
+            return mContext.getString(R.string.silent_ringtone_title)
+        }
+
+        // If the ringtone is custom, it has its own title.
+        val customRingtone = getCustomRingtone(uri)
+        if (customRingtone != null) {
+            return customRingtone.title
+        }
+
+        // Check the cache.
+        var title = mRingtoneTitles[uri]
+
+        if (title == null) {
+            // This is slow because a media player is created during Ringtone object creation.
+            val 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[uri] = title
+        }
+        return title
+    }
+
+    private val mutableCustomRingtones: MutableList<CustomRingtone>
+        get() {
+            if (mCustomRingtones == null) {
+                mCustomRingtones = CustomRingtoneDAO.getCustomRingtones(mPrefs)
+                mCustomRingtones!!.sort()
+            }
+
+            return mCustomRingtones!!
+        }
+
+    /**
+     * This receiver is notified when system settings change. Cached information built on
+     * those system settings must be cleared.
+     */
+    private inner class SystemAlarmAlertChangeObserver
+        : ContentObserver(Handler(Looper.myLooper()!!)) {
+        override fun onChange(selfChange: Boolean) {
+            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 inner class LocaleChangedReceiver : BroadcastReceiver() {
+        override fun 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
deleted file mode 100644
index 78e8e1a..0000000
--- a/src/com/android/deskclock/data/SettingsDAO.java
+++ /dev/null
@@ -1,387 +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.data;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.provider.Settings;
-import androidx.annotation.NonNull;
-import android.text.format.DateUtils;
-
-import com.android.deskclock.R;
-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}.
- */
-final class SettingsDAO {
-
-    /** Key to a preference that stores the preferred sort order of world cities. */
-    private static final String KEY_SORT_PREFERENCE = "sort_preference";
-
-    /** 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(SharedPreferences prefs) {
-        final int defaultSortOrdinal = CitySort.NAME.ordinal();
-        final int citySortOrdinal = prefs.getInt(KEY_SORT_PREFERENCE, defaultSortOrdinal);
-        return CitySort.values()[citySortOrdinal];
-    }
-
-    /**
-     * Adjust the sort order of cities.
-     */
-    static void toggleCitySort(SharedPreferences prefs) {
-        final CitySort oldSort = getCitySort(prefs);
-        final CitySort newSort = oldSort == CitySort.NAME ? CitySort.UTC_OFFSET : CitySort.NAME;
-        prefs.edit().putInt(KEY_SORT_PREFERENCE, newSort.ordinal()).apply();
-    }
-
-    /**
-     * @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(SharedPreferences prefs) {
-        return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, true);
-    }
-
-    /**
-     * @return the user's home timezone
-     */
-    static TimeZone getHomeTimeZone(Context context, SharedPreferences prefs, TimeZone defaultTZ) {
-        String timeZoneId = prefs.getString(SettingsActivity.KEY_HOME_TZ, null);
-
-        // 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, 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, 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(SharedPreferences prefs) {
-        return prefs.getBoolean(ScreensaverSettingsActivity.KEY_NIGHT_MODE, false);
-    }
-
-    /**
-     * @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
-     *      has yet been made
-     */
-    static Uri getTimerRingtoneUri(SharedPreferences prefs, Uri defaultUri) {
-        final String uriString = prefs.getString(SettingsActivity.KEY_TIMER_RINGTONE, null);
-        return uriString == null ? defaultUri : Uri.parse(uriString);
-    }
-
-    /**
-     * @return whether timer vibration is enabled. false by default.
-     */
-    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(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(SharedPreferences prefs, Uri uri) {
-        prefs.edit().putString(SettingsActivity.KEY_TIMER_RINGTONE, uri.toString()).apply();
-    }
-
-    /**
-     * @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
-     *      has yet been made
-     */
-    static Uri getDefaultAlarmRingtoneUri(SharedPreferences prefs) {
-        final String uriString = prefs.getString(KEY_DEFAULT_ALARM_RINGTONE_URI, null);
-        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(SharedPreferences prefs, Uri uri) {
-        prefs.edit().putString(KEY_DEFAULT_ALARM_RINGTONE_URI, uri.toString()).apply();
-    }
-
-    /**
-     * @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 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/SettingsDAO.kt b/src/com/android/deskclock/data/SettingsDAO.kt
new file mode 100644
index 0000000..43e77b5
--- /dev/null
+++ b/src/com/android/deskclock/data/SettingsDAO.kt
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Resources
+import android.net.Uri
+import android.provider.Settings
+import android.text.format.DateUtils
+import android.text.format.DateUtils.HOUR_IN_MILLIS
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+
+import com.android.deskclock.R
+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.data.Weekdays.Order
+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 kotlin.math.abs
+
+/**
+ * This class encapsulates the storage of application preferences in [SharedPreferences].
+ */
+internal object SettingsDAO {
+    /** Key to a preference that stores the preferred sort order of world cities.  */
+    private const val KEY_SORT_PREFERENCE = "sort_preference"
+
+    /** Key to a preference that stores the default ringtone for new alarms.  */
+    private const val KEY_DEFAULT_ALARM_RINGTONE_URI = "default_alarm_ringtone_uri"
+
+    /** Key to a preference that stores the global broadcast id.  */
+    private const val KEY_ALARM_GLOBAL_ID = "intent.extra.alarm.global.id"
+
+    /** Key to a preference that indicates whether restore (of backup and restore) has completed. */
+    private const val KEY_RESTORE_BACKUP_FINISHED = "restore_finished"
+
+    /**
+     * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
+     */
+    fun getGlobalIntentId(prefs: SharedPreferences): Int {
+        return prefs.getInt(KEY_ALARM_GLOBAL_ID, -1)
+    }
+
+    /**
+     * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
+     */
+    fun updateGlobalIntentId(prefs: SharedPreferences) {
+        val globalId: Int = 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
+     */
+    fun getCitySort(prefs: SharedPreferences): CitySort {
+        val defaultSortOrdinal = CitySort.NAME.ordinal
+        val citySortOrdinal: Int = prefs.getInt(KEY_SORT_PREFERENCE, defaultSortOrdinal)
+        return CitySort.values()[citySortOrdinal]
+    }
+
+    /**
+     * Adjust the sort order of cities.
+     */
+    fun toggleCitySort(prefs: SharedPreferences) {
+        val oldSort = getCitySort(prefs)
+        val newSort = if (oldSort == CitySort.NAME) CitySort.UTC_OFFSET else CitySort.NAME
+        prefs.edit().putInt(KEY_SORT_PREFERENCE, newSort.ordinal).apply()
+    }
+
+    /**
+     * @return `true` if a clock for the user's home timezone should be automatically
+     * displayed when it doesn't match the current timezone
+     */
+    fun getAutoShowHomeClock(prefs: SharedPreferences): Boolean {
+        return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, true)
+    }
+
+    /**
+     * @return the user's home timezone
+     */
+    fun getHomeTimeZone(context: Context, prefs: SharedPreferences, defaultTZ: TimeZone): TimeZone {
+        var timeZoneId: String? = prefs.getString(SettingsActivity.KEY_HOME_TZ, null)
+
+        // If the recorded home timezone is legal, use it.
+        val 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.id
+        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
+     */
+    fun getClockStyle(context: Context, prefs: SharedPreferences): ClockStyle {
+        return getClockStyle(context, prefs, SettingsActivity.KEY_CLOCK_STYLE)
+    }
+
+    /**
+     * @return a value indicating whether analog or digital clocks are displayed in the app
+     */
+    fun getDisplayClockSeconds(prefs: SharedPreferences): Boolean {
+        return prefs.getBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, false)
+    }
+
+    /**
+     * @param displaySeconds whether or not to display seconds on main clock
+     */
+    fun setDisplayClockSeconds(prefs: SharedPreferences, displaySeconds: Boolean) {
+        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.
+     */
+    fun setDefaultDisplayClockSeconds(context: Context, prefs: SharedPreferences) {
+        if (!prefs.contains(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS)) {
+            // If on analog clock style on upgrade, default to true. Otherwise, default to false.
+            val isAnalog = getClockStyle(context, prefs) == ClockStyle.ANALOG
+            setDisplayClockSeconds(prefs, isAnalog)
+        }
+    }
+
+    /**
+     * @return a value indicating whether analog or digital clocks are displayed on the screensaver
+     */
+    fun getScreensaverClockStyle(context: Context, prefs: SharedPreferences): ClockStyle {
+        return getClockStyle(context, prefs, ScreensaverSettingsActivity.KEY_CLOCK_STYLE)
+    }
+
+    /**
+     * @return `true` if the screen saver should be dimmed for lower contrast at night
+     */
+    fun getScreensaverNightModeOn(prefs: SharedPreferences): Boolean {
+        return prefs.getBoolean(ScreensaverSettingsActivity.KEY_NIGHT_MODE, false)
+    }
+
+    /**
+     * @return the uri of the selected ringtone or the `defaultUri` if no explicit selection
+     * has yet been made
+     */
+    fun getTimerRingtoneUri(prefs: SharedPreferences, defaultUri: Uri): Uri {
+        val uriString: String? = prefs.getString(SettingsActivity.KEY_TIMER_RINGTONE, null)
+        return if (uriString == null) defaultUri else Uri.parse(uriString)
+    }
+
+    /**
+     * @return whether timer vibration is enabled. false by default.
+     */
+    fun getTimerVibrate(prefs: SharedPreferences): Boolean {
+        return prefs.getBoolean(SettingsActivity.KEY_TIMER_VIBRATE, false)
+    }
+
+    /**
+     * @param enabled whether vibration will be turned on for all timers.
+     */
+    fun setTimerVibrate(prefs: SharedPreferences, enabled: Boolean) {
+        prefs.edit().putBoolean(SettingsActivity.KEY_TIMER_VIBRATE, enabled).apply()
+    }
+
+    /**
+     * @param uri the uri of the ringtone to play for all timers
+     */
+    fun setTimerRingtoneUri(prefs: SharedPreferences, uri: Uri) {
+        prefs.edit().putString(SettingsActivity.KEY_TIMER_RINGTONE, uri.toString()).apply()
+    }
+
+    /**
+     * @return the uri of the selected ringtone or the `defaultUri` if no explicit selection
+     * has yet been made
+     */
+    fun getDefaultAlarmRingtoneUri(prefs: SharedPreferences): Uri {
+        val uriString: String? = prefs.getString(KEY_DEFAULT_ALARM_RINGTONE_URI, null)
+        return if (uriString == null) {
+            Settings.System.DEFAULT_ALARM_ALERT_URI
+        } else {
+            Uri.parse(uriString)
+        }
+    }
+
+    /**
+     * @param uri identifies the default ringtone to play for new alarms
+     */
+    fun setDefaultAlarmRingtoneUri(prefs: SharedPreferences, uri: Uri) {
+        prefs.edit().putString(KEY_DEFAULT_ALARM_RINGTONE_URI, uri.toString()).apply()
+    }
+
+    /**
+     * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
+     * `0` implies no crescendo should be applied
+     */
+    fun getAlarmCrescendoDuration(prefs: SharedPreferences): Long {
+        val crescendoSeconds: String = prefs.getString(SettingsActivity.KEY_ALARM_CRESCENDO, "0")!!
+        return crescendoSeconds.toInt() * DateUtils.SECOND_IN_MILLIS
+    }
+
+    /**
+     * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+     * `0` implies no crescendo should be applied
+     */
+    fun getTimerCrescendoDuration(prefs: SharedPreferences): Long {
+        val crescendoSeconds: String = prefs.getString(SettingsActivity.KEY_TIMER_CRESCENDO, "0")!!
+        return crescendoSeconds.toInt() * DateUtils.SECOND_IN_MILLIS
+    }
+
+    /**
+     * @return the display order of the weekdays, which can start with [Calendar.SATURDAY],
+     * [Calendar.SUNDAY] or [Calendar.MONDAY]
+     */
+    fun getWeekdayOrder(prefs: SharedPreferences): Order {
+        val defaultValue = Calendar.getInstance().firstDayOfWeek.toString()
+        val value: String = prefs.getString(SettingsActivity.KEY_WEEK_START, defaultValue)!!
+        return when (val firstCalendarDay = value.toInt()) {
+            Calendar.SATURDAY -> Order.SAT_TO_FRI
+            Calendar.SUNDAY -> Order.SUN_TO_SAT
+            Calendar.MONDAY -> Order.MON_TO_SUN
+            else -> throw IllegalArgumentException("Unknown weekday: $firstCalendarDay")
+        }
+    }
+
+    /**
+     * @return `true` if the restore process (of backup and restore) has completed
+     */
+    fun isRestoreBackupFinished(prefs: SharedPreferences): Boolean {
+        return prefs.getBoolean(KEY_RESTORE_BACKUP_FINISHED, false)
+    }
+
+    /**
+     * @param finished `true` means the restore process (of backup and restore) has completed
+     */
+    fun setRestoreBackupFinished(prefs: SharedPreferences, finished: Boolean) {
+        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
+     */
+    fun getAlarmVolumeButtonBehavior(prefs: SharedPreferences): AlarmVolumeButtonBehavior {
+        val defaultValue = SettingsActivity.DEFAULT_VOLUME_BEHAVIOR
+        val value: String = prefs.getString(SettingsActivity.KEY_VOLUME_BUTTONS, defaultValue)!!
+        return when (value) {
+            SettingsActivity.DEFAULT_VOLUME_BEHAVIOR -> AlarmVolumeButtonBehavior.NOTHING
+            SettingsActivity.VOLUME_BEHAVIOR_SNOOZE -> AlarmVolumeButtonBehavior.SNOOZE
+            SettingsActivity.VOLUME_BEHAVIOR_DISMISS -> AlarmVolumeButtonBehavior.DISMISS
+            else -> throw IllegalArgumentException("Unknown volume button behavior: $value")
+        }
+    }
+
+    /**
+     * @return the number of minutes an alarm may ring before it has timed out and becomes missed
+     */
+    fun getAlarmTimeout(prefs: SharedPreferences): Int {
+        // Default value must match the one in res/xml/settings.xml
+        val string: String = prefs.getString(SettingsActivity.KEY_AUTO_SILENCE, "10")!!
+        return string.toInt()
+    }
+
+    /**
+     * @return the number of minutes an alarm will remain snoozed before it rings again
+     */
+    fun getSnoozeLength(prefs: SharedPreferences): Int {
+        // Default value must match the one in res/xml/settings.xml
+        val string: String = prefs.getString(SettingsActivity.KEY_ALARM_SNOOZE, "10")!!
+        return string.toInt()
+    }
+
+    /**
+     * @param currentTime timezone offsets created relative to this time
+     * @return a description of the time zones available for selection
+     */
+    fun getTimeZones(context: Context, currentTime: Long): TimeZones {
+        val locale = Locale.getDefault()
+        val resources: Resources = context.getResources()
+        val timeZoneIds: Array<String> = resources.getStringArray(R.array.timezone_values)
+        val timeZoneNames: Array<String> = resources.getStringArray(R.array.timezone_labels)
+
+        // Verify the data is consistent.
+        if (timeZoneIds.size != timeZoneNames.size) {
+            val message = String.format(Locale.US,
+                    "id count (%d) does not match name count (%d) for locale %s",
+                    timeZoneIds.size, timeZoneNames.size, locale)
+            throw IllegalStateException(message)
+        }
+
+        // Create TimeZoneDescriptors for each TimeZone so they can be sorted.
+        val descriptors = arrayOfNulls<TimeZoneDescriptor>(timeZoneIds.size)
+        for (i in timeZoneIds.indices) {
+            val id = timeZoneIds[i]
+            val name = timeZoneNames[i].replace("\"".toRegex(), "")
+            descriptors[i] = TimeZoneDescriptor(locale, id, name, currentTime)
+        }
+        Arrays.sort(descriptors)
+
+        // Transfer the TimeZoneDescriptors into parallel arrays for easy consumption by the caller.
+        val tzIds = arrayOfNulls<CharSequence>(descriptors.size)
+        val tzNames = arrayOfNulls<CharSequence>(descriptors.size)
+        for (i in descriptors.indices) {
+            val descriptor = descriptors[i]
+            tzIds[i] = descriptor!!.mTimeZoneId
+            tzNames[i] = descriptor.mTimeZoneName
+        }
+
+        return TimeZones(tzIds.requireNoNulls(), tzNames.requireNoNulls())
+    }
+
+    private fun getClockStyle(context: Context, prefs: SharedPreferences, key: String): ClockStyle {
+        val defaultStyle: String = context.getString(R.string.default_clock_style)
+        val clockStyle: String = 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 class TimeZoneDescriptor(
+        locale: Locale,
+        val mTimeZoneId: String,
+        name: String,
+        currentTime: Long
+    ) : Comparable<TimeZoneDescriptor> {
+        private val mOffset: Int
+        val mTimeZoneName: String
+
+        init {
+            val tz = TimeZone.getTimeZone(mTimeZoneId)
+            mOffset = tz.getOffset(currentTime)
+
+            val sign = if (mOffset < 0) '-' else '+'
+            val absoluteGMTOffset = abs(mOffset)
+            val hour: Long = absoluteGMTOffset / HOUR_IN_MILLIS
+            val minute: Long = absoluteGMTOffset / MINUTE_IN_MILLIS % 60
+            mTimeZoneName = String.format(locale, "(GMT%s%d:%02d) %s", sign, hour, minute, name)
+        }
+
+        override fun compareTo(other: TimeZoneDescriptor): Int {
+            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
deleted file mode 100644
index 103c210..0000000
--- a/src/com/android/deskclock/data/SettingsModel.java
+++ /dev/null
@@ -1,175 +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.data;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.net.Uri;
-
-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 java.util.TimeZone;
-
-/**
- * All settings data is accessed via this model.
- */
-final class SettingsModel {
-
-    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, SharedPreferences prefs, TimeModel timeModel) {
-        mContext = context;
-        mPrefs = prefs;
-        mTimeModel = timeModel;
-
-        // 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(mPrefs);
-    }
-
-    void toggleCitySort() {
-        SettingsDAO.toggleCitySort(mPrefs);
-    }
-
-    TimeZone getHomeTimeZone() {
-        return SettingsDAO.getHomeTimeZone(mContext, mPrefs, TimeZone.getDefault());
-    }
-
-    ClockStyle getClockStyle() {
-        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, mPrefs);
-    }
-
-    boolean getScreensaverNightModeOn() {
-        return SettingsDAO.getScreensaverNightModeOn(mPrefs);
-    }
-
-    boolean getShowHomeClock() {
-        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 defaultTZ = TimeZone.getDefault();
-        final TimeZone homeTimeZone = SettingsDAO.getHomeTimeZone(mContext, mPrefs, defaultTZ);
-        final long now = System.currentTimeMillis();
-        return homeTimeZone.getOffset(now) != defaultTZ.getOffset(now);
-    }
-
-    Uri getDefaultTimerRingtoneUri() {
-        if (mDefaultTimerRingtoneUri == null) {
-            mDefaultTimerRingtoneUri = Utils.getResourceUri(mContext, R.raw.timer_expire);
-        }
-
-        return mDefaultTimerRingtoneUri;
-    }
-
-    void setTimerRingtoneUri(Uri uri) {
-        SettingsDAO.setTimerRingtoneUri(mPrefs, uri);
-    }
-
-    Uri getTimerRingtoneUri() {
-        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(mPrefs);
-    }
-
-    void setDefaultAlarmRingtoneUri(Uri 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(mPrefs);
-    }
-
-    void setTimerVibrate(boolean 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/SettingsModel.kt b/src/com/android/deskclock/data/SettingsModel.kt
new file mode 100644
index 0000000..040c791
--- /dev/null
+++ b/src/com/android/deskclock/data/SettingsModel.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.net.Uri
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.util.TimeZone
+
+/**
+ * All settings data is accessed via this model.
+ */
+internal class SettingsModel(
+    private val mContext: Context,
+    private val mPrefs: SharedPreferences,
+    /** The model from which time data are fetched.  */
+    private val mTimeModel: TimeModel
+) {
+
+    /** The uri of the default ringtone to use for timers until the user explicitly chooses one.  */
+    private var mDefaultTimerRingtoneUri: Uri? = null
+
+    init {
+        // Set the user's default display seconds preference if one has not yet been chosen.
+        SettingsDAO.setDefaultDisplayClockSeconds(mContext, mPrefs)
+    }
+
+    val globalIntentId: Int
+        get() = SettingsDAO.getGlobalIntentId(mPrefs)
+
+    fun updateGlobalIntentId() {
+        SettingsDAO.updateGlobalIntentId(mPrefs)
+    }
+
+    val citySort: DataModel.CitySort
+        get() = SettingsDAO.getCitySort(mPrefs)
+
+    fun toggleCitySort() {
+        SettingsDAO.toggleCitySort(mPrefs)
+    }
+
+    val homeTimeZone: TimeZone
+        get() = SettingsDAO.getHomeTimeZone(mContext, mPrefs, TimeZone.getDefault())
+
+    val clockStyle: DataModel.ClockStyle
+        get() = SettingsDAO.getClockStyle(mContext, mPrefs)
+
+    var displayClockSeconds: Boolean
+        get() = SettingsDAO.getDisplayClockSeconds(mPrefs)
+        set(shouldDisplaySeconds) {
+            SettingsDAO.setDisplayClockSeconds(mPrefs, shouldDisplaySeconds)
+        }
+
+    val screensaverClockStyle: DataModel.ClockStyle
+        get() = SettingsDAO.getScreensaverClockStyle(mContext, mPrefs)
+
+    val screensaverNightModeOn: Boolean
+        get() = SettingsDAO.getScreensaverNightModeOn(mPrefs)
+
+    val showHomeClock: Boolean
+        get() {
+            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)
+            val defaultTZ = TimeZone.getDefault()
+            val homeTimeZone = SettingsDAO.getHomeTimeZone(mContext, mPrefs, defaultTZ)
+            val now = System.currentTimeMillis()
+            return homeTimeZone.getOffset(now) != defaultTZ.getOffset(now)
+        }
+
+    val defaultTimerRingtoneUri: Uri
+        get() {
+            if (mDefaultTimerRingtoneUri == null) {
+                mDefaultTimerRingtoneUri = Utils.getResourceUri(mContext, R.raw.timer_expire)
+            }
+
+            return mDefaultTimerRingtoneUri!!
+        }
+
+    var timerRingtoneUri: Uri
+        get() = SettingsDAO.getTimerRingtoneUri(mPrefs, defaultTimerRingtoneUri)
+        set(uri) {
+            SettingsDAO.setTimerRingtoneUri(mPrefs, uri)
+        }
+
+    val alarmVolumeButtonBehavior: DataModel.AlarmVolumeButtonBehavior
+        get() = SettingsDAO.getAlarmVolumeButtonBehavior(mPrefs)
+
+    val alarmTimeout: Int
+        get() = SettingsDAO.getAlarmTimeout(mPrefs)
+
+    val snoozeLength: Int
+        get() = SettingsDAO.getSnoozeLength(mPrefs)
+
+    var defaultAlarmRingtoneUri: Uri
+        get() = SettingsDAO.getDefaultAlarmRingtoneUri(mPrefs)
+        set(uri) {
+            SettingsDAO.setDefaultAlarmRingtoneUri(mPrefs, uri)
+        }
+
+    val alarmCrescendoDuration: Long
+        get() = SettingsDAO.getAlarmCrescendoDuration(mPrefs)
+
+    val timerCrescendoDuration: Long
+        get() = SettingsDAO.getTimerCrescendoDuration(mPrefs)
+
+    val weekdayOrder: Weekdays.Order
+        get() = SettingsDAO.getWeekdayOrder(mPrefs)
+
+    var isRestoreBackupFinished: Boolean
+        get() = SettingsDAO.isRestoreBackupFinished(mPrefs)
+        set(finished) {
+            SettingsDAO.setRestoreBackupFinished(mPrefs, finished)
+        }
+
+    var timerVibrate: Boolean
+        get() = SettingsDAO.getTimerVibrate(mPrefs)
+        set(enabled) {
+            SettingsDAO.setTimerVibrate(mPrefs, enabled)
+        }
+
+    val timeZones: TimeZones
+        get() = 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
deleted file mode 100644
index b50702a..0000000
--- a/src/com/android/deskclock/data/SilentSettingsModel.java
+++ /dev/null
@@ -1,246 +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.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 androidx.core.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/SilentSettingsModel.kt b/src/com/android/deskclock/data/SilentSettingsModel.kt
new file mode 100644
index 0000000..a21f1b5
--- /dev/null
+++ b/src/com/android/deskclock/data/SilentSettingsModel.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED
+import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Context.AUDIO_SERVICE
+import android.content.Context.NOTIFICATION_SERVICE
+import android.content.Intent
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.media.AudioManager
+import android.media.AudioManager.STREAM_ALARM
+import android.media.RingtoneManager
+import android.media.RingtoneManager.TYPE_ALARM
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings.System.CONTENT_URI
+import android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI
+import androidx.core.app.NotificationManagerCompat
+
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel.SilentSetting
+
+/**
+ * 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.
+ */
+internal class SilentSettingsModel(
+    private val mContext: Context,
+    /** Used to determine if the application is in the foreground.  */
+    private val mNotificationModel: NotificationModel
+) {
+
+    /** Used to query the alarm volume and display the system control to change the alarm volume. */
+    private val mAudioManager = mContext.getSystemService(AUDIO_SERVICE) as AudioManager
+
+    /** Used to query the do-not-disturb setting value, also called "interruption filter".  */
+    private val mNotificationManager =
+            mContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+    /** List of listeners to invoke upon silence state change.  */
+    private val mListeners: MutableList<OnSilentSettingsListener> = ArrayList(1)
+
+    /**
+     * The last setting known to be blocking alarms; `null` indicates no settings are
+     * blocking the app or the app is not in the foreground.
+     */
+    private var mSilentSetting: SilentSetting? = null
+
+    /** The background task that checks the device system settings that influence alarm firing.  */
+    private var mCheckSilenceSettingsTask: CheckSilenceSettingsTask? = null
+
+    init {
+        // Watch for changes to the settings that may silence alarms.
+        val cr: ContentResolver = mContext.getContentResolver()
+        val contentChangeWatcher: ContentObserver = ContentChangeWatcher()
+        cr.registerContentObserver(VOLUME_URI, false, contentChangeWatcher)
+        cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, contentChangeWatcher)
+        if (Utils.isMOrLater) {
+            val filter = IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED)
+            mContext.registerReceiver(DoNotDisturbChangeReceiver(), filter)
+        }
+    }
+
+    fun addSilentSettingsListener(listener: OnSilentSettingsListener) {
+        mListeners.add(listener)
+    }
+
+    fun removeSilentSettingsListener(listener: OnSilentSettingsListener) {
+        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.
+     */
+    fun updateSilentState() {
+        // Cancel any task in flight, the result is no longer relevant.
+        if (mCheckSilenceSettingsTask != null) {
+            mCheckSilenceSettingsTask!!.cancel(true)
+            mCheckSilenceSettingsTask = null
+        }
+
+        if (mNotificationModel.isApplicationInForeground) {
+            mCheckSilenceSettingsTask = CheckSilenceSettingsTask()
+            mCheckSilenceSettingsTask!!.execute()
+        } else {
+            setSilentState(null)
+        }
+    }
+
+    /**
+     * @param silentSetting the latest notion of which setting is suppressing alarms; `null`
+     * if no settings are suppressing alarms
+     */
+    private fun setSilentState(silentSetting: SilentSetting?) {
+        if (mSilentSetting != silentSetting) {
+            val oldReason = mSilentSetting
+            mSilentSetting = silentSetting
+            for (listener in 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.
+     */
+    // TODO(b/165664115) Replace deprecated AsyncTask calls
+    private inner class CheckSilenceSettingsTask : AsyncTask<Void?, Void?, SilentSetting?>() {
+        override fun doInBackground(vararg parameters: Void?): SilentSetting? {
+            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 fun onCancelled() {
+            super.onCancelled()
+            if (mCheckSilenceSettingsTask == this) {
+                mCheckSilenceSettingsTask = null
+            }
+        }
+
+        override fun onPostExecute(silentSetting: SilentSetting?) {
+            if (mCheckSilenceSettingsTask == this) {
+                mCheckSilenceSettingsTask = null
+                setSilentState(silentSetting)
+            }
+        }
+
+        @get:TargetApi(Build.VERSION_CODES.M)
+        private val isDoNotDisturbBlockingAlarms: Boolean
+            get() = if (!Utils.isMOrLater) {
+                false
+            } else try {
+                val interruptionFilter: Int = mNotificationManager.getCurrentInterruptionFilter()
+                interruptionFilter == INTERRUPTION_FILTER_NONE
+            } catch (e: Exception) {
+                // Since this is purely informational, avoid crashing the app.
+                false
+            }
+
+        private val isAlarmStreamMuted: Boolean
+            get() = try {
+                mAudioManager.getStreamVolume(STREAM_ALARM) <= 0
+            } catch (e: Exception) {
+                // Since this is purely informational, avoid crashing the app.
+                false
+            }
+
+        private val isSystemAlarmRingtoneSilent: Boolean
+            get() = try {
+                RingtoneManager.getActualDefaultRingtoneUri(mContext, TYPE_ALARM) == null
+            } catch (e: Exception) {
+                // Since this is purely informational, avoid crashing the app.
+                false
+            }
+
+        private val isAppNotificationBlocked: Boolean
+            get() = try {
+                !NotificationManagerCompat.from(mContext).areNotificationsEnabled()
+            } catch (e: Exception) {
+                // Since this is purely informational, avoid crashing the app.
+                false
+            }
+    }
+
+    /**
+     * Observe changes to specific URI for settings that can silence firing alarms.
+     */
+    private inner class ContentChangeWatcher : ContentObserver(Handler(Looper.myLooper()!!)) {
+        override fun onChange(selfChange: Boolean) {
+            updateSilentState()
+        }
+    }
+
+    /**
+     * Observe changes to the do-not-disturb setting.
+     */
+    private inner class DoNotDisturbChangeReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            updateSilentState()
+        }
+    }
+
+    companion object {
+        /** The Uri to the settings entry that stores alarm stream volume.  */
+        private val VOLUME_URI: Uri = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker")
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Stopwatch.java b/src/com/android/deskclock/data/Stopwatch.java
deleted file mode 100644
index f53af38..0000000
--- a/src/com/android/deskclock/data/Stopwatch.java
+++ /dev/null
@@ -1,151 +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.data;
-
-import static com.android.deskclock.Utils.now;
-import static com.android.deskclock.Utils.wallClock;
-import static com.android.deskclock.data.Stopwatch.State.PAUSED;
-import static com.android.deskclock.data.Stopwatch.State.RESET;
-import static com.android.deskclock.data.Stopwatch.State.RUNNING;
-
-/**
- * A read-only domain object representing a stopwatch.
- */
-public final class Stopwatch {
-
-    public enum State { RESET, RUNNING, PAUSED }
-
-    static final long UNUSED = Long.MIN_VALUE;
-
-    /** The single, immutable instance of a reset stopwatch. */
-    private static final Stopwatch RESET_STOPWATCH = new Stopwatch(RESET, UNUSED, UNUSED, 0);
-
-    /** Current state of this stopwatch. */
-    private final State mState;
-
-    /** Elapsed time in ms the stopwatch was last started; {@link #UNUSED} if not running. */
-    private final long mLastStartTime;
-
-    /** The time since epoch at which the stopwatch was last started. */
-    private final long mLastStartWallClockTime;
-
-    /** Elapsed time in ms this stopwatch has accumulated while running. */
-    private final long mAccumulatedTime;
-
-    Stopwatch(State state, long lastStartTime, long lastWallClockTime, long accumulatedTime) {
-        mState = state;
-        mLastStartTime = lastStartTime;
-        mLastStartWallClockTime = lastWallClockTime;
-        mAccumulatedTime = accumulatedTime;
-    }
-
-    public State getState() { return mState; }
-    public long getLastStartTime() { return mLastStartTime; }
-    public long getLastWallClockTime() { return mLastStartWallClockTime; }
-    public boolean isReset() { return mState == RESET; }
-    public boolean isPaused() { return mState == PAUSED; }
-    public boolean isRunning() { return mState == RUNNING; }
-
-    /**
-     * @return the total amount of time accumulated up to this moment
-     */
-    public long getTotalTime() {
-        if (mState != RUNNING) {
-            return mAccumulatedTime;
-        }
-
-        // 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 stopwatch is monotonically increasing, normalize negative time segments to 0,
-        final long timeSinceStart = now() - mLastStartTime;
-        return mAccumulatedTime + Math.max(0, timeSinceStart);
-    }
-
-    /**
-     * @return the amount of time accumulated up to the last time the stopwatch was started
-     */
-    public long getAccumulatedTime() {
-        return mAccumulatedTime;
-    }
-
-    /**
-     * @return a copy of this stopwatch that is running
-     */
-    Stopwatch start() {
-        if (mState == RUNNING) {
-            return this;
-        }
-
-        return new Stopwatch(RUNNING, now(), wallClock(), getTotalTime());
-    }
-
-    /**
-     * @return a copy of this stopwatch that is paused
-     */
-    Stopwatch pause() {
-        if (mState != RUNNING) {
-            return this;
-        }
-
-        return new Stopwatch(PAUSED, UNUSED, UNUSED, getTotalTime());
-    }
-
-    /**
-     * @return a copy of this stopwatch that is reset
-     */
-    Stopwatch reset() {
-        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;
-        }
-        final long timeSinceBoot = now();
-        final long wallClockTime = wallClock();
-        // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
-        // update the recorded times and proceed with no change in accumulated time.
-        final long delta = Math.max(0, wallClockTime - mLastStartWallClockTime);
-        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;
-        }
-        final long timeSinceBoot = now();
-        final long wallClockTime = wallClock();
-        final long delta = timeSinceBoot - mLastStartTime;
-        if (delta < 0) {
-            // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
-            // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
-            // updateAfterReboot() can successfully correct the data at a later time.
-            return this;
-        }
-        return new Stopwatch(mState, timeSinceBoot, wallClockTime, mAccumulatedTime + delta);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Stopwatch.kt b/src/com/android/deskclock/data/Stopwatch.kt
new file mode 100644
index 0000000..2953670
--- /dev/null
+++ b/src/com/android/deskclock/data/Stopwatch.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.deskclock.Utils
+
+import kotlin.math.max
+
+/**
+ * A read-only domain object representing a stopwatch.
+ */
+class Stopwatch internal constructor(
+    /** Current state of this stopwatch.  */
+    val state: State,
+    /** Elapsed time in ms the stopwatch was last started; [.UNUSED] if not running.  */
+    val lastStartTime: Long,
+    /** The time since epoch at which the stopwatch was last started.  */
+    val lastWallClockTime: Long,
+    /** Elapsed time in ms this stopwatch has accumulated while running.  */
+    val accumulatedTime: Long
+) {
+
+    enum class State {
+        RESET, RUNNING, PAUSED
+    }
+
+    val isReset: Boolean
+        get() = state == State.RESET
+
+    val isPaused: Boolean
+        get() = state == State.PAUSED
+
+    val isRunning: Boolean
+        get() = state == State.RUNNING
+
+    /**
+     * @return the total amount of time accumulated up to this moment
+     */
+    val totalTime: Long
+        get() {
+            if (state != State.RUNNING) {
+                return accumulatedTime
+            }
+
+            // 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 stopwatch is monotonically increasing, normalize negative time segments to
+            // 0
+            val timeSinceStart = Utils.now() - lastStartTime
+            return accumulatedTime + max(0, timeSinceStart)
+        }
+
+    /**
+     * @return a copy of this stopwatch that is running
+     */
+    fun start(): Stopwatch {
+        return if (state == State.RUNNING) {
+            this
+        } else {
+            Stopwatch(State.RUNNING, Utils.now(), Utils.wallClock(), totalTime)
+        }
+    }
+
+    /**
+     * @return a copy of this stopwatch that is paused
+     */
+    fun pause(): Stopwatch {
+        return if (state != State.RUNNING) {
+            this
+        } else {
+            Stopwatch(State.PAUSED, UNUSED, UNUSED, totalTime)
+        }
+    }
+
+    /**
+     * @return a copy of this stopwatch that is reset
+     */
+    fun reset(): Stopwatch = 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.
+     */
+    fun updateAfterReboot(): Stopwatch {
+        if (state != State.RUNNING) {
+            return this
+        }
+        val timeSinceBoot = Utils.now()
+        val wallClockTime = Utils.wallClock()
+        // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
+        // update the recorded times and proceed with no change in accumulated time.
+        val delta = max(0, wallClockTime - lastWallClockTime)
+        return Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + 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.
+     */
+    fun updateAfterTimeSet(): Stopwatch {
+        if (state != State.RUNNING) {
+            return this
+        }
+        val timeSinceBoot = Utils.now()
+        val wallClockTime = Utils.wallClock()
+        val delta = timeSinceBoot - lastStartTime
+        return if (delta < 0) {
+            // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
+            // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
+            // updateAfterReboot() can successfully correct the data at a later time.
+            this
+        } else {
+            Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta)
+        }
+    }
+
+    companion object {
+        const val UNUSED = Long.MIN_VALUE
+
+        /** The single, immutable instance of a reset stopwatch.  */
+        private val RESET_STOPWATCH = Stopwatch(State.RESET, UNUSED, UNUSED, 0)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchDAO.java b/src/com/android/deskclock/data/StopwatchDAO.java
deleted file mode 100644
index 5413901..0000000
--- a/src/com/android/deskclock/data/StopwatchDAO.java
+++ /dev/null
@@ -1,152 +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.data;
-
-import android.content.SharedPreferences;
-
-import com.android.deskclock.data.Stopwatch.State;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static com.android.deskclock.data.Stopwatch.State.RESET;
-
-/**
- * This class encapsulates the transfer of data between {@link Stopwatch} and {@link Lap} domain
- * objects and their permanent storage in {@link SharedPreferences}.
- */
-final class StopwatchDAO {
-
-    /** Key to a preference that stores the state of the stopwatch. */
-    private static final String STATE = "sw_state";
-
-    /** Key to a preference that stores the last start time of the stopwatch. */
-    private static final String LAST_START_TIME = "sw_start_time";
-
-    /** Key to a preference that stores the epoch time when the stopwatch last started. */
-    private static final String LAST_WALL_CLOCK_TIME = "sw_wall_clock_time";
-
-    /** Key to a preference that stores the accumulated elapsed time of the stopwatch. */
-    private static final String ACCUMULATED_TIME = "sw_accum_time";
-
-    /** Prefix for a key to a preference that stores the number of recorded laps. */
-    private static final String LAP_COUNT = "sw_lap_num";
-
-    /** Prefix for a key to a preference that stores accumulated time at the end of a lap. */
-    private static final String LAP_ACCUMULATED_TIME = "sw_lap_time_";
-
-    private StopwatchDAO() {}
-
-    /**
-     * @return the stopwatch from permanent storage or a reset stopwatch if none exists
-     */
-    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);
-        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(SharedPreferences prefs, Stopwatch stopwatch) {
-        final SharedPreferences.Editor editor = prefs.edit();
-
-        if (stopwatch.isReset()) {
-            editor.remove(STATE)
-                    .remove(LAST_START_TIME)
-                    .remove(LAST_WALL_CLOCK_TIME)
-                    .remove(ACCUMULATED_TIME);
-        } else {
-            editor.putInt(STATE, stopwatch.getState().ordinal())
-                    .putLong(LAST_START_TIME, stopwatch.getLastStartTime())
-                    .putLong(LAST_WALL_CLOCK_TIME, stopwatch.getLastWallClockTime())
-                    .putLong(ACCUMULATED_TIME, stopwatch.getAccumulatedTime());
-        }
-
-        editor.apply();
-    }
-
-    /**
-     * @return a list of recorded laps for the stopwatch
-     */
-    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);
-
-        long prevAccumulatedTime = 0;
-
-        // Lap numbers are 1-based and so the are corresponding shared preference keys.
-        for (int lapNumber = 1; lapNumber <= lapCount; lapNumber++) {
-            // Look up the accumulated time for the lap.
-            final String lapAccumulatedTimeKey = LAP_ACCUMULATED_TIME + lapNumber;
-            final long accumulatedTime = prefs.getLong(lapAccumulatedTimeKey, 0);
-
-            // Lap time is the delta between accumulated time of this lap and prior lap.
-            final long lapTime = accumulatedTime - prevAccumulatedTime;
-
-            // Create the lap instance from the data.
-            laps.add(new Lap(lapNumber, lapTime, accumulatedTime));
-
-            // Update the accumulated time of the previous lap.
-            prevAccumulatedTime = accumulatedTime;
-        }
-
-        // Laps are stored in the order they were recorded; display order is the reverse.
-        Collections.reverse(laps);
-
-        return laps;
-    }
-
-    /**
-     * @param newLapCount the number of laps including the new lap
-     * @param accumulatedTime the amount of time accumulate by the stopwatch at the end of the lap
-     */
-    static void addLap(SharedPreferences prefs, int newLapCount, long accumulatedTime) {
-        prefs.edit()
-                .putInt(LAP_COUNT, newLapCount)
-                .putLong(LAP_ACCUMULATED_TIME + newLapCount, accumulatedTime)
-                .apply();
-    }
-
-    /**
-     * Remove the recorded laps for the stopwatch
-     */
-    static void clearLaps(SharedPreferences prefs) {
-        final SharedPreferences.Editor editor = prefs.edit();
-
-        final int lapCount = prefs.getInt(LAP_COUNT, 0);
-        for (int lapNumber = 1; lapNumber <= lapCount; lapNumber++) {
-            editor.remove(LAP_ACCUMULATED_TIME + lapNumber);
-        }
-        editor.remove(LAP_COUNT);
-
-        editor.apply();
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchDAO.kt b/src/com/android/deskclock/data/StopwatchDAO.kt
new file mode 100644
index 0000000..5e049fe
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchDAO.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+/**
+ * This class encapsulates the transfer of data between [Stopwatch] and [Lap] domain
+ * objects and their permanent storage in [SharedPreferences].
+ */
+internal object StopwatchDAO {
+    /** Key to a preference that stores the state of the stopwatch.  */
+    private const val STATE = "sw_state"
+
+    /** Key to a preference that stores the last start time of the stopwatch.  */
+    private const val LAST_START_TIME = "sw_start_time"
+
+    /** Key to a preference that stores the epoch time when the stopwatch last started.  */
+    private const val LAST_WALL_CLOCK_TIME = "sw_wall_clock_time"
+
+    /** Key to a preference that stores the accumulated elapsed time of the stopwatch.  */
+    private const val ACCUMULATED_TIME = "sw_accum_time"
+
+    /** Prefix for a key to a preference that stores the number of recorded laps.  */
+    private const val LAP_COUNT = "sw_lap_num"
+
+    /** Prefix for a key to a preference that stores accumulated time at the end of a lap.  */
+    private const val LAP_ACCUMULATED_TIME = "sw_lap_time_"
+
+    /**
+     * @return the stopwatch from permanent storage or a reset stopwatch if none exists
+     */
+    fun getStopwatch(prefs: SharedPreferences): Stopwatch {
+        val stateIndex: Int = prefs.getInt(STATE, Stopwatch.State.RESET.ordinal)
+        val state = Stopwatch.State.values()[stateIndex]
+        val lastStartTime: Long = prefs.getLong(LAST_START_TIME, Stopwatch.UNUSED)
+        val lastWallClockTime: Long = prefs.getLong(LAST_WALL_CLOCK_TIME, Stopwatch.UNUSED)
+        val accumulatedTime: Long = prefs.getLong(ACCUMULATED_TIME, 0)
+        var s = Stopwatch(state, lastStartTime, lastWallClockTime, accumulatedTime)
+
+        // If the stopwatch reports an illegal (negative) amount of time, remove the bad data.
+        if (s.totalTime < 0) {
+            s = s.reset()
+            setStopwatch(prefs, s)
+        }
+        return s
+    }
+
+    /**
+     * @param stopwatch the last state of the stopwatch
+     */
+    fun setStopwatch(prefs: SharedPreferences, stopwatch: Stopwatch) {
+        val editor: SharedPreferences.Editor = prefs.edit()
+
+        if (stopwatch.isReset) {
+            editor.remove(STATE)
+                    .remove(LAST_START_TIME)
+                    .remove(LAST_WALL_CLOCK_TIME)
+                    .remove(ACCUMULATED_TIME)
+        } else {
+            editor.putInt(STATE, stopwatch.state.ordinal)
+                    .putLong(LAST_START_TIME, stopwatch.lastStartTime)
+                    .putLong(LAST_WALL_CLOCK_TIME, stopwatch.lastWallClockTime)
+                    .putLong(ACCUMULATED_TIME, stopwatch.accumulatedTime)
+        }
+
+        editor.apply()
+    }
+
+    /**
+     * @return a list of recorded laps for the stopwatch
+     */
+    fun getLaps(prefs: SharedPreferences): MutableList<Lap> {
+        // Prepare the container to be filled with laps.
+        val lapCount: Int = prefs.getInt(LAP_COUNT, 0)
+        val laps: MutableList<Lap> = mutableListOf()
+
+        var prevAccumulatedTime: Long = 0
+
+        // Lap numbers are 1-based and so the are corresponding shared preference keys.
+        for (lapNumber in 1..lapCount) {
+            // Look up the accumulated time for the lap.
+            val lapAccumulatedTimeKey = LAP_ACCUMULATED_TIME + lapNumber
+            val accumulatedTime: Long = prefs.getLong(lapAccumulatedTimeKey, 0)
+
+            // Lap time is the delta between accumulated time of this lap and prior lap.
+            val lapTime = accumulatedTime - prevAccumulatedTime
+
+            // Create the lap instance from the data.
+            laps.add(Lap(lapNumber, lapTime, accumulatedTime))
+
+            // Update the accumulated time of the previous lap.
+            prevAccumulatedTime = accumulatedTime
+        }
+
+        // Laps are stored in the order they were recorded; display order is the reverse.
+        laps.reverse()
+
+        return laps
+    }
+
+    /**
+     * @param newLapCount the number of laps including the new lap
+     * @param accumulatedTime the amount of time accumulate by the stopwatch at the end of the lap
+     */
+    fun addLap(prefs: SharedPreferences, newLapCount: Int, accumulatedTime: Long) {
+        prefs.edit()
+                .putInt(LAP_COUNT, newLapCount)
+                .putLong(LAP_ACCUMULATED_TIME + newLapCount, accumulatedTime)
+                .apply()
+    }
+
+    /**
+     * Remove the recorded laps for the stopwatch
+     */
+    fun clearLaps(prefs: SharedPreferences) {
+        val editor: SharedPreferences.Editor = prefs.edit()
+
+        val lapCount: Int = prefs.getInt(LAP_COUNT, 0)
+        for (lapNumber in 1..lapCount) {
+            editor.remove(LAP_ACCUMULATED_TIME + lapNumber)
+        }
+        editor.remove(LAP_COUNT)
+
+        editor.apply()
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchListener.java b/src/com/android/deskclock/data/StopwatchListener.kt
similarity index 79%
rename from src/com/android/deskclock/data/StopwatchListener.java
rename to src/com/android/deskclock/data/StopwatchListener.kt
index 838dfab..334662f 100644
--- a/src/com/android/deskclock/data/StopwatchListener.java
+++ b/src/com/android/deskclock/data/StopwatchListener.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,21 +14,20 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.data;
+package com.android.deskclock.data
 
 /**
  * The interface through which interested parties are notified of changes to the stopwatch or laps.
  */
-public interface StopwatchListener {
-
+interface StopwatchListener {
     /**
      * @param before the stopwatch state before the update
      * @param after the stopwatch state after the update
      */
-    void stopwatchUpdated(Stopwatch before, Stopwatch after);
+    fun stopwatchUpdated(before: Stopwatch, after: Stopwatch)
 
     /**
      * @param lap the lap that was added
      */
-    void lapAdded(Lap lap);
+    fun lapAdded(lap: Lap)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchModel.java b/src/com/android/deskclock/data/StopwatchModel.java
deleted file mode 100644
index b5c93f9..0000000
--- a/src/com/android/deskclock/data/StopwatchModel.java
+++ /dev/null
@@ -1,259 +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.data;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.android.deskclock.R;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * All {@link Stopwatch} data is accessed via this model.
- */
-final class StopwatchModel {
-
-    private final Context mContext;
-
-    private final SharedPreferences mPrefs;
-
-    /** The model from which notification data are fetched. */
-    private final NotificationModel mNotificationModel;
-
-    /** Used to create and destroy system notifications related to the stopwatch. */
-    private final NotificationManagerCompat mNotificationManager;
-
-    /** Update stopwatch notification when locale changes. */
-    @SuppressWarnings("FieldCanBeLocal")
-    private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
-    /** The listeners to notify when the stopwatch or its laps change. */
-    private final List<StopwatchListener> mStopwatchListeners = new ArrayList<>();
-
-    /** Delegate that builds platform-specific stopwatch notifications. */
-    private final StopwatchNotificationBuilder mNotificationBuilder =
-            new StopwatchNotificationBuilder();
-
-    /** The current state of the stopwatch. */
-    private Stopwatch mStopwatch;
-
-    /** A mutable copy of the recorded stopwatch laps. */
-    private List<Lap> mLaps;
-
-    StopwatchModel(Context context, SharedPreferences prefs, NotificationModel notificationModel) {
-        mContext = context;
-        mPrefs = prefs;
-        mNotificationModel = notificationModel;
-        mNotificationManager = NotificationManagerCompat.from(context);
-
-        // Update stopwatch notification when locale changes.
-        final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
-        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
-    }
-
-    /**
-     * @param stopwatchListener to be notified when stopwatch changes or laps are added
-     */
-    void addStopwatchListener(StopwatchListener stopwatchListener) {
-        mStopwatchListeners.add(stopwatchListener);
-    }
-
-    /**
-     * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
-     */
-    void removeStopwatchListener(StopwatchListener stopwatchListener) {
-        mStopwatchListeners.remove(stopwatchListener);
-    }
-
-    /**
-     * @return the current state of the stopwatch
-     */
-    Stopwatch getStopwatch() {
-        if (mStopwatch == null) {
-            mStopwatch = StopwatchDAO.getStopwatch(mPrefs);
-        }
-
-        return mStopwatch;
-    }
-
-    /**
-     * @param stopwatch the new state of the stopwatch
-     */
-    Stopwatch setStopwatch(Stopwatch stopwatch) {
-        final Stopwatch before = getStopwatch();
-        if (before != stopwatch) {
-            StopwatchDAO.setStopwatch(mPrefs, stopwatch);
-            mStopwatch = stopwatch;
-
-            // Refresh the stopwatch notification to reflect the latest stopwatch state.
-            if (!mNotificationModel.isApplicationInForeground()) {
-                updateNotification();
-            }
-
-            // Resetting the stopwatch implicitly clears the recorded laps.
-            if (stopwatch.isReset()) {
-                clearLaps();
-            }
-
-            // Notify listeners of the stopwatch change.
-            for (StopwatchListener stopwatchListener : mStopwatchListeners) {
-                stopwatchListener.stopwatchUpdated(before, stopwatch);
-            }
-        }
-
-        return stopwatch;
-    }
-
-    /**
-     * @return the laps recorded for this stopwatch
-     */
-    List<Lap> getLaps() {
-        return Collections.unmodifiableList(getMutableLaps());
-    }
-
-    /**
-     * @return a newly recorded lap completed now; {@code null} if no more laps can be added
-     */
-    Lap addLap() {
-        if (!mStopwatch.isRunning() || !canAddMoreLaps()) {
-            return null;
-        }
-
-        final long totalTime = getStopwatch().getTotalTime();
-        final List<Lap> laps = getMutableLaps();
-
-        final int lapNumber = laps.size() + 1;
-        StopwatchDAO.addLap(mPrefs, lapNumber, totalTime);
-
-        final long prevAccumulatedTime = laps.isEmpty() ? 0 : laps.get(0).getAccumulatedTime();
-        final long lapTime = totalTime - prevAccumulatedTime;
-
-        final Lap lap = new Lap(lapNumber, lapTime, totalTime);
-        laps.add(0, lap);
-
-        // Refresh the stopwatch notification to reflect the latest stopwatch state.
-        if (!mNotificationModel.isApplicationInForeground()) {
-            updateNotification();
-        }
-
-        // Notify listeners of the new lap.
-        for (StopwatchListener stopwatchListener : mStopwatchListeners) {
-            stopwatchListener.lapAdded(lap);
-        }
-
-        return lap;
-    }
-
-    /**
-     * Clears the laps recorded for this stopwatch.
-     */
-    @VisibleForTesting
-    void clearLaps() {
-        StopwatchDAO.clearLaps(mPrefs);
-        getMutableLaps().clear();
-    }
-
-    /**
-     * @return {@code true} iff more laps can be recorded
-     */
-    boolean canAddMoreLaps() {
-        return getLaps().size() < 98;
-    }
-
-    /**
-     * @return the longest lap time of all recorded laps and the current lap
-     */
-    long getLongestLapTime() {
-        long maxLapTime = 0;
-
-        final List<Lap> laps = getLaps();
-        if (!laps.isEmpty()) {
-            // Compute the maximum lap time across all recorded laps.
-            for (Lap lap : getLaps()) {
-                maxLapTime = Math.max(maxLapTime, lap.getLapTime());
-            }
-
-            // Compare with the maximum lap time for the current lap.
-            final Stopwatch stopwatch = getStopwatch();
-            final long currentLapTime = stopwatch.getTotalTime() - laps.get(0).getAccumulatedTime();
-            maxLapTime = Math.max(maxLapTime, currentLapTime);
-        }
-
-        return maxLapTime;
-    }
-
-    /**
-     * In practice, {@code time} can be any value due to device reboots. When the real-time clock is
-     * reset, there is no more guarantee that this time falls after the last recorded lap.
-     *
-     * @param time a point in time expected, but not required, to be after the end of the prior lap
-     * @return the elapsed time between the given {@code time} and the end of the prior lap;
-     *      negative elapsed times are normalized to {@code 0}
-     */
-    long getCurrentLapTime(long time) {
-        final Lap previousLap = getLaps().get(0);
-        final long currentLapTime = time - previousLap.getAccumulatedTime();
-        return Math.max(0, currentLapTime);
-    }
-
-    /**
-     * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
-     */
-    void updateNotification() {
-        final Stopwatch stopwatch = getStopwatch();
-
-        // Notification should be hidden if the stopwatch has no time or the app is open.
-        if (stopwatch.isReset() || mNotificationModel.isApplicationInForeground()) {
-            mNotificationManager.cancel(mNotificationModel.getStopwatchNotificationId());
-            return;
-        }
-
-        // Otherwise build and post a notification reflecting the latest stopwatch state.
-        final Notification notification =
-                mNotificationBuilder.build(mContext, mNotificationModel, stopwatch);
-        mNotificationBuilder.buildChannel(mContext, mNotificationManager);
-        mNotificationManager.notify(mNotificationModel.getStopwatchNotificationId(), notification);
-    }
-
-    private List<Lap> getMutableLaps() {
-        if (mLaps == null) {
-            mLaps = StopwatchDAO.getLaps(mPrefs);
-        }
-
-        return mLaps;
-    }
-
-    /**
-     * Update the stopwatch notification in response to a locale change.
-     */
-    private final class LocaleChangedReceiver extends BroadcastReceiver {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            updateNotification();
-        }
-    }
-}
diff --git a/src/com/android/deskclock/data/StopwatchModel.kt b/src/com/android/deskclock/data/StopwatchModel.kt
new file mode 100644
index 0000000..c3e022f
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchModel.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationManagerCompat
+
+import kotlin.math.max
+
+/**
+ * All [Stopwatch] data is accessed via this model.
+ */
+internal class StopwatchModel(
+    private val mContext: Context,
+    private val mPrefs: SharedPreferences,
+    /** The model from which notification data are fetched.  */
+    private val mNotificationModel: NotificationModel
+) {
+
+    /** Used to create and destroy system notifications related to the stopwatch.  */
+    private val mNotificationManager = NotificationManagerCompat.from(mContext)
+
+    /** Update stopwatch notification when locale changes.  */
+    private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+    /** The listeners to notify when the stopwatch or its laps change.  */
+    private val mStopwatchListeners: MutableList<StopwatchListener> = mutableListOf()
+
+    /** Delegate that builds platform-specific stopwatch notifications.  */
+    private val mNotificationBuilder = StopwatchNotificationBuilder()
+
+    /** The current state of the stopwatch.  */
+    private var mStopwatch: Stopwatch? = null
+
+    /** A mutable copy of the recorded stopwatch laps.  */
+    private var mLaps: MutableList<Lap>? = null
+
+    init {
+        // Update stopwatch notification when locale changes.
+        val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+    }
+
+    /**
+     * @param stopwatchListener to be notified when stopwatch changes or laps are added
+     */
+    fun addStopwatchListener(stopwatchListener: StopwatchListener) {
+        mStopwatchListeners.add(stopwatchListener)
+    }
+
+    /**
+     * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
+     */
+    fun removeStopwatchListener(stopwatchListener: StopwatchListener) {
+        mStopwatchListeners.remove(stopwatchListener)
+    }
+
+    /**
+     * @return the current state of the stopwatch
+     */
+    val stopwatch: Stopwatch
+        get() {
+            if (mStopwatch == null) {
+                mStopwatch = StopwatchDAO.getStopwatch(mPrefs)
+            }
+
+            return mStopwatch!!
+        }
+
+    /**
+     * @param stopwatch the new state of the stopwatch
+     */
+    fun setStopwatch(stopwatch: Stopwatch): Stopwatch {
+        val before = this.stopwatch
+        if (before != stopwatch) {
+            StopwatchDAO.setStopwatch(mPrefs, stopwatch)
+            mStopwatch = stopwatch
+
+            // Refresh the stopwatch notification to reflect the latest stopwatch state.
+            if (!mNotificationModel.isApplicationInForeground) {
+                updateNotification()
+            }
+
+            // Resetting the stopwatch implicitly clears the recorded laps.
+            if (stopwatch.isReset) {
+                clearLaps()
+            }
+
+            // Notify listeners of the stopwatch change.
+            for (stopwatchListener in mStopwatchListeners) {
+                stopwatchListener.stopwatchUpdated(before, stopwatch)
+            }
+        }
+
+        return stopwatch
+    }
+
+    /**
+     * @return the laps recorded for this stopwatch
+     */
+    val laps: List<Lap>
+        get() = mutableLaps
+
+    /**
+     * @return a newly recorded lap completed now; `null` if no more laps can be added
+     */
+    fun addLap(): Lap? {
+        if (!mStopwatch!!.isRunning || !canAddMoreLaps()) {
+            return null
+        }
+
+        val totalTime = stopwatch.totalTime
+        val laps: MutableList<Lap> = mutableLaps
+
+        val lapNumber = laps.size + 1
+        StopwatchDAO.addLap(mPrefs, lapNumber, totalTime)
+
+        val prevAccumulatedTime = if (laps.isEmpty()) 0 else laps[0].accumulatedTime
+        val lapTime = totalTime - prevAccumulatedTime
+
+        val lap = Lap(lapNumber, lapTime, totalTime)
+        laps.add(0, lap)
+
+        // Refresh the stopwatch notification to reflect the latest stopwatch state.
+        if (!mNotificationModel.isApplicationInForeground) {
+            updateNotification()
+        }
+
+        // Notify listeners of the new lap.
+        for (stopwatchListener in mStopwatchListeners) {
+            stopwatchListener.lapAdded(lap)
+        }
+
+        return lap
+    }
+
+    /**
+     * Clears the laps recorded for this stopwatch.
+     */
+    @VisibleForTesting
+    fun clearLaps() {
+        StopwatchDAO.clearLaps(mPrefs)
+        mutableLaps.clear()
+    }
+
+    /**
+     * @return `true` iff more laps can be recorded
+     */
+    fun canAddMoreLaps(): Boolean = laps.size < 98
+
+    /**
+     * @return the longest lap time of all recorded laps and the current lap
+     */
+    val longestLapTime: Long
+        get() {
+            var maxLapTime: Long = 0
+
+            val laps = laps
+            if (laps.isNotEmpty()) {
+                // Compute the maximum lap time across all recorded laps.
+                for (lap in laps) {
+                    maxLapTime = max(maxLapTime, lap.lapTime)
+                }
+
+                // Compare with the maximum lap time for the current lap.
+                val stopwatch = stopwatch
+                val currentLapTime = stopwatch.totalTime - laps[0].accumulatedTime
+                maxLapTime = max(maxLapTime, currentLapTime)
+            }
+
+            return maxLapTime
+        }
+
+    /**
+     * In practice, `time` can be any value due to device reboots. When the real-time clock is
+     * reset, there is no more guarantee that this time falls after the last recorded lap.
+     *
+     * @param time a point in time expected, but not required, to be after the end of the prior lap
+     * @return the elapsed time between the given `time` and the end of the prior lap;
+     * negative elapsed times are normalized to `0`
+     */
+    fun getCurrentLapTime(time: Long): Long {
+        val previousLap = laps[0]
+        val currentLapTime = time - previousLap.accumulatedTime
+        return max(0, currentLapTime)
+    }
+
+    /**
+     * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
+     */
+    fun updateNotification() {
+        val stopwatch = stopwatch
+
+        // Notification should be hidden if the stopwatch has no time or the app is open.
+        if (stopwatch.isReset || mNotificationModel.isApplicationInForeground) {
+            mNotificationManager.cancel(mNotificationModel.stopwatchNotificationId)
+            return
+        }
+
+        // Otherwise build and post a notification reflecting the latest stopwatch state.
+        val notification: Notification =
+                mNotificationBuilder.build(mContext, mNotificationModel, stopwatch)
+        mNotificationBuilder.buildChannel(mContext, mNotificationManager)
+        mNotificationManager.notify(mNotificationModel.stopwatchNotificationId, notification)
+    }
+
+    private val mutableLaps: MutableList<Lap>
+        get() {
+            if (mLaps == null) {
+                mLaps = StopwatchDAO.getLaps(mPrefs)
+            }
+
+            return mLaps!!
+        }
+
+    /**
+     * Update the stopwatch notification in response to a locale change.
+     */
+    private inner class LocaleChangedReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            updateNotification()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchNotificationBuilder.java b/src/com/android/deskclock/data/StopwatchNotificationBuilder.java
deleted file mode 100644
index d21fe80..0000000
--- a/src/com/android/deskclock/data/StopwatchNotificationBuilder.java
+++ /dev/null
@@ -1,169 +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.NotificationChannel;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.SystemClock;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.StringRes;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Action;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.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 java.util.ArrayList;
-import java.util.List;
-
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-
-/**
- * Builds notification to reflect the latest state of the stopwatch and recorded laps.
- */
-class StopwatchNotificationBuilder {
-
-    /**
-     * Notification channel containing all stopwatch notifications.
-     */
-    private static final String STOPWATCH_NOTIFICATION_CHANNEL_ID = "StopwatchNotification";
-
-    public void buildChannel(Context context, NotificationManagerCompat notificationManager) {
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    STOPWATCH_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            notificationManager.createNotificationChannel(channel);
-        }
-    }
-
-    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 content = new RemoteViews(pname, R.layout.chronometer_notif_content);
-        content.setChronometer(R.id.chronometer, base, null, running);
-
-        final List<Action> actions = new ArrayList<>(2);
-
-        if (running) {
-            // Left button: Pause
-            final Intent pause = new Intent(context, StopwatchService.class)
-                    .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH)
-                    .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
-            @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 Action.Builder(icon1, title1, intent1).build());
-
-            // Right button: Add Lap
-            if (DataModel.getDataModel().canAddMoreLaps()) {
-                final Intent lap = new Intent(context, StopwatchService.class)
-                        .setAction(StopwatchService.ACTION_LAP_STOPWATCH)
-                        .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
-                @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 Action.Builder(icon2, title2, intent2).build());
-            }
-
-            // 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);
-                content.setTextViewText(R.id.state, lap);
-                content.setViewVisibility(R.id.state, VISIBLE);
-            } else {
-                content.setViewVisibility(R.id.state, GONE);
-            }
-        } else {
-            // Left button: Start
-            final Intent start = new Intent(context, StopwatchService.class)
-                    .setAction(StopwatchService.ACTION_START_STOPWATCH)
-                    .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
-            @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 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);
-
-            @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 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);
-        }
-
-        final Builder notification = new NotificationCompat.Builder(
-                context, STOPWATCH_NOTIFICATION_CHANNEL_ID)
-                        .setLocalOnly(true)
-                        .setOngoing(running)
-                        .setCustomContentView(content)
-                        .setContentIntent(pendingShowApp)
-                        .setAutoCancel(stopwatch.isPaused())
-                        .setPriority(Notification.PRIORITY_MAX)
-                        .setSmallIcon(R.drawable.stat_notify_stopwatch)
-                        .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/StopwatchNotificationBuilder.kt b/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt
new file mode 100644
index 0000000..6827da0
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.NotificationChannel
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.SystemClock
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.RemoteViews
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.Action
+import androidx.core.app.NotificationCompat.Builder
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.events.Events
+import com.android.deskclock.stopwatch.StopwatchService
+
+/**
+ * Builds notification to reflect the latest state of the stopwatch and recorded laps.
+ */
+internal class StopwatchNotificationBuilder {
+    fun buildChannel(context: Context, notificationManager: NotificationManagerCompat) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    STOPWATCH_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            notificationManager.createNotificationChannel(channel)
+        }
+    }
+
+    fun build(context: Context, nm: NotificationModel, stopwatch: Stopwatch?): Notification {
+        @StringRes val eventLabel: Int = R.string.label_notification
+
+        // Intent to load the app when the notification is tapped.
+        val showApp: Intent = Intent(context, StopwatchService::class.java)
+                .setAction(StopwatchService.ACTION_SHOW_STOPWATCH)
+                .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+        val pendingShowApp: PendingIntent = PendingIntent.getService(context, 0, showApp,
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+        // Compute some values required below.
+        val running = stopwatch!!.isRunning
+        val pname: String = context.getPackageName()
+        val res: Resources = context.getResources()
+        val base: Long = SystemClock.elapsedRealtime() - stopwatch.totalTime
+
+        val content = RemoteViews(pname, R.layout.chronometer_notif_content)
+        content.setChronometer(R.id.chronometer, base, null, running)
+
+        val actions: MutableList<Action> = ArrayList<Action>(2)
+
+        if (running) {
+            // Left button: Pause
+            val pause: Intent = Intent(context, StopwatchService::class.java)
+                    .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH)
+                    .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+            @DrawableRes val icon1: Int = R.drawable.ic_pause_24dp
+            val title1: CharSequence = res.getText(R.string.sw_pause_button)
+            val intent1: PendingIntent = Utils.pendingServiceIntent(context, pause)
+            actions.add(Action.Builder(icon1, title1, intent1).build())
+
+            // Right button: Add Lap
+            if (DataModel.dataModel.canAddMoreLaps()) {
+                val lap: Intent = Intent(context, StopwatchService::class.java)
+                        .setAction(StopwatchService.ACTION_LAP_STOPWATCH)
+                        .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+                @DrawableRes val icon2: Int = R.drawable.ic_sw_lap_24dp
+                val title2: CharSequence = res.getText(R.string.sw_lap_button)
+                val intent2: PendingIntent = Utils.pendingServiceIntent(context, lap)
+                actions.add(Action.Builder(icon2, title2, intent2).build())
+            }
+
+            // Show the current lap number if any laps have been recorded.
+            val lapCount = DataModel.dataModel.laps.size
+            if (lapCount > 0) {
+                val lapNumber = lapCount + 1
+                val lap: String = res.getString(R.string.sw_notification_lap_number, lapNumber)
+                content.setTextViewText(R.id.state, lap)
+                content.setViewVisibility(R.id.state, VISIBLE)
+            } else {
+                content.setViewVisibility(R.id.state, GONE)
+            }
+        } else {
+            // Left button: Start
+            val start: Intent = Intent(context, StopwatchService::class.java)
+                    .setAction(StopwatchService.ACTION_START_STOPWATCH)
+                    .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+            @DrawableRes val icon1: Int = R.drawable.ic_start_24dp
+            val title1: CharSequence = res.getText(R.string.sw_start_button)
+            val intent1: PendingIntent = Utils.pendingServiceIntent(context, start)
+            actions.add(Action.Builder(icon1, title1, intent1).build())
+
+            // Right button: Reset (dismisses notification and resets stopwatch)
+            val reset: Intent = Intent(context, StopwatchService::class.java)
+                    .setAction(StopwatchService.ACTION_RESET_STOPWATCH)
+                    .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+            @DrawableRes val icon2: Int = R.drawable.ic_reset_24dp
+            val title2: CharSequence = res.getText(R.string.sw_reset_button)
+            val intent2: PendingIntent = Utils.pendingServiceIntent(context, reset)
+            actions.add(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)
+        }
+        val notification: Builder = Builder(
+                context, STOPWATCH_NOTIFICATION_CHANNEL_ID)
+                .setLocalOnly(true)
+                .setOngoing(running)
+                .setCustomContentView(content)
+                .setContentIntent(pendingShowApp)
+                .setAutoCancel(stopwatch.isPaused)
+                .setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
+                .setSmallIcon(R.drawable.stat_notify_stopwatch)
+                .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+        if (Utils.isNOrLater) {
+            notification.setGroup(nm.stopwatchNotificationGroupKey)
+        }
+
+        for (action in actions) {
+            notification.addAction(action)
+        }
+
+        return notification.build()
+    }
+
+    companion object {
+        /**
+         * Notification channel containing all stopwatch notifications.
+         */
+        private const val STOPWATCH_NOTIFICATION_CHANNEL_ID = "StopwatchNotification"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimeModel.java b/src/com/android/deskclock/data/TimeModel.java
deleted file mode 100644
index 5f8b9ad..0000000
--- a/src/com/android/deskclock/data/TimeModel.java
+++ /dev/null
@@ -1,66 +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.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/TimeModel.kt b/src/com/android/deskclock/data/TimeModel.kt
new file mode 100644
index 0000000..28be8a3
--- /dev/null
+++ b/src/com/android/deskclock/data/TimeModel.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.
+ */
+internal class TimeModel(private val mContext: Context) {
+    /**
+     * @return the current time in milliseconds
+     */
+    fun currentTimeMillis(): Long = System.currentTimeMillis()
+
+    /**
+     * @return milliseconds since boot, including time spent in sleep
+     */
+    fun elapsedRealtime(): Long = SystemClock.elapsedRealtime()
+
+    /**
+     * @return `true` if 24 hour time format is selected; `false` otherwise
+     */
+    fun is24HourFormat(): Boolean = DateFormat.is24HourFormat(mContext)
+
+    /**
+     * @return a new Calendar with the [.currentTimeMillis]
+     */
+    val calendar: Calendar
+        get() {
+            val calendar = Calendar.getInstance()
+            calendar.timeInMillis = 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
deleted file mode 100644
index 60153c7..0000000
--- a/src/com/android/deskclock/data/TimeZones.java
+++ /dev/null
@@ -1,63 +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.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/TimeZones.kt b/src/com/android/deskclock/data/TimeZones.kt
new file mode 100644
index 0000000..8a5d73e
--- /dev/null
+++ b/src/com/android/deskclock/data/TimeZones.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.
+ */
+class TimeZones internal constructor(
+    val timeZoneIds: Array<CharSequence>,
+    val timeZoneNames: Array<CharSequence>
+) {
+
+    /**
+     * @param timeZoneId identifies the timezone to locate
+     * @return the timezone name with the `timeZoneId`; `null` if it does not exist
+     */
+    fun getTimeZoneName(timeZoneId: CharSequence?): CharSequence? {
+        for (i in timeZoneIds.indices) {
+            if (TextUtils.equals(timeZoneId, timeZoneIds[i])) {
+                return timeZoneNames[i]
+            }
+        }
+
+        return null
+    }
+
+    /**
+     * @param timeZoneId identifies the timezone to locate
+     * @return `true` iff the timezone with the given id is present
+     */
+    operator fun contains(timeZoneId: String?): Boolean {
+        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
deleted file mode 100644
index 71716a3..0000000
--- a/src/com/android/deskclock/data/Timer.java
+++ /dev/null
@@ -1,433 +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.data;
-
-import android.text.TextUtils;
-
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
-
-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;
-import static com.android.deskclock.Utils.now;
-import static com.android.deskclock.Utils.wallClock;
-import static com.android.deskclock.data.Timer.State.EXPIRED;
-import static com.android.deskclock.data.Timer.State.MISSED;
-import static com.android.deskclock.data.Timer.State.PAUSED;
-import static com.android.deskclock.data.Timer.State.RESET;
-import static com.android.deskclock.data.Timer.State.RUNNING;
-
-/**
- * A read-only domain object representing a countdown timer.
- */
-public final class Timer {
-
-    public enum State {
-        RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4), MISSED(5);
-
-        /** The value assigned to this State in prior releases. */
-        private final int mValue;
-
-        State(int value) {
-            mValue = value;
-        }
-
-        /**
-         * @return the numeric value assigned to this state
-         */
-        public int getValue() {
-            return mValue;
-        }
-
-        /**
-         * @return the state corresponding to the given {@code value}
-         */
-        public static State fromValue(int value) {
-            for (State state : values()) {
-                if (state.getValue() == value) {
-                    return state;
-                }
-            }
-
-            return null;
-        }
-    }
-
-    /** The minimum duration of a timer. */
-    public static final long MIN_LENGTH = SECOND_IN_MILLIS;
-
-    /** 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;
-
-    /** A unique identifier for the timer. */
-    private final int mId;
-
-    /** The current state of the timer. */
-    private final State mState;
-
-    /** The original length of the timer in milliseconds when it was created. */
-    private final long mLength;
-
-    /** The length of the timer in milliseconds including additional time added by the user. */
-    private final long mTotalLength;
-
-    /** The time at which the timer was last started; {@link #UNUSED} when not running. */
-    private final long mLastStartTime;
-
-    /** The time since epoch at which the timer was last started. */
-    private final long mLastStartWallClockTime;
-
-    /** The time at which the timer is scheduled to expire; negative if it is already expired. */
-    private final long mRemainingTime;
-
-    /** A message describing the meaning of the timer. */
-    private final String mLabel;
-
-    /** A flag indicating the timer should be deleted when it is reset. */
-    private final boolean mDeleteAfterUse;
-
-    Timer(int id, State state, long length, long totalLength, long lastStartTime,
-          long lastWallClockTime, long remainingTime, String label, boolean deleteAfterUse) {
-        mId = id;
-        mState = state;
-        mLength = length;
-        mTotalLength = totalLength;
-        mLastStartTime = lastStartTime;
-        mLastStartWallClockTime = lastWallClockTime;
-        mRemainingTime = remainingTime;
-        mLabel = label;
-        mDeleteAfterUse = deleteAfterUse;
-    }
-
-    public int getId() { return mId; }
-    public State getState() { return mState; }
-    public String getLabel() { return mLabel; }
-    public long getLength() { return mLength; }
-    public long getTotalLength() { return mTotalLength; }
-    public boolean getDeleteAfterUse() { return mDeleteAfterUse; }
-    public boolean isReset() { return mState == RESET; }
-    public boolean isRunning() { return mState == RUNNING; }
-    public boolean isPaused() { return mState == PAUSED; }
-    public boolean isExpired() { return mState == EXPIRED; }
-    public boolean isMissed() { return mState == MISSED; }
-
-    /**
-     * @return the amount of remaining time when the timer was last started or paused.
-     */
-    public long getLastRemainingTime() {
-        return mRemainingTime;
-    }
-
-    /**
-     * @return the total amount of time remaining up to this moment; expired and missed timers will
-     *      return a negative amount
-     */
-    public long getRemainingTime() {
-        if (mState == PAUSED || mState == RESET) {
-            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);
-    }
-
-    /**
-     * @return the elapsed realtime at which this timer will or did expire
-     */
-    public long getExpirationTime() {
-        if (mState != RUNNING && mState != EXPIRED && mState != MISSED) {
-            throw new IllegalStateException("cannot compute expiration time in state " + mState);
-        }
-
-        return mLastStartTime + mRemainingTime;
-    }
-
-    /**
-     * @return the wall clock time at which this timer will or did expire
-     */
-    public long getWallClockExpirationTime() {
-        if (mState != RUNNING && mState != EXPIRED && mState != MISSED) {
-            throw new IllegalStateException("cannot compute expiration time in state " + mState);
-        }
-
-        return mLastStartWallClockTime + mRemainingTime;
-    }
-
-    /**
-     *
-     * @return the total amount of time elapsed up to this moment; expired timers will report more
-     *      than the {@link #getTotalLength() total length}
-     */
-    public long getElapsedTime() {
-        return getTotalLength() - getRemainingTime();
-    }
-
-    long getLastStartTime() { return mLastStartTime; }
-    long getLastWallClockTime() { return mLastStartWallClockTime; }
-
-    /**
-     * @return a copy of this timer that is running, expired or missed
-     */
-    Timer start() {
-        if (mState == RUNNING || mState == EXPIRED || mState == MISSED) {
-            return this;
-        }
-
-        return new Timer(mId, RUNNING, mLength, mTotalLength, now(), wallClock(), mRemainingTime,
-                mLabel, mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer that is paused or reset
-     */
-    Timer pause() {
-        if (mState == PAUSED || mState == RESET) {
-            return this;
-        } else if (mState == EXPIRED || mState == MISSED) {
-            return reset();
-        }
-
-        final long remainingTime = getRemainingTime();
-        return new Timer(mId, PAUSED, mLength, mTotalLength, UNUSED, UNUSED, remainingTime, mLabel,
-                mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer that is expired, missed or reset
-     */
-    Timer expire() {
-        if (mState == EXPIRED || mState == RESET || mState == MISSED) {
-            return this;
-        }
-
-        final long remainingTime = Math.min(0L, getRemainingTime());
-        return new Timer(mId, EXPIRED, mLength, 0L, now(), wallClock(), remainingTime, mLabel,
-                mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer that is missed or reset
-     */
-    Timer miss() {
-        if (mState == RESET || mState == MISSED) {
-            return this;
-        }
-
-        final long remainingTime = Math.min(0L, getRemainingTime());
-        return new Timer(mId, MISSED, mLength, 0L, now(), wallClock(), remainingTime, mLabel,
-                mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer that is reset
-     */
-    Timer reset() {
-        if (mState == RESET) {
-            return this;
-        }
-
-        return new Timer(mId, RESET, mLength, mLength, UNUSED, UNUSED, mLength, mLabel,
-                mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer that has its times adjusted after a reboot
-     */
-    Timer updateAfterReboot() {
-        if (mState == RESET || mState == PAUSED) {
-            return this;
-        }
-
-        final long timeSinceBoot = now();
-        final long wallClockTime = wallClock();
-        // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
-        // update the recorded times and proceed with no change in accumulated time.
-        final long delta = Math.max(0, wallClockTime - mLastStartWallClockTime);
-        final long remainingTime = mRemainingTime - delta;
-        return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime,
-                remainingTime, mLabel, mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer that has its times adjusted after time has been set
-     */
-    Timer updateAfterTimeSet() {
-        if (mState == RESET || mState == PAUSED) {
-            return this;
-        }
-
-        final long timeSinceBoot = now();
-        final long wallClockTime = wallClock();
-        final long delta = timeSinceBoot - mLastStartTime;
-        final long remainingTime = mRemainingTime - delta;
-        if (delta < 0) {
-            // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
-            // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
-            // updateAfterReboot() can successfully correct the data at a later time.
-            return this;
-        }
-        return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime,
-                remainingTime, mLabel, mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer with the given {@code label}
-     */
-    Timer setLabel(String label) {
-        if (TextUtils.equals(mLabel, label)) {
-            return this;
-        }
-
-        return new Timer(mId, mState, mLength, mTotalLength, mLastStartTime,
-                mLastStartWallClockTime, mRemainingTime, label, mDeleteAfterUse);
-    }
-
-    /**
-     * @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 <= Timer.MIN_LENGTH) {
-            return this;
-        }
-
-        final long totalLength;
-        final long remainingTime;
-        if (mState == RESET) {
-            totalLength = length;
-            remainingTime = length;
-        } else {
-            totalLength = mTotalLength;
-            remainingTime = mRemainingTime;
-        }
-
-        return new Timer(mId, mState, length, totalLength, mLastStartTime,
-                mLastStartWallClockTime, remainingTime, mLabel, mDeleteAfterUse);
-    }
-
-    /**
-     * @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 change the remaining time of a reset timer.
-        if (mRemainingTime == remainingTime || mState == RESET) {
-            return this;
-        }
-
-        final long delta = remainingTime - mRemainingTime;
-        final long totalLength = mTotalLength + delta;
-
-        final long lastStartTime;
-        final long lastWallClockTime;
-        final State state;
-        if (remainingTime > 0 && (mState == EXPIRED || mState == MISSED)) {
-            state = RUNNING;
-            lastStartTime = now();
-            lastWallClockTime = wallClock();
-        } else {
-            state = mState;
-            lastStartTime = mLastStartTime;
-            lastWallClockTime = mLastStartWallClockTime;
-        }
-
-        return new Timer(mId, state, mLength, totalLength, lastStartTime,
-                lastWallClockTime, remainingTime, mLabel, mDeleteAfterUse);
-    }
-
-    /**
-     * @return a copy of this timer with an additional minute added to the remaining time and total
-     *      length, or this Timer if the minute could not be added
-     */
-    Timer addMinute() {
-        // 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
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-
-        final Timer timer = (Timer) o;
-
-        return mId == timer.mId;
-    }
-
-    @Override
-    public int hashCode() {
-        return mId;
-    }
-
-    /**
-     * Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top.
-     */
-    static Comparator<Timer> ID_COMPARATOR = new Comparator<Timer>() {
-        @Override
-        public int compare(Timer timer1, Timer timer2) {
-            return Integer.compare(timer2.getId(), timer1.getId());
-        }
-    };
-
-    /**
-     * Orders timers by their expected/actual expiration time. The general order is:
-     *
-     * <ol>
-     *     <li>{@link State#MISSED MISSED} timers; ties broken by {@link #getRemainingTime()}</li>
-     *     <li>{@link State#EXPIRED EXPIRED} timers; ties broken by {@link #getRemainingTime()}</li>
-     *     <li>{@link State#RUNNING RUNNING} timers; ties broken by {@link #getRemainingTime()}</li>
-     *     <li>{@link State#PAUSED PAUSED} timers; ties broken by {@link #getRemainingTime()}</li>
-     *     <li>{@link State#RESET RESET} timers; ties broken by {@link #getLength()}</li>
-     * </ol>
-     */
-    static Comparator<Timer> EXPIRY_COMPARATOR = new Comparator<Timer>() {
-
-        private final List<State> stateExpiryOrder = Arrays.asList(MISSED, EXPIRED, RUNNING, PAUSED,
-                RESET);
-
-        @Override
-        public int compare(Timer timer1, Timer timer2) {
-            final int stateIndex1 = stateExpiryOrder.indexOf(timer1.getState());
-            final int stateIndex2 = stateExpiryOrder.indexOf(timer2.getState());
-
-            int order = Integer.compare(stateIndex1, stateIndex2);
-            if (order == 0) {
-                final State state = timer1.getState();
-                if (state == RESET) {
-                    order = Long.compare(timer1.getLength(), timer2.getLength());
-                } else {
-                    order = Long.compare(timer1.getRemainingTime(), timer2.getRemainingTime());
-                }
-            }
-
-            return order;
-        }
-    };
-}
diff --git a/src/com/android/deskclock/data/Timer.kt b/src/com/android/deskclock/data/Timer.kt
new file mode 100644
index 0000000..f19b75a
--- /dev/null
+++ b/src/com/android/deskclock/data/Timer.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+import android.text.format.DateUtils.HOUR_IN_MILLIS
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.SECOND_IN_MILLIS
+
+import com.android.deskclock.Utils
+
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * A read-only domain object representing a countdown timer.
+ */
+class Timer internal constructor(
+    /** A unique identifier for the timer.  */
+    val id: Int,
+    /** The current state of the timer.  */
+    val state: State,
+    /** The original length of the timer in milliseconds when it was created.  */
+    val length: Long,
+    /** The length of the timer in milliseconds including additional time added by the user.  */
+    val totalLength: Long,
+    /** The time at which the timer was last started; [.UNUSED] when not running.  */
+    val lastStartTime: Long,
+    /** The time since epoch at which the timer was last started.  */
+    val lastWallClockTime: Long,
+    /** The time at which the timer is scheduled to expire; negative if it is already expired.  */
+    val lastRemainingTime: Long,
+    /** A message describing the meaning of the timer.  */
+    val label: String?,
+    /** A flag indicating the timer should be deleted when it is reset.  */
+    val deleteAfterUse: Boolean
+) {
+
+    enum class State(
+        /** The value assigned to this State in prior releases.  */
+        val value: Int
+    ) {
+        RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4), MISSED(5);
+
+        companion object {
+            /**
+             * @return the state corresponding to the given `value`
+             */
+            fun fromValue(value: Int): State? {
+                for (state in values()) {
+                    if (state.value == value) {
+                        return state
+                    }
+                }
+                return null
+            }
+        }
+    }
+
+    val isReset: Boolean
+        get() = state == State.RESET
+
+    val isRunning: Boolean
+        get() = state == State.RUNNING
+
+    val isPaused: Boolean
+        get() = state == State.PAUSED
+
+    val isExpired: Boolean
+        get() = state == State.EXPIRED
+
+    val isMissed: Boolean
+        get() = state == State.MISSED
+
+    /**
+     * @return the total amount of time remaining up to this moment; expired and missed timers will
+     * return a negative amount
+     */
+    val remainingTime: Long
+        get() {
+            if (state == State.PAUSED || state == State.RESET) {
+                return lastRemainingTime
+            }
+
+            // 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,
+            val timeSinceStart = Utils.now() - lastStartTime
+            return lastRemainingTime - max(0, timeSinceStart)
+        }
+
+    /**
+     * @return the elapsed realtime at which this timer will or did expire
+     */
+    val expirationTime: Long
+        get() {
+            check(!(state != State.RUNNING && state != State.EXPIRED && state != State.MISSED)) {
+                "cannot compute expiration time in state $state"
+            }
+            return lastStartTime + lastRemainingTime
+        }
+
+    /**
+     * @return the wall clock time at which this timer will or did expire
+     */
+    val wallClockExpirationTime: Long
+        get() {
+            check(!(state != State.RUNNING && state != State.EXPIRED && state != State.MISSED)) {
+                "cannot compute expiration time in state $state"
+            }
+            return lastWallClockTime + lastRemainingTime
+        }
+
+    /**
+     *
+     * @return the total amount of time elapsed up to this moment; expired timers will report more
+     * than the [total length][.getTotalLength]
+     */
+    val elapsedTime: Long
+        get() = totalLength - remainingTime
+
+    /**
+     * @return a copy of this timer that is running, expired or missed
+     */
+    fun start(): Timer {
+        return if (state == State.RUNNING || state == State.EXPIRED || state == State.MISSED) {
+            this
+        } else {
+            Timer(id, State.RUNNING, length, totalLength,
+                    Utils.now(), Utils.wallClock(), lastRemainingTime, label, deleteAfterUse)
+        }
+    }
+
+    /**
+     * @return a copy of this timer that is paused or reset
+     */
+    fun pause(): Timer {
+        if (state == State.PAUSED || state == State.RESET) {
+            return this
+        } else if (state == State.EXPIRED || state == State.MISSED) {
+            return reset()
+        }
+
+        val remainingTime = this.remainingTime
+        return Timer(id, State.PAUSED, length, totalLength, UNUSED, UNUSED, remainingTime, label,
+                deleteAfterUse)
+    }
+
+    /**
+     * @return a copy of this timer that is expired, missed or reset
+     */
+    fun expire(): Timer {
+        if (state == State.EXPIRED || state == State.RESET || state == State.MISSED) {
+            return this
+        }
+
+        val remainingTime = min(0L, lastRemainingTime)
+        return Timer(id, State.EXPIRED, length, 0L, Utils.now(),
+                Utils.wallClock(), remainingTime, label, deleteAfterUse)
+    }
+
+    /**
+     * @return a copy of this timer that is missed or reset
+     */
+    fun miss(): Timer {
+        if (state == State.RESET || state == State.MISSED) {
+            return this
+        }
+
+        val remainingTime = min(0L, lastRemainingTime)
+        return Timer(id, State.MISSED, length, 0L, Utils.now(),
+                Utils.wallClock(), remainingTime, label, deleteAfterUse)
+    }
+
+    /**
+     * @return a copy of this timer that is reset
+     */
+    fun reset(): Timer {
+        return if (state == State.RESET) {
+            this
+        } else {
+            Timer(id, State.RESET, length, length, UNUSED, UNUSED, length, label,
+                    deleteAfterUse)
+        }
+    }
+
+    /**
+     * @return a copy of this timer that has its times adjusted after a reboot
+     */
+    fun updateAfterReboot(): Timer {
+        if (state == State.RESET || state == State.PAUSED) {
+            return this
+        }
+        val timeSinceBoot = Utils.now()
+        val wallClockTime = Utils.wallClock()
+        // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
+        // update the recorded times and proceed with no change in accumulated time.
+        val delta = max(0, wallClockTime - lastWallClockTime)
+        val remainingTime = lastRemainingTime - delta
+        return Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
+                remainingTime, label, deleteAfterUse)
+    }
+
+    /**
+     * @return a copy of this timer that has its times adjusted after time has been set
+     */
+    fun updateAfterTimeSet(): Timer {
+        if (state == State.RESET || state == State.PAUSED) {
+            return this
+        }
+        val timeSinceBoot = Utils.now()
+        val wallClockTime = Utils.wallClock()
+        val delta = timeSinceBoot - lastStartTime
+        val remainingTime = lastRemainingTime - delta
+        return if (delta < 0) {
+            // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
+            // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
+            // updateAfterReboot() can successfully correct the data at a later time.
+            this
+        } else {
+            Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
+                    remainingTime, label, deleteAfterUse)
+        }
+    }
+
+    /**
+     * @return a copy of this timer with the given `label`
+     */
+    fun setLabel(label: String?): Timer {
+        return if (TextUtils.equals(this.label, label)) {
+            this
+        } else {
+            Timer(id, state, length, totalLength, lastStartTime,
+                    lastWallClockTime, lastRemainingTime, label, deleteAfterUse)
+        }
+    }
+
+    /**
+     * @return a copy of this timer with the given `length` or this timer if the length could
+     * not be legally adjusted
+     */
+    fun setLength(length: Long): Timer {
+        if (this.length == length || length <= MIN_LENGTH) {
+            return this
+        }
+
+        val totalLength: Long
+        val remainingTime: Long
+        if (state == State.RESET) {
+            totalLength = length
+            remainingTime = length
+        } else {
+            totalLength = this.totalLength
+            remainingTime = lastRemainingTime
+        }
+
+        return Timer(id, state, length, totalLength, lastStartTime,
+                lastWallClockTime, remainingTime, label, deleteAfterUse)
+    }
+
+    /**
+     * @return a copy of this timer with the given `remainingTime` or this timer if the
+     * remaining time could not be legally adjusted
+     */
+    fun setRemainingTime(remainingTime: Long): Timer {
+        // Do not change the remaining time of a reset timer.
+        if (lastRemainingTime == remainingTime || state == State.RESET) {
+            return this
+        }
+
+        val delta = remainingTime - lastRemainingTime
+        val totalLength = totalLength + delta
+
+        val lastStartTime: Long
+        val lastWallClockTime: Long
+        val state: State?
+        if (remainingTime > 0 && (this.state == State.EXPIRED || this.state == State.MISSED)) {
+            state = State.RUNNING
+            lastStartTime = Utils.now()
+            lastWallClockTime = Utils.wallClock()
+        } else {
+            state = this.state
+            lastStartTime = this.lastStartTime
+            lastWallClockTime = this.lastWallClockTime
+        }
+
+        return Timer(id, state, length, totalLength, lastStartTime,
+                lastWallClockTime, remainingTime, label, deleteAfterUse)
+    }
+
+    /**
+     * @return a copy of this timer with an additional minute added to the remaining time and total
+     * length, or this Timer if the minute could not be added
+     */
+    fun addMinute(): Timer {
+        return if (state == State.EXPIRED || state == State.MISSED) {
+            // Expired and missed timers restart with 60 seconds of remaining time.
+            setRemainingTime(MINUTE_IN_MILLIS)
+        } else {
+            // Otherwise try to add a minute to the remaining time.
+            setRemainingTime(lastRemainingTime + MINUTE_IN_MILLIS)
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || javaClass != other.javaClass) return false
+
+        val timer = other as Timer
+
+        return id == timer.id
+    }
+
+    override fun hashCode(): Int {
+        return id
+    }
+
+    companion object {
+        /** The minimum duration of a timer.  */
+        @JvmField
+        val MIN_LENGTH: Long = SECOND_IN_MILLIS
+
+        /** The maximum duration of a new timer created via the user interface.  */
+        val MAX_LENGTH: Long = 99 * HOUR_IN_MILLIS + 99 * MINUTE_IN_MILLIS + 99 * SECOND_IN_MILLIS
+
+        const val UNUSED = Long.MIN_VALUE
+
+        /**
+         * Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top
+         */
+        @JvmField
+        var ID_COMPARATOR = Comparator<Timer> { timer1, timer2 -> timer2.id.compareTo(timer1.id) }
+
+        /**
+         * Orders timers by their expected/actual expiration time. The general order is:
+         *
+         *  1. [MISSED][State.MISSED] timers; ties broken by [.getRemainingTime]
+         *  2. [EXPIRED][State.EXPIRED] timers; ties broken by [.getRemainingTime]
+         *  3. [RUNNING][State.RUNNING] timers; ties broken by [.getRemainingTime]
+         *  4. [PAUSED][State.PAUSED] timers; ties broken by [.getRemainingTime]
+         *  5. [RESET][State.RESET] timers; ties broken by [.getLength]
+         *
+         */
+        @JvmField
+        var EXPIRY_COMPARATOR: Comparator<Timer> = object : Comparator<Timer> {
+            private val stateExpiryOrder =
+                    listOf(State.MISSED, State.EXPIRED, State.RUNNING, State.PAUSED, State.RESET)
+
+            override fun compare(timer1: Timer, timer2: Timer): Int {
+                val stateIndex1 = stateExpiryOrder.indexOf(timer1.state)
+                val stateIndex2 = stateExpiryOrder.indexOf(timer2.state)
+
+                var order = stateIndex1.compareTo(stateIndex2)
+                if (order == 0) {
+                    val state = timer1.state
+                    order = if (state == State.RESET) {
+                        timer1.length.compareTo(timer2.length)
+                    } else {
+                        timer1.lastRemainingTime.compareTo(timer2.lastRemainingTime)
+                    }
+                }
+
+                return order
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerDAO.java b/src/com/android/deskclock/data/TimerDAO.java
deleted file mode 100644
index 32d36d9..0000000
--- a/src/com/android/deskclock/data/TimerDAO.java
+++ /dev/null
@@ -1,189 +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.data;
-
-import android.content.SharedPreferences;
-
-import com.android.deskclock.data.Timer.State;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import static com.android.deskclock.data.Timer.State.RESET;
-
-/**
- * This class encapsulates the transfer of data between {@link Timer} domain objects and their
- * permanent storage in {@link SharedPreferences}.
- */
-final class TimerDAO {
-
-    /** Key to a preference that stores the set of timer ids. */
-    private static final String TIMER_IDS = "timers_list";
-
-    /** Key to a preference that stores the id to assign to the next timer. */
-    private static final String NEXT_TIMER_ID = "next_timer_id";
-
-    /** Prefix for a key to a preference that stores the state of the timer. */
-    private static final String STATE = "timer_state_";
-
-    /** Prefix for a key to a preference that stores the original timer length at creation. */
-    private static final String LENGTH = "timer_setup_timet_";
-
-    /** Prefix for a key to a preference that stores the total timer length with additions. */
-    private static final String TOTAL_LENGTH = "timer_original_timet_";
-
-    /** Prefix for a key to a preference that stores the last start time of the timer. */
-    private static final String LAST_START_TIME = "timer_start_time_";
-
-    /** Prefix for a key to a preference that stores the epoch time when the timer last started. */
-    private static final String LAST_WALL_CLOCK_TIME = "timer_wall_clock_time_";
-
-    /** Prefix for a key to a preference that stores the remaining time before expiry. */
-    private static final String REMAINING_TIME = "timer_time_left_";
-
-    /** Prefix for a key to a preference that stores the label of the timer. */
-    private static final String LABEL = "timer_label_";
-
-    /** Prefix for a key to a preference that signals the timer should be deleted on first reset. */
-    private static final String DELETE_AFTER_USE = "delete_after_use_";
-
-    private TimerDAO() {}
-
-    /**
-     * @return the timers from permanent storage
-     */
-    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());
-
-        // Build a timer using the data associated with each timer id.
-        for (String timerId : timerIds) {
-            final int id = Integer.parseInt(timerId);
-            final int stateValue = prefs.getInt(STATE + id, RESET.getValue());
-            final State state = State.fromValue(stateValue);
-
-            // Timer state may be null when migrating timers from prior releases which defined a
-            // "deleted" state. Such a state is no longer required.
-            if (state != null) {
-                final long length = prefs.getLong(LENGTH + id, Long.MIN_VALUE);
-                final long totalLength = prefs.getLong(TOTAL_LENGTH + id, Long.MIN_VALUE);
-                final long lastStartTime = prefs.getLong(LAST_START_TIME + id, Timer.UNUSED);
-                final long lastWallClockTime = prefs.getLong(LAST_WALL_CLOCK_TIME + id,
-                        Timer.UNUSED);
-                final long remainingTime = prefs.getLong(REMAINING_TIME + id, totalLength);
-                final String label = prefs.getString(LABEL + id, null);
-                final boolean deleteAfterUse = prefs.getBoolean(DELETE_AFTER_USE + id, false);
-                timers.add(new Timer(id, state, length, totalLength, lastStartTime,
-                        lastWallClockTime, remainingTime, label, deleteAfterUse));
-            }
-        }
-
-        return timers;
-    }
-
-    /**
-     * @param timer the timer to be added
-     */
-    static Timer addTimer(SharedPreferences prefs, Timer timer) {
-        final SharedPreferences.Editor editor = prefs.edit();
-
-        // Fetch the next timer id.
-        final int id = prefs.getInt(NEXT_TIMER_ID, 0);
-        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(prefs));
-        timerIds.add(String.valueOf(id));
-        editor.putStringSet(TIMER_IDS, timerIds);
-
-        // Record the fields of the timer.
-        editor.putInt(STATE + id, timer.getState().getValue());
-        editor.putLong(LENGTH + id, timer.getLength());
-        editor.putLong(TOTAL_LENGTH + id, timer.getTotalLength());
-        editor.putLong(LAST_START_TIME + id, timer.getLastStartTime());
-        editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.getLastWallClockTime());
-        editor.putLong(REMAINING_TIME + id, timer.getRemainingTime());
-        editor.putString(LABEL + id, timer.getLabel());
-        editor.putBoolean(DELETE_AFTER_USE + id, timer.getDeleteAfterUse());
-
-        editor.apply();
-
-        // Return a new timer with the generated timer id present.
-        return new Timer(id, timer.getState(), timer.getLength(), timer.getTotalLength(),
-                timer.getLastStartTime(), timer.getLastWallClockTime(), timer.getRemainingTime(),
-                timer.getLabel(), timer.getDeleteAfterUse());
-    }
-
-    /**
-     * @param timer the timer to be updated
-     */
-    static void updateTimer(SharedPreferences prefs, Timer timer) {
-        final SharedPreferences.Editor editor = prefs.edit();
-
-        // Record the fields of the timer.
-        final int id = timer.getId();
-        editor.putInt(STATE + id, timer.getState().getValue());
-        editor.putLong(LENGTH + id, timer.getLength());
-        editor.putLong(TOTAL_LENGTH + id, timer.getTotalLength());
-        editor.putLong(LAST_START_TIME + id, timer.getLastStartTime());
-        editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.getLastWallClockTime());
-        editor.putLong(REMAINING_TIME + id, timer.getRemainingTime());
-        editor.putString(LABEL + id, timer.getLabel());
-        editor.putBoolean(DELETE_AFTER_USE + id, timer.getDeleteAfterUse());
-
-        editor.apply();
-    }
-
-    /**
-     * @param timer the timer to be removed
-     */
-    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(prefs));
-        timerIds.remove(String.valueOf(id));
-        if (timerIds.isEmpty()) {
-            editor.remove(TIMER_IDS);
-            editor.remove(NEXT_TIMER_ID);
-        } else {
-            editor.putStringSet(TIMER_IDS, timerIds);
-        }
-
-        // Record the fields of the timer.
-        editor.remove(STATE + id);
-        editor.remove(LENGTH + id);
-        editor.remove(TOTAL_LENGTH + id);
-        editor.remove(LAST_START_TIME + id);
-        editor.remove(LAST_WALL_CLOCK_TIME + id);
-        editor.remove(REMAINING_TIME + id);
-        editor.remove(LABEL + id);
-        editor.remove(DELETE_AFTER_USE + id);
-
-        editor.apply();
-    }
-
-    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/TimerDAO.kt b/src/com/android/deskclock/data/TimerDAO.kt
new file mode 100644
index 0000000..137c450
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerDAO.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+/**
+ * This class encapsulates the transfer of data between [Timer] domain objects and their
+ * permanent storage in [SharedPreferences].
+ */
+internal object TimerDAO {
+    /** Key to a preference that stores the set of timer ids.  */
+    private const val TIMER_IDS = "timers_list"
+
+    /** Key to a preference that stores the id to assign to the next timer.  */
+    private const val NEXT_TIMER_ID = "next_timer_id"
+
+    /** Prefix for a key to a preference that stores the state of the timer.  */
+    private const val STATE = "timer_state_"
+
+    /** Prefix for a key to a preference that stores the original timer length at creation.  */
+    private const val LENGTH = "timer_setup_timet_"
+
+    /** Prefix for a key to a preference that stores the total timer length with additions.  */
+    private const val TOTAL_LENGTH = "timer_original_timet_"
+
+    /** Prefix for a key to a preference that stores the last start time of the timer.  */
+    private const val LAST_START_TIME = "timer_start_time_"
+
+    /** Prefix for a key to a preference that stores the epoch time when the timer last started.  */
+    private const val LAST_WALL_CLOCK_TIME = "timer_wall_clock_time_"
+
+    /** Prefix for a key to a preference that stores the remaining time before expiry.  */
+    private const val REMAINING_TIME = "timer_time_left_"
+
+    /** Prefix for a key to a preference that stores the label of the timer.  */
+    private const val LABEL = "timer_label_"
+
+    /** Prefix for a key to a preference that signals the timer should be deleted on first reset. */
+    private const val DELETE_AFTER_USE = "delete_after_use_"
+
+    /**
+     * @return the timers from permanent storage
+     */
+    @JvmStatic
+    fun getTimers(prefs: SharedPreferences): MutableList<Timer> {
+        // Read the set of timer ids.
+        val timerIds: Set<String> = prefs.getStringSet(TIMER_IDS, emptySet<String>())!!
+        val timers: MutableList<Timer> = ArrayList(timerIds.size)
+
+        // Build a timer using the data associated with each timer id.
+        for (timerId in timerIds) {
+            val id = timerId.toInt()
+            val stateValue: Int = prefs.getInt(STATE + id, Timer.State.RESET.value)
+            val state: Timer.State? = Timer.State.fromValue(stateValue)
+
+            // Timer state may be null when migrating timers from prior releases which defined a
+            // "deleted" state. Such a state is no longer required.
+            state?.let {
+                val length: Long = prefs.getLong(LENGTH + id, Long.MIN_VALUE)
+                val totalLength: Long = prefs.getLong(TOTAL_LENGTH + id, Long.MIN_VALUE)
+                val lastStartTime: Long = prefs.getLong(LAST_START_TIME + id, Timer.UNUSED)
+                val lastWallClockTime: Long = prefs.getLong(LAST_WALL_CLOCK_TIME + id, Timer.UNUSED)
+                val remainingTime: Long = prefs.getLong(REMAINING_TIME + id, totalLength)
+                val label: String? = prefs.getString(LABEL + id, null)
+                val deleteAfterUse: Boolean = prefs.getBoolean(DELETE_AFTER_USE + id, false)
+                timers.add(Timer(id, it, length, totalLength, lastStartTime,
+                        lastWallClockTime, remainingTime, label, deleteAfterUse))
+            }
+        }
+
+        return timers
+    }
+
+    /**
+     * @param timer the timer to be added
+     */
+    @JvmStatic
+    fun addTimer(prefs: SharedPreferences, timer: Timer): Timer {
+        val editor: SharedPreferences.Editor = prefs.edit()
+
+        // Fetch the next timer id.
+        val id: Int = prefs.getInt(NEXT_TIMER_ID, 0)
+        editor.putInt(NEXT_TIMER_ID, id + 1)
+
+        // Add the new timer id to the set of all timer ids.
+        val timerIds: MutableSet<String> = HashSet(getTimerIds(prefs))
+        timerIds.add(id.toString())
+        editor.putStringSet(TIMER_IDS, timerIds)
+
+        // Record the fields of the timer.
+        editor.putInt(STATE + id, timer.state.value)
+        editor.putLong(LENGTH + id, timer.length)
+        editor.putLong(TOTAL_LENGTH + id, timer.totalLength)
+        editor.putLong(LAST_START_TIME + id, timer.lastStartTime)
+        editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.lastWallClockTime)
+        editor.putLong(REMAINING_TIME + id, timer.remainingTime)
+        editor.putString(LABEL + id, timer.label)
+        editor.putBoolean(DELETE_AFTER_USE + id, timer.deleteAfterUse)
+
+        editor.apply()
+
+        // Return a new timer with the generated timer id present.
+        return Timer(id, timer.state, timer.length, timer.totalLength,
+                timer.lastStartTime, timer.lastWallClockTime, timer.remainingTime,
+                timer.label, timer.deleteAfterUse)
+    }
+
+    /**
+     * @param timer the timer to be updated
+     */
+    @JvmStatic
+    fun updateTimer(prefs: SharedPreferences, timer: Timer) {
+        val editor: SharedPreferences.Editor = prefs.edit()
+
+        // Record the fields of the timer.
+        val id = timer.id
+        editor.putInt(STATE + id, timer.state.value)
+        editor.putLong(LENGTH + id, timer.length)
+        editor.putLong(TOTAL_LENGTH + id, timer.totalLength)
+        editor.putLong(LAST_START_TIME + id, timer.lastStartTime)
+        editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.lastWallClockTime)
+        editor.putLong(REMAINING_TIME + id, timer.remainingTime)
+        editor.putString(LABEL + id, timer.label)
+        editor.putBoolean(DELETE_AFTER_USE + id, timer.deleteAfterUse)
+
+        editor.apply()
+    }
+
+    /**
+     * @param timer the timer to be removed
+     */
+    @JvmStatic
+    fun removeTimer(prefs: SharedPreferences, timer: Timer) {
+        val editor: SharedPreferences.Editor = prefs.edit()
+        val id = timer.id
+
+        // Remove the timer id from the set of all timer ids.
+        val timerIds: MutableSet<String> = HashSet(getTimerIds(prefs))
+        timerIds.remove(id.toString())
+        if (timerIds.isEmpty()) {
+            editor.remove(TIMER_IDS)
+            editor.remove(NEXT_TIMER_ID)
+        } else {
+            editor.putStringSet(TIMER_IDS, timerIds)
+        }
+
+        // Record the fields of the timer.
+        editor.remove(STATE + id)
+        editor.remove(LENGTH + id)
+        editor.remove(TOTAL_LENGTH + id)
+        editor.remove(LAST_START_TIME + id)
+        editor.remove(LAST_WALL_CLOCK_TIME + id)
+        editor.remove(REMAINING_TIME + id)
+        editor.remove(LABEL + id)
+        editor.remove(DELETE_AFTER_USE + id)
+
+        editor.apply()
+    }
+
+    private fun getTimerIds(prefs: SharedPreferences): Set<String> {
+        return prefs.getStringSet(TIMER_IDS, emptySet<String>())!!
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerListener.java b/src/com/android/deskclock/data/TimerListener.kt
similarity index 78%
rename from src/com/android/deskclock/data/TimerListener.java
rename to src/com/android/deskclock/data/TimerListener.kt
index a2f1d80..9a3d8b0 100644
--- a/src/com/android/deskclock/data/TimerListener.java
+++ b/src/com/android/deskclock/data/TimerListener.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,26 +14,25 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.data;
+package com.android.deskclock.data
 
 /**
  * The interface through which interested parties are notified of changes to one of the timers.
  */
-public interface TimerListener {
-
+interface TimerListener {
     /**
      * @param timer the timer that was added
      */
-    void timerAdded(Timer timer);
+    fun timerAdded(timer: Timer)
 
     /**
      * @param before the timer state before the update
      * @param after the timer state after the update
      */
-    void timerUpdated(Timer before, Timer after);
+    fun timerUpdated(before: Timer, after: Timer)
 
     /**
      * @param timer the timer that was removed
      */
-    void timerRemoved(Timer timer);
+    fun timerRemoved(timer: Timer)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerModel.java b/src/com/android/deskclock/data/TimerModel.java
deleted file mode 100644
index 54dfeab..0000000
--- a/src/com/android/deskclock/data/TimerModel.java
+++ /dev/null
@@ -1,846 +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.data;
-
-import android.annotation.SuppressLint;
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.net.Uri;
-import androidx.annotation.StringRes;
-import androidx.core.app.NotificationManagerCompat;
-import android.util.ArraySet;
-
-import com.android.deskclock.AlarmAlertWakeLock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.settings.SettingsActivity;
-import com.android.deskclock.timer.TimerKlaxon;
-import com.android.deskclock.timer.TimerService;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
-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;
-
-/**
- * All {@link Timer} data is accessed via this model.
- */
-final class TimerModel {
-
-    /**
-     * Running timers less than this threshold are left running/expired; greater than this
-     * threshold are considered missed.
-     */
-    private static final long MISSED_THRESHOLD = -MINUTE_IN_MILLIS;
-
-    private final Context mContext;
-
-    private final SharedPreferences mPrefs;
-
-    /** The alarm manager system service that calls back when timers expire. */
-    private final AlarmManager mAlarmManager;
-
-    /** The model from which settings are fetched. */
-    private final SettingsModel mSettingsModel;
-
-    /** 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;
-
-    /** Update timer notification when locale changes. */
-    @SuppressWarnings("FieldCanBeLocal")
-    private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
-    /**
-     * Retain a hard reference to the shared preference observer to prevent it from being garbage
-     * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
-     */
-    @SuppressWarnings("FieldCanBeLocal")
-    private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
-
-    /** 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
-     * be absent from this collection.
-     */
-    @SuppressLint("NewApi")
-    private final Set<Integer> mRingingIds = new ArraySet<>();
-
-    /** The uri of the ringtone to play for timers. */
-    private Uri mTimerRingtoneUri;
-
-    /** The title of the ringtone to play for timers. */
-    private String mTimerRingtoneTitle;
-
-    /** A mutable copy of the timers. */
-    private List<Timer> mTimers;
-
-    /** A mutable copy of the expired timers. */
-    private List<Timer> mExpiredTimers;
-
-    /** A mutable copy of the missed timers. */
-    private List<Timer> mMissedTimers;
-
-    /**
-     * 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
-     * from killing this application while expired timers are actively firing.
-     */
-    private Service mService;
-
-    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.
-        prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
-
-        // Update timer notification when locale changes.
-        final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
-        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
-    }
-
-    /**
-     * @param timerListener to be notified when timers are added, updated and removed
-     */
-    void addTimerListener(TimerListener timerListener) {
-        mTimerListeners.add(timerListener);
-    }
-
-    /**
-     * @param timerListener to no longer be notified when timers are added, updated and removed
-     */
-    void removeTimerListener(TimerListener timerListener) {
-        mTimerListeners.remove(timerListener);
-    }
-
-    /**
-     * @return all defined timers in their creation order
-     */
-    List<Timer> getTimers() {
-        return Collections.unmodifiableList(getMutableTimers());
-    }
-
-    /**
-     * @return all expired timers in their expiration order
-     */
-    List<Timer> getExpiredTimers() {
-        return Collections.unmodifiableList(getMutableExpiredTimers());
-    }
-
-    /**
-     * @return all missed timers in their expiration order
-     */
-    private List<Timer> getMissedTimers() {
-        return Collections.unmodifiableList(getMutableMissedTimers());
-    }
-
-    /**
-     * @param timerId identifies the timer to return
-     * @return the timer with the given {@code timerId}
-     */
-    Timer getTimer(int timerId) {
-        for (Timer timer : getMutableTimers()) {
-            if (timer.getId() == timerId) {
-                return timer;
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * @return the timer that last expired and is still expired now; {@code null} if no timers are
-     *      expired
-     */
-    Timer getMostRecentExpiredTimer() {
-        final List<Timer> timers = getMutableExpiredTimers();
-        return timers.isEmpty() ? null : timers.get(timers.size() - 1);
-    }
-
-    /**
-     * @param length the length of the timer in milliseconds
-     * @param label describes the purpose of the timer
-     * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
-     * @return the newly added timer
-     */
-    Timer addTimer(long length, String label, boolean deleteAfterUse) {
-        // Create the timer instance.
-        Timer timer = new Timer(-1, RESET, length, length, Timer.UNUSED, Timer.UNUSED, length,
-                label, deleteAfterUse);
-
-        // Add the timer to permanent storage.
-        timer = TimerDAO.addTimer(mPrefs, timer);
-
-        // Add the timer to the cache.
-        getMutableTimers().add(0, timer);
-
-        // Update the timer notification.
-        updateNotification();
-        // Heads-Up notification is unaffected by this change
-
-        // Notify listeners of the change.
-        for (TimerListener timerListener : mTimerListeners) {
-            timerListener.timerAdded(timer);
-        }
-
-        return timer;
-    }
-
-    /**
-     * @param service used to start foreground notifications related to expired timers
-     * @param timer the timer to be expired
-     */
-    void expireTimer(Service service, Timer timer) {
-        if (mService == null) {
-            // If this is the first expired timer, retain the service that will be used to start
-            // the heads-up notification in the foreground.
-            mService = service;
-        } else if (mService != service) {
-            // If this is not the first expired timer, the service should match the one given when
-            // the first timer expired.
-            LogUtils.wtf("Expected TimerServices to be identical");
-        }
-
-        updateTimer(timer.expire());
-    }
-
-    /**
-     * @param timer an updated timer to store
-     */
-    void updateTimer(Timer timer) {
-        final Timer before = doUpdateTimer(timer);
-
-        // Update the notification after updating the timer data.
-        updateNotification();
-
-        // If the timer started or stopped being expired, update the heads-up notification.
-        if (before.getState() != timer.getState()) {
-            if (before.isExpired() || timer.isExpired()) {
-                updateHeadsUpNotification();
-            }
-        }
-    }
-
-    /**
-     * @param timer an existing timer to be removed
-     */
-    void removeTimer(Timer timer) {
-        doRemoveTimer(timer);
-
-        // Update the timer notifications after removing the timer data.
-        if (timer.isExpired()) {
-            updateHeadsUpNotification();
-        } else {
-            updateNotification();
-        }
-    }
-
-    /**
-     * If the given {@code timer} is expired and marked for deletion after use then this method
-     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
-     * to exist.
-     *
-     * @param timer        the timer to be reset
-     * @param allowDelete  {@code true} if the timer is allowed to be deleted instead of reset
-     *                     (e.g. one use timers)
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     * @return the reset {@code timer} or {@code null} if the timer was deleted
-     */
-    Timer resetTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId) {
-        final Timer result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId);
-
-        // Update the notification after updating the timer data.
-        if (timer.isMissed()) {
-            updateMissedNotification();
-        } else if (timer.isExpired()) {
-            updateHeadsUpNotification();
-        } else {
-            updateNotification();
-        }
-
-        return result;
-    }
-
-    /**
-     * Update timers after system reboot.
-     */
-    void updateTimersAfterReboot() {
-        final List<Timer> timers = new ArrayList<>(getTimers());
-        for (Timer timer : timers) {
-            doUpdateAfterRebootTimer(timer);
-        }
-
-        // Update the notifications once after all timers are updated.
-        updateNotification();
-        updateMissedNotification();
-        updateHeadsUpNotification();
-    }
-
-    /**
-     * Update timers after time set.
-     */
-    void updateTimersAfterTimeSet() {
-        final List<Timer> timers = new ArrayList<>(getTimers());
-        for (Timer timer : timers) {
-            doUpdateAfterTimeSetTimer(timer);
-        }
-
-        // Update the notifications once after all timers are updated.
-        updateNotification();
-        updateMissedNotification();
-        updateHeadsUpNotification();
-    }
-
-    /**
-     * Reset all expired timers. Exactly one parameter should be filled, with preference given to
-     * eventLabelId.
-     *
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     */
-    void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) {
-        final List<Timer> timers = new ArrayList<>(getTimers());
-        for (Timer timer : timers) {
-            if (timer.isExpired()) {
-                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
-            }
-        }
-
-        // Update the notifications once after all timers are updated.
-        updateHeadsUpNotification();
-    }
-
-    /**
-     * Reset all missed timers.
-     *
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     */
-    void resetMissedTimers(@StringRes int eventLabelId) {
-        final List<Timer> timers = new ArrayList<>(getTimers());
-        for (Timer timer : timers) {
-            if (timer.isMissed()) {
-                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
-            }
-        }
-
-        // Update the notifications once after all timers are updated.
-        updateMissedNotification();
-    }
-
-    /**
-     * Reset all unexpired timers.
-     *
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     */
-    void resetUnexpiredTimers(@StringRes int eventLabelId) {
-        final List<Timer> timers = new ArrayList<>(getTimers());
-        for (Timer timer : timers) {
-            if (timer.isRunning() || timer.isPaused()) {
-                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
-            }
-        }
-
-        // Update the notification once after all timers are updated.
-        updateNotification();
-        // Heads-Up notification is unaffected by this change
-    }
-
-    /**
-     * @return the uri of the default ringtone to play for all timers when no user selection exists
-     */
-    Uri getDefaultTimerRingtoneUri() {
-        return mSettingsModel.getDefaultTimerRingtoneUri();
-    }
-
-    /**
-     * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
-     */
-    boolean isTimerRingtoneSilent() {
-        return Uri.EMPTY.equals(getTimerRingtoneUri());
-    }
-
-    /**
-     * @return the uri of the ringtone to play for all timers
-     */
-    Uri getTimerRingtoneUri() {
-        if (mTimerRingtoneUri == null) {
-            mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri();
-        }
-
-        return mTimerRingtoneUri;
-    }
-
-    /**
-     * @param uri the uri of the ringtone to play for all timers
-     */
-    void setTimerRingtoneUri(Uri uri) {
-        mSettingsModel.setTimerRingtoneUri(uri);
-    }
-
-    /**
-     * @return the title of the ringtone that is played for all timers
-     */
-    String getTimerRingtoneTitle() {
-        if (mTimerRingtoneTitle == null) {
-            if (isTimerRingtoneSilent()) {
-                // Special case: no ringtone has a title of "Silent".
-                mTimerRingtoneTitle = mContext.getString(R.string.silent_ringtone_title);
-            } else {
-                final Uri defaultUri = getDefaultTimerRingtoneUri();
-                final Uri uri = getTimerRingtoneUri();
-
-                if (defaultUri.equals(uri)) {
-                    // Special case: default ringtone has a title of "Timer Expired".
-                    mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title);
-                } else {
-                    mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri);
-                }
-            }
-        }
-
-        return mTimerRingtoneTitle;
-    }
-
-    /**
-     * @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 {@code true} if the device should vibrate when timers expire
-     */
-    void setTimerVibrate(boolean enabled) {
-        mSettingsModel.setTimerVibrate(enabled);
-    }
-
-    private List<Timer> getMutableTimers() {
-        if (mTimers == null) {
-            mTimers = TimerDAO.getTimers(mPrefs);
-            Collections.sort(mTimers, Timer.ID_COMPARATOR);
-        }
-
-        return mTimers;
-    }
-
-    private List<Timer> getMutableExpiredTimers() {
-        if (mExpiredTimers == null) {
-            mExpiredTimers = new ArrayList<>();
-
-            for (Timer timer : getMutableTimers()) {
-                if (timer.isExpired()) {
-                    mExpiredTimers.add(timer);
-                }
-            }
-            Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR);
-        }
-
-        return mExpiredTimers;
-    }
-
-    private List<Timer> getMutableMissedTimers() {
-        if (mMissedTimers == null) {
-            mMissedTimers = new ArrayList<>();
-
-            for (Timer timer : getMutableTimers()) {
-                if (timer.isMissed()) {
-                    mMissedTimers.add(timer);
-                }
-            }
-            Collections.sort(mMissedTimers, Timer.EXPIRY_COMPARATOR);
-        }
-
-        return mMissedTimers;
-    }
-
-    /**
-     * This method updates timer data without updating notifications. This is useful in bulk-update
-     * scenarios so the notifications are only rebuilt once.
-     *
-     * @param timer an updated timer to store
-     * @return the state of the timer prior to the update
-     */
-    private Timer doUpdateTimer(Timer timer) {
-        // Retrieve the cached form of the timer.
-        final List<Timer> timers = getMutableTimers();
-        final int index = timers.indexOf(timer);
-        final Timer before = timers.get(index);
-
-        // If no change occurred, ignore this update.
-        if (timer == before) {
-            return timer;
-        }
-
-        // Update the timer in permanent storage.
-        TimerDAO.updateTimer(mPrefs, timer);
-
-        // Update the timer in the cache.
-        final Timer oldTimer = timers.set(index, timer);
-
-        // Clear the cache of expired timers if the timer changed to/from expired.
-        if (before.isExpired() || timer.isExpired()) {
-            mExpiredTimers = null;
-        }
-        // Clear the cache of missed timers if the timer changed to/from missed.
-        if (before.isMissed() || timer.isMissed()) {
-            mMissedTimers = null;
-        }
-
-        // Update the timer expiration callback.
-        updateAlarmManager();
-
-        // Update the timer ringer.
-        updateRinger(before, timer);
-
-        // Notify listeners of the change.
-        for (TimerListener timerListener : mTimerListeners) {
-            timerListener.timerUpdated(before, timer);
-        }
-
-        return oldTimer;
-    }
-
-    /**
-     * This method removes timer data without updating notifications. This is useful in bulk-remove
-     * scenarios so the notifications are only rebuilt once.
-     *
-     * @param timer an existing timer to be removed
-     */
-    private void doRemoveTimer(Timer timer) {
-        // Remove the timer from permanent storage.
-        TimerDAO.removeTimer(mPrefs, timer);
-
-        // Remove the timer from the cache.
-        final List<Timer> timers = getMutableTimers();
-        final int index = timers.indexOf(timer);
-
-        // If the timer cannot be located there is nothing to remove.
-        if (index == -1) {
-            return;
-        }
-
-        timer = timers.remove(index);
-
-        // Clear the cache of expired timers if a new expired timer was added.
-        if (timer.isExpired()) {
-            mExpiredTimers = null;
-        }
-
-        // Clear the cache of missed timers if a new missed timer was added.
-        if (timer.isMissed()) {
-            mMissedTimers = null;
-        }
-
-        // Update the timer expiration callback.
-        updateAlarmManager();
-
-        // Update the timer ringer.
-        updateRinger(timer, null);
-
-        // Notify listeners of the change.
-        for (TimerListener timerListener : mTimerListeners) {
-            timerListener.timerRemoved(timer);
-        }
-    }
-
-    /**
-     * This method updates/removes timer data without updating notifications. This is useful in
-     * bulk-update scenarios so the notifications are only rebuilt once.
-     *
-     * If the given {@code timer} is expired and marked for deletion after use then this method
-     * removes the the timer. The timer is otherwise transitioned to the reset state and continues
-     * to exist.
-     *
-     * @param timer the timer to be reset
-     * @param allowDelete  {@code true} if the timer is allowed to be deleted instead of reset
-     *                     (e.g. one use timers)
-     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
-     * @return the reset {@code timer} or {@code null} if the timer was deleted
-     */
-    private Timer doResetOrDeleteTimer(Timer timer, boolean allowDelete,
-            @StringRes int eventLabelId) {
-        if (allowDelete
-                && (timer.isExpired() || timer.isMissed())
-                && timer.getDeleteAfterUse()) {
-            doRemoveTimer(timer);
-            if (eventLabelId != 0) {
-                Events.sendTimerEvent(R.string.action_delete, eventLabelId);
-            }
-            return null;
-        } else if (!timer.isReset()) {
-            final Timer reset = timer.reset();
-            doUpdateTimer(reset);
-            if (eventLabelId != 0) {
-                Events.sendTimerEvent(R.string.action_reset, eventLabelId);
-            }
-            return reset;
-        }
-
-        return timer;
-    }
-
-    /**
-     * This method updates/removes timer data after a reboot without updating notifications.
-     *
-     * @param timer the timer to be updated
-     */
-    private void doUpdateAfterRebootTimer(Timer timer) {
-        Timer updated = timer.updateAfterReboot();
-        if (updated.getRemainingTime() < MISSED_THRESHOLD && updated.isRunning()) {
-            updated = updated.miss();
-        }
-        doUpdateTimer(updated);
-    }
-
-    private void doUpdateAfterTimeSetTimer(Timer timer) {
-        final Timer updated = timer.updateAfterTimeSet();
-        doUpdateTimer(updated);
-    }
-
-
-    /**
-     * Updates the callback given to this application from the {@link AlarmManager} that signals the
-     * expiration of the next timer. If no timers are currently set to expire (i.e. no running
-     * timers exist) then this method clears the expiration callback from AlarmManager.
-     */
-    private void updateAlarmManager() {
-        // Locate the next firing timer if one exists.
-        Timer nextExpiringTimer = null;
-        for (Timer timer : getMutableTimers()) {
-            if (timer.isRunning()) {
-                if (nextExpiringTimer == null) {
-                    nextExpiringTimer = timer;
-                } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) {
-                    nextExpiringTimer = timer;
-                }
-            }
-        }
-
-        // Build the intent that signals the timer expiration.
-        final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer);
-
-        if (nextExpiringTimer == null) {
-            // Cancel the existing timer expiration callback.
-            final PendingIntent pi = PendingIntent.getService(mContext,
-                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
-            if (pi != null) {
-                mAlarmManager.cancel(pi);
-                pi.cancel();
-            }
-        } else {
-            // Update the existing timer expiration callback.
-            final PendingIntent pi = PendingIntent.getService(mContext,
-                    0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-            schedulePendingIntent(mAlarmManager, nextExpiringTimer.getExpirationTime(), pi);
-        }
-    }
-
-    /**
-     * Starts and stops the ringer for timers if the change to the timer demands it.
-     *
-     * @param before the state of the timer before the change; {@code null} indicates added
-     * @param after the state of the timer after the change; {@code null} indicates delete
-     */
-    private void updateRinger(Timer before, Timer after) {
-        // Retrieve the states before and after the change.
-        final Timer.State beforeState = before == null ? null : before.getState();
-        final Timer.State afterState = after == null ? null : after.getState();
-
-        // If the timer state did not change, the ringer state is unchanged.
-        if (beforeState == afterState) {
-            return;
-        }
-
-        // If the timer is the first to expire, start ringing.
-        if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) {
-            AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext);
-            TimerKlaxon.start(mContext);
-        }
-
-        // If the expired timer was the last to reset, stop ringing.
-        if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) {
-            TimerKlaxon.stop(mContext);
-            AlarmAlertWakeLock.releaseCpuLock();
-        }
-    }
-
-    /**
-     * Updates the notification controlling unexpired timers. This notification is only displayed
-     * when the application is not open.
-     */
-    void updateNotification() {
-        // Notifications should be hidden if the app is open.
-        if (mNotificationModel.isApplicationInForeground()) {
-            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
-            return;
-        }
-
-        // Filter the timers to just include unexpired ones.
-        final List<Timer> unexpired = new ArrayList<>();
-        for (Timer timer : getMutableTimers()) {
-            if (timer.isRunning() || timer.isPaused()) {
-                unexpired.add(timer);
-            }
-        }
-
-        // If no unexpired timers exist, cancel the notification.
-        if (unexpired.isEmpty()) {
-            mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
-            return;
-        }
-
-        // Sort the unexpired timers to locate the next one scheduled to expire.
-        Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR);
-
-        // Otherwise build and post a notification reflecting the latest unexpired timers.
-        final Notification notification =
-                mNotificationBuilder.build(mContext, mNotificationModel, unexpired);
-        final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
-        mNotificationBuilder.buildChannel(mContext, mNotificationManager);
-        mNotificationManager.notify(notificationId, notification);
-    }
-
-    /**
-     * Updates the notification controlling missed timers. This notification is only displayed when
-     * the application is not open.
-     */
-    void updateMissedNotification() {
-        // Notifications should be hidden if the app is open.
-        if (mNotificationModel.isApplicationInForeground()) {
-            mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
-            return;
-        }
-
-        final List<Timer> missed = getMissedTimers();
-
-        if (missed.isEmpty()) {
-            mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
-            return;
-        }
-
-        final Notification notification = mNotificationBuilder.buildMissed(mContext,
-                mNotificationModel, missed);
-        final int notificationId = mNotificationModel.getMissedTimerNotificationId();
-        mNotificationManager.notify(notificationId, notification);
-    }
-
-    /**
-     * Updates the heads-up notification controlling expired timers. This heads-up notification is
-     * displayed whether the application is open or not.
-     */
-    private void updateHeadsUpNotification() {
-        // Nothing can be done with the heads-up notification without a valid service reference.
-        if (mService == null) {
-            return;
-        }
-
-        final List<Timer> expired = getExpiredTimers();
-
-        // If no expired timers exist, stop the service (which cancels the foreground notification).
-        if (expired.isEmpty()) {
-            mService.stopSelf();
-            mService = null;
-            return;
-        }
-
-        // Otherwise build and post a foreground notification reflecting the latest expired timers.
-        final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired);
-        final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
-        mService.startForeground(notificationId, notification);
-    }
-
-    /**
-     * 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();
-        }
-    }
-
-    /**
-     * This receiver is notified when shared preferences change. Cached information built on
-     * preferences must be cleared.
-     */
-    private final class PreferenceListener implements OnSharedPreferenceChangeListener {
-        @Override
-        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
-            switch (key) {
-                case SettingsActivity.KEY_TIMER_RINGTONE:
-                    mTimerRingtoneUri = null;
-                    mTimerRingtoneTitle = null;
-                    break;
-            }
-        }
-    }
-
-    static void schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi) {
-        if (Utils.isMOrLater()) {
-            // Ensure the timer fires even if the device is dozing.
-            am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
-        } else {
-            am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/data/TimerModel.kt b/src/com/android/deskclock/data/TimerModel.kt
new file mode 100644
index 0000000..08f6fb1
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerModel.kt
@@ -0,0 +1,809 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.AlarmManager
+import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.net.Uri
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationManagerCompat
+
+import com.android.deskclock.AlarmAlertWakeLock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.events.Events
+import com.android.deskclock.settings.SettingsActivity
+import com.android.deskclock.timer.TimerKlaxon
+import com.android.deskclock.timer.TimerService
+
+/**
+ * All [Timer] data is accessed via this model.
+ */
+internal class TimerModel(
+    private val mContext: Context,
+    private val mPrefs: SharedPreferences,
+    /** The model from which settings are fetched.  */
+    private val mSettingsModel: SettingsModel,
+    /** The model from which ringtone data are fetched.  */
+    private val mRingtoneModel: RingtoneModel,
+    /** The model from which notification data are fetched.  */
+    private val mNotificationModel: NotificationModel
+) {
+    /** The alarm manager system service that calls back when timers expire.  */
+    private val mAlarmManager = mContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+    /** Used to create and destroy system notifications related to timers.  */
+    private val mNotificationManager = NotificationManagerCompat.from(mContext)
+
+    /** Update timer notification when locale changes.  */
+    private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+    /**
+     * Retain a hard reference to the shared preference observer to prevent it from being garbage
+     * collected. See [SharedPreferences.registerOnSharedPreferenceChangeListener] for detail.
+     */
+    private val mPreferenceListener: OnSharedPreferenceChangeListener = PreferenceListener()
+
+    /** The listeners to notify when a timer is added, updated or removed.  */
+    private val mTimerListeners: MutableList<TimerListener> = mutableListOf()
+
+    /** Delegate that builds platform-specific timer notifications.  */
+    private val mNotificationBuilder = 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
+     * be absent from this collection.
+     */
+    @SuppressLint("NewApi")
+    private val mRingingIds: MutableSet<Int> = mutableSetOf()
+
+    /** The uri of the ringtone to play for timers.  */
+    private var mTimerRingtoneUri: Uri? = null
+
+    /** The title of the ringtone to play for timers.  */
+    private var mTimerRingtoneTitle: String? = null
+
+    /** A mutable copy of the timers.  */
+    private var mTimers: MutableList<Timer>? = null
+
+    /** A mutable copy of the expired timers.  */
+    private var mExpiredTimers: MutableList<Timer>? = null
+
+    /** A mutable copy of the missed timers.  */
+    private var mMissedTimers: MutableList<Timer>? = null
+
+    /**
+     * 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
+     * from killing this application while expired timers are actively firing.
+     */
+    private var mService: Service? = null
+
+    init {
+        // Clear caches affected by preferences when preferences change.
+        mPrefs.registerOnSharedPreferenceChangeListener(mPreferenceListener)
+
+        // Update timer notification when locale changes.
+        val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+        mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+    }
+
+    /**
+     * @param timerListener to be notified when timers are added, updated and removed
+     */
+    fun addTimerListener(timerListener: TimerListener) {
+        mTimerListeners.add(timerListener)
+    }
+
+    /**
+     * @param timerListener to no longer be notified when timers are added, updated and removed
+     */
+    fun removeTimerListener(timerListener: TimerListener) {
+        mTimerListeners.remove(timerListener)
+    }
+
+    /**
+     * @return all defined timers in their creation order
+     */
+    val timers: List<Timer>
+        get() = mutableTimers
+
+    /**
+     * @return all expired timers in their expiration order
+     */
+    val expiredTimers: List<Timer>
+        get() = mutableExpiredTimers
+
+    /**
+     * @return all missed timers in their expiration order
+     */
+    private val missedTimers: List<Timer>
+        get() = mutableMissedTimers
+
+    /**
+     * @param timerId identifies the timer to return
+     * @return the timer with the given `timerId`
+     */
+    fun getTimer(timerId: Int): Timer? {
+        for (timer in mutableTimers) {
+            if (timer.id == timerId) {
+                return timer
+            }
+        }
+
+        return null
+    }
+
+    /**
+     * @return the timer that last expired and is still expired now; `null` if no timers are
+     * expired
+     */
+    val mostRecentExpiredTimer: Timer?
+        get() {
+            val timers = mutableExpiredTimers
+            return if (timers.isEmpty()) null else timers[timers.size - 1]
+        }
+
+    /**
+     * @param length the length of the timer in milliseconds
+     * @param label describes the purpose of the timer
+     * @param deleteAfterUse `true` indicates the timer should be deleted when it is reset
+     * @return the newly added timer
+     */
+    fun addTimer(length: Long, label: String?, deleteAfterUse: Boolean): Timer {
+        // Create the timer instance.
+        var timer =
+                Timer(-1, Timer.State.RESET, length, length, Timer.UNUSED, Timer.UNUSED, length,
+                label, deleteAfterUse)
+
+        // Add the timer to permanent storage.
+        timer = TimerDAO.addTimer(mPrefs, timer)
+
+        // Add the timer to the cache.
+        mutableTimers.add(0, timer)
+
+        // Update the timer notification.
+        updateNotification()
+        // Heads-Up notification is unaffected by this change
+
+        // Notify listeners of the change.
+        for (timerListener in mTimerListeners) {
+            timerListener.timerAdded(timer)
+        }
+
+        return timer
+    }
+
+    /**
+     * @param service used to start foreground notifications related to expired timers
+     * @param timer the timer to be expired
+     */
+    fun expireTimer(service: Service?, timer: Timer) {
+        if (mService == null) {
+            // If this is the first expired timer, retain the service that will be used to start
+            // the heads-up notification in the foreground.
+            mService = service
+        } else if (mService != service) {
+            // If this is not the first expired timer, the service should match the one given when
+            // the first timer expired.
+            LogUtils.wtf("Expected TimerServices to be identical")
+        }
+
+        updateTimer(timer.expire())
+    }
+
+    /**
+     * @param timer an updated timer to store
+     */
+    fun updateTimer(timer: Timer) {
+        val before = doUpdateTimer(timer)
+
+        // Update the notification after updating the timer data.
+        updateNotification()
+
+        // If the timer started or stopped being expired, update the heads-up notification.
+        if (before.state != timer.state) {
+            if (before.isExpired || timer.isExpired) {
+                updateHeadsUpNotification()
+            }
+        }
+    }
+
+    /**
+     * @param timer an existing timer to be removed
+     */
+    fun removeTimer(timer: Timer) {
+        doRemoveTimer(timer)
+
+        // Update the timer notifications after removing the timer data.
+        if (timer.isExpired) {
+            updateHeadsUpNotification()
+        } else {
+            updateNotification()
+        }
+    }
+
+    /**
+     * If the given `timer` is expired and marked for deletion after use then this method
+     * removes the timer. The timer is otherwise transitioned to the reset state and continues
+     * to exist.
+     *
+     * @param timer the timer to be reset
+     * @param allowDelete `true` if the timer is allowed to be deleted instead of reset
+     * (e.g. one use timers)
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     * @return the reset `timer` or `null` if the timer was deleted
+     */
+    fun resetTimer(timer: Timer, allowDelete: Boolean, @StringRes eventLabelId: Int): Timer? {
+        val result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId)
+
+        // Update the notification after updating the timer data.
+        when {
+            timer.isMissed -> updateMissedNotification()
+            timer.isExpired -> updateHeadsUpNotification()
+            else -> updateNotification()
+        }
+
+        return result
+    }
+
+    /**
+     * Update timers after system reboot.
+     */
+    fun updateTimersAfterReboot() {
+        for (timer in timers) {
+            doUpdateAfterRebootTimer(timer)
+        }
+
+        // Update the notifications once after all timers are updated.
+        updateNotification()
+        updateMissedNotification()
+        updateHeadsUpNotification()
+    }
+
+    /**
+     * Update timers after time set.
+     */
+    fun updateTimersAfterTimeSet() {
+        for (timer in timers) {
+            doUpdateAfterTimeSetTimer(timer)
+        }
+
+        // Update the notifications once after all timers are updated.
+        updateNotification()
+        updateMissedNotification()
+        updateHeadsUpNotification()
+    }
+
+    /**
+     * Reset all expired timers. Exactly one parameter should be filled, with preference given to
+     * eventLabelId.
+     *
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     */
+    fun resetOrDeleteExpiredTimers(@StringRes eventLabelId: Int) {
+        for (timer in timers) {
+            if (timer.isExpired) {
+                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId)
+            }
+        }
+
+        // Update the notifications once after all timers are updated.
+        updateHeadsUpNotification()
+    }
+
+    /**
+     * Reset all missed timers.
+     *
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     */
+    fun resetMissedTimers(@StringRes eventLabelId: Int) {
+        for (timer in timers) {
+            if (timer.isMissed) {
+                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId)
+            }
+        }
+
+        // Update the notifications once after all timers are updated.
+        updateMissedNotification()
+    }
+
+    /**
+     * Reset all unexpired timers.
+     *
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     */
+    fun resetUnexpiredTimers(@StringRes eventLabelId: Int) {
+        for (timer in timers) {
+            if (timer.isRunning || timer.isPaused) {
+                doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId)
+            }
+        }
+
+        // Update the notification once after all timers are updated.
+        updateNotification()
+        // Heads-Up notification is unaffected by this change
+    }
+
+    /**
+     * @return the uri of the default ringtone to play for all timers when no user selection exists
+     */
+    val defaultTimerRingtoneUri: Uri
+        get() = mSettingsModel.defaultTimerRingtoneUri
+
+    /**
+     * @return `true` iff the ringtone to play for all timers is the silent ringtone
+     */
+    val isTimerRingtoneSilent: Boolean
+        get() = Uri.EMPTY.equals(timerRingtoneUri)
+
+    var timerRingtoneUri: Uri
+        /**
+         * @return the uri of the ringtone to play for all timers
+         */
+        get() {
+            if (mTimerRingtoneUri == null) {
+                mTimerRingtoneUri = mSettingsModel.timerRingtoneUri
+            }
+
+            return mTimerRingtoneUri!!
+        }
+        /**
+         * @param uri the uri of the ringtone to play for all timers
+         */
+        set(uri) {
+            mSettingsModel.timerRingtoneUri = uri
+        }
+
+    /**
+     * @return the title of the ringtone that is played for all timers
+     */
+    val timerRingtoneTitle: String
+        get() {
+            if (mTimerRingtoneTitle == null) {
+                mTimerRingtoneTitle = if (isTimerRingtoneSilent) {
+                    // Special case: no ringtone has a title of "Silent".
+                    mContext.getString(R.string.silent_ringtone_title)
+                } else {
+                    val defaultUri: Uri = defaultTimerRingtoneUri
+                    val uri: Uri = timerRingtoneUri
+                    if (defaultUri.equals(uri)) {
+                        // Special case: default ringtone has a title of "Timer Expired".
+                        mContext.getString(R.string.default_timer_ringtone_title)
+                    } else {
+                        mRingtoneModel.getRingtoneTitle(uri)
+                    }
+                }
+            }
+
+            return mTimerRingtoneTitle!!
+        }
+
+    /**
+     * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+     * `0` implies no crescendo should be applied
+     */
+    val timerCrescendoDuration: Long
+        get() = mSettingsModel.timerCrescendoDuration
+
+    var timerVibrate: Boolean
+        /**
+         * @return `true` if the device vibrates when timers expire
+         */
+        get() = mSettingsModel.timerVibrate
+        /**
+         * @param enabled `true` if the device should vibrate when timers expire
+         */
+        set(enabled) {
+            mSettingsModel.timerVibrate = enabled
+        }
+
+    private val mutableTimers: MutableList<Timer>
+        get() {
+            if (mTimers == null) {
+                mTimers = TimerDAO.getTimers(mPrefs)
+                mTimers!!.sortWith(Timer.ID_COMPARATOR)
+            }
+
+            return mTimers!!
+        }
+
+    private val mutableExpiredTimers: List<Timer>
+        get() {
+            if (mExpiredTimers == null) {
+                mExpiredTimers = mutableListOf()
+                for (timer in mutableTimers) {
+                    if (timer.isExpired) {
+                        mExpiredTimers!!.add(timer)
+                    }
+                }
+                mExpiredTimers!!.sortWith(Timer.EXPIRY_COMPARATOR)
+            }
+
+            return mExpiredTimers!!
+        }
+
+    private val mutableMissedTimers: List<Timer>
+        get() {
+            if (mMissedTimers == null) {
+                mMissedTimers = mutableListOf()
+                for (timer in mutableTimers) {
+                    if (timer.isMissed) {
+                        mMissedTimers!!.add(timer)
+                    }
+                }
+                mMissedTimers!!.sortWith(Timer.EXPIRY_COMPARATOR)
+            }
+
+            return mMissedTimers!!
+        }
+
+    /**
+     * This method updates timer data without updating notifications. This is useful in bulk-update
+     * scenarios so the notifications are only rebuilt once.
+     *
+     * @param timer an updated timer to store
+     * @return the state of the timer prior to the update
+     */
+    private fun doUpdateTimer(timer: Timer): Timer {
+        // Retrieve the cached form of the timer.
+        val timers = mutableTimers
+        val index = timers.indexOf(timer)
+        val before = timers[index]
+
+        // If no change occurred, ignore this update.
+        if (timer === before) {
+            return timer
+        }
+
+        // Update the timer in permanent storage.
+        TimerDAO.updateTimer(mPrefs, timer)
+
+        // Update the timer in the cache.
+        val oldTimer = timers.set(index, timer)
+
+        // Clear the cache of expired timers if the timer changed to/from expired.
+        if (before.isExpired || timer.isExpired) {
+            mExpiredTimers = null
+        }
+        // Clear the cache of missed timers if the timer changed to/from missed.
+        if (before.isMissed || timer.isMissed) {
+            mMissedTimers = null
+        }
+
+        // Update the timer expiration callback.
+        updateAlarmManager()
+
+        // Update the timer ringer.
+        updateRinger(before, timer)
+
+        // Notify listeners of the change.
+        for (timerListener in mTimerListeners) {
+            timerListener.timerUpdated(before, timer)
+        }
+
+        return oldTimer
+    }
+
+    /**
+     * This method removes timer data without updating notifications. This is useful in bulk-remove
+     * scenarios so the notifications are only rebuilt once.
+     *
+     * @param timer an existing timer to be removed
+     */
+    private fun doRemoveTimer(timer: Timer) {
+        // Remove the timer from permanent storage.
+        var timerVar = timer
+        TimerDAO.removeTimer(mPrefs, timerVar)
+
+        // Remove the timer from the cache.
+        val timers: MutableList<Timer> = mutableTimers
+        val index = timers.indexOf(timerVar)
+
+        // If the timer cannot be located there is nothing to remove.
+        if (index == -1) {
+            return
+        }
+        timerVar = timers.removeAt(index)
+
+        // Clear the cache of expired timers if a new expired timer was added.
+        if (timerVar.isExpired) {
+            mExpiredTimers = null
+        }
+
+        // Clear the cache of missed timers if a new missed timer was added.
+        if (timerVar.isMissed) {
+            mMissedTimers = null
+        }
+
+        // Update the timer expiration callback.
+        updateAlarmManager()
+
+        // Update the timer ringer.
+        updateRinger(timerVar, null)
+
+        // Notify listeners of the change.
+        for (timerListener in mTimerListeners) {
+            timerListener.timerRemoved(timerVar)
+        }
+    }
+
+    /**
+     * This method updates/removes timer data without updating notifications. This is useful in
+     * bulk-update scenarios so the notifications are only rebuilt once.
+     *
+     * If the given `timer` is expired and marked for deletion after use then this method
+     * removes the timer. The timer is otherwise transitioned to the reset state and continues
+     * to exist.
+     *
+     * @param timer the timer to be reset
+     * @param allowDelete `true` if the timer is allowed to be deleted instead of reset
+     * (e.g. one use timers)
+     * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+     * @return the reset `timer` or `null` if the timer was deleted
+     */
+    private fun doResetOrDeleteTimer(
+        timer: Timer,
+        allowDelete: Boolean,
+        @StringRes eventLabelId: Int
+    ): Timer? {
+        if (allowDelete &&
+                (timer.isExpired || timer.isMissed) &&
+                timer.deleteAfterUse) {
+            doRemoveTimer(timer)
+            if (eventLabelId != 0) {
+                Events.sendTimerEvent(R.string.action_delete, eventLabelId)
+            }
+            return null
+        } else if (!timer.isReset) {
+            val reset = timer.reset()
+            doUpdateTimer(reset)
+            if (eventLabelId != 0) {
+                Events.sendTimerEvent(R.string.action_reset, eventLabelId)
+            }
+            return reset
+        }
+        return timer
+    }
+
+    /**
+     * This method updates/removes timer data after a reboot without updating notifications.
+     *
+     * @param timer the timer to be updated
+     */
+    private fun doUpdateAfterRebootTimer(timer: Timer) {
+        var updated = timer.updateAfterReboot()
+        if (updated.remainingTime < MISSED_THRESHOLD && updated.isRunning) {
+            updated = updated.miss()
+        }
+        doUpdateTimer(updated)
+    }
+
+    private fun doUpdateAfterTimeSetTimer(timer: Timer) {
+        val updated = timer.updateAfterTimeSet()
+        doUpdateTimer(updated)
+    }
+
+    /**
+     * Updates the callback given to this application from the [AlarmManager] that signals the
+     * expiration of the next timer. If no timers are currently set to expire (i.e. no running
+     * timers exist) then this method clears the expiration callback from AlarmManager.
+     */
+    private fun updateAlarmManager() {
+        // Locate the next firing timer if one exists.
+        var nextExpiringTimer: Timer? = null
+        for (timer in mutableTimers) {
+            if (timer.isRunning) {
+                if (nextExpiringTimer == null) {
+                    nextExpiringTimer = timer
+                } else if (timer.expirationTime < nextExpiringTimer.expirationTime) {
+                    nextExpiringTimer = timer
+                }
+            }
+        }
+
+        // Build the intent that signals the timer expiration.
+        val intent: Intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer)
+        if (nextExpiringTimer == null) {
+            // Cancel the existing timer expiration callback.
+            val pi: PendingIntent? = PendingIntent.getService(mContext,
+                    0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE)
+            if (pi != null) {
+                mAlarmManager.cancel(pi)
+                pi.cancel()
+            }
+        } else {
+            // Update the existing timer expiration callback.
+            val pi: PendingIntent = PendingIntent.getService(mContext,
+                    0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+            schedulePendingIntent(mAlarmManager, nextExpiringTimer.expirationTime, pi)
+        }
+    }
+
+    /**
+     * Starts and stops the ringer for timers if the change to the timer demands it.
+     *
+     * @param before the state of the timer before the change; `null` indicates added
+     * @param after the state of the timer after the change; `null` indicates delete
+     */
+    private fun updateRinger(before: Timer?, after: Timer?) {
+        // Retrieve the states before and after the change.
+        val beforeState = before?.state
+        val afterState = after?.state
+
+        // If the timer state did not change, the ringer state is unchanged.
+        if (beforeState == afterState) {
+            return
+        }
+
+        // If the timer is the first to expire, start ringing.
+        if (afterState == Timer.State.EXPIRED && mRingingIds.add(after.id) &&
+                mRingingIds.size == 1) {
+            AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext)
+            TimerKlaxon.start(mContext)
+        }
+
+        // If the expired timer was the last to reset, stop ringing.
+        if (beforeState == Timer.State.EXPIRED && mRingingIds.remove(before.id) &&
+                mRingingIds.isEmpty()) {
+            TimerKlaxon.stop(mContext)
+            AlarmAlertWakeLock.releaseCpuLock()
+        }
+    }
+
+    /**
+     * Updates the notification controlling unexpired timers. This notification is only displayed
+     * when the application is not open.
+     */
+    fun updateNotification() {
+        // Notifications should be hidden if the app is open.
+        if (mNotificationModel.isApplicationInForeground) {
+            mNotificationManager.cancel(mNotificationModel.unexpiredTimerNotificationId)
+            return
+        }
+
+        // Filter the timers to just include unexpired ones.
+        val unexpired: MutableList<Timer> = mutableListOf()
+        for (timer in mutableTimers) {
+            if (timer.isRunning || timer.isPaused) {
+                unexpired.add(timer)
+            }
+        }
+
+        // If no unexpired timers exist, cancel the notification.
+        if (unexpired.isEmpty()) {
+            mNotificationManager.cancel(mNotificationModel.unexpiredTimerNotificationId)
+            return
+        }
+
+        // Sort the unexpired timers to locate the next one scheduled to expire.
+        unexpired.sortWith(Timer.EXPIRY_COMPARATOR)
+
+        // Otherwise build and post a notification reflecting the latest unexpired timers.
+        val notification: Notification =
+                mNotificationBuilder.build(mContext, mNotificationModel, unexpired)
+        val notificationId = mNotificationModel.unexpiredTimerNotificationId
+        mNotificationBuilder.buildChannel(mContext, mNotificationManager)
+        mNotificationManager.notify(notificationId, notification)
+    }
+
+    /**
+     * Updates the notification controlling missed timers. This notification is only displayed when
+     * the application is not open.
+     */
+    fun updateMissedNotification() {
+        // Notifications should be hidden if the app is open.
+        if (mNotificationModel.isApplicationInForeground) {
+            mNotificationManager.cancel(mNotificationModel.missedTimerNotificationId)
+            return
+        }
+
+        val missed = missedTimers
+
+        if (missed.isEmpty()) {
+            mNotificationManager.cancel(mNotificationModel.missedTimerNotificationId)
+            return
+        }
+
+        val notification: Notification = mNotificationBuilder.buildMissed(mContext,
+                mNotificationModel, missed)
+        val notificationId = mNotificationModel.missedTimerNotificationId
+        mNotificationManager.notify(notificationId, notification)
+    }
+
+    /**
+     * Updates the heads-up notification controlling expired timers. This heads-up notification is
+     * displayed whether the application is open or not.
+     */
+    private fun updateHeadsUpNotification() {
+        // Nothing can be done with the heads-up notification without a valid service reference.
+        if (mService == null) {
+            return
+        }
+
+        val expired = expiredTimers
+
+        // If no expired timers exist, stop the service (which cancels the foreground notification).
+        if (expired.isEmpty()) {
+            mService!!.stopSelf()
+            mService = null
+            return
+        }
+
+        // Otherwise build and post a foreground notification reflecting the latest expired timers.
+        val notification: Notification = mNotificationBuilder.buildHeadsUp(mContext, expired)
+        val notificationId = mNotificationModel.expiredTimerNotificationId
+        mService!!.startForeground(notificationId, notification)
+    }
+
+    /**
+     * Update the timer notification in response to a locale change.
+     */
+    private inner class LocaleChangedReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            mTimerRingtoneTitle = null
+            updateNotification()
+            updateMissedNotification()
+            updateHeadsUpNotification()
+        }
+    }
+
+    /**
+     * This receiver is notified when shared preferences change. Cached information built on
+     * preferences must be cleared.
+     */
+    private inner class PreferenceListener : OnSharedPreferenceChangeListener {
+        override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
+            when (key) {
+                SettingsActivity.KEY_TIMER_RINGTONE -> {
+                    mTimerRingtoneUri = null
+                    mTimerRingtoneTitle = null
+                }
+            }
+        }
+    }
+
+    companion object {
+        /**
+         * Running timers less than this threshold are left running/expired; greater than this
+         * threshold are considered missed.
+         */
+        private val MISSED_THRESHOLD: Long = -MINUTE_IN_MILLIS
+
+        fun schedulePendingIntent(am: AlarmManager, triggerTime: Long, pi: PendingIntent?) {
+            if (Utils.isMOrLater) {
+                // Ensure the timer fires even if the device is dozing.
+                am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi)
+            } else {
+                am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerNotificationBuilder.java b/src/com/android/deskclock/data/TimerNotificationBuilder.java
deleted file mode 100644
index efd4297..0000000
--- a/src/com/android/deskclock/data/TimerNotificationBuilder.java
+++ /dev/null
@@ -1,416 +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.annotation.TargetApi;
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Build;
-import android.os.SystemClock;
-import androidx.annotation.DrawableRes;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-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;
-import com.android.deskclock.timer.ExpiredTimersActivity;
-import com.android.deskclock.timer.TimerService;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static androidx.core.app.NotificationCompat.Action;
-import static androidx.core.app.NotificationCompat.Builder;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-
-/**
- * Builds notifications to reflect the latest state of the timers.
- */
-class TimerNotificationBuilder {
-
-    /**
-     * Notification channel containing all TimerModel notifications.
-     */
-    private static final String TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "TimerModelNotification";
-
-    private static final int REQUEST_CODE_UPCOMING = 0;
-    private static final int REQUEST_CODE_MISSING = 1;
-
-    public void buildChannel(Context context, NotificationManagerCompat notificationManager) {
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    TIMER_MODEL_NOTIFICATION_CHANNEL_ID,
-                    context.getString(R.string.default_label),
-                    NotificationManagerCompat.IMPORTANCE_DEFAULT);
-            notificationManager.createNotificationChannel(channel);
-        }
-    }
-
-    public Notification build(Context context, NotificationModel nm, List<Timer> unexpired) {
-        final Timer timer = unexpired.get(0);
-        final int count = unexpired.size();
-
-        // Compute some values required below.
-        final boolean running = timer.isRunning();
-        final Resources res = context.getResources();
-
-        final long base = getChronometerBase(timer);
-        final String pname = context.getPackageName();
-
-        final List<Action> actions = new ArrayList<>(2);
-
-        final CharSequence stateText;
-        if (count == 1) {
-            if (running) {
-                // Single timer is running.
-                if (TextUtils.isEmpty(timer.getLabel())) {
-                    stateText = res.getString(R.string.timer_notification_label);
-                } else {
-                    stateText = timer.getLabel();
-                }
-
-                // Left button: Pause
-                final Intent pause = new Intent(context, TimerService.class)
-                        .setAction(TimerService.ACTION_PAUSE_TIMER)
-                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
-                @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 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());
-
-                @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 Action.Builder(icon2, title2, intent2).build());
-
-            } else {
-                // Single timer is paused.
-                stateText = res.getString(R.string.timer_paused);
-
-                // Left button: Start
-                final Intent start = new Intent(context, TimerService.class)
-                        .setAction(TimerService.ACTION_START_TIMER)
-                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
-                @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 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());
-
-                @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 Action.Builder(icon2, title2, intent2).build());
-            }
-        } else {
-            if (running) {
-                // At least one timer is running.
-                stateText = res.getString(R.string.timers_in_use, count);
-            } else {
-                // All timers are paused.
-                stateText = res.getString(R.string.timers_stopped, count);
-            }
-
-            final Intent reset = TimerService.createResetUnexpiredTimersIntent(context);
-
-            @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 Action.Builder(icon1, title1, intent1).build());
-        }
-
-        // 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, REQUEST_CODE_UPCOMING, showApp,
-                        PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
-        final Builder notification = new NotificationCompat.Builder(
-                context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
-                        .setOngoing(true)
-                        .setLocalOnly(true)
-                        .setShowWhen(false)
-                        .setAutoCancel(false)
-                        .setContentIntent(pendingShowApp)
-                        .setPriority(Notification.PRIORITY_HIGH)
-                        .setCategory(NotificationCompat.CATEGORY_ALARM)
-                        .setSmallIcon(R.drawable.stat_notify_timer)
-                        .setSortKey(nm.getTimerNotificationSortKey())
-                        .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();
-    }
-
-    Notification buildHeadsUp(Context context, List<Timer> expired) {
-        final Timer timer = expired.get(0);
-
-        // First action intent is to reset all timers.
-        @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<Action> actions = new ArrayList<>(2);
-        if (count == 1) {
-            final String label = timer.getLabel();
-            if (TextUtils.isEmpty(label)) {
-                stateText = context.getString(R.string.timer_times_up);
-            } else {
-                stateText = label;
-            }
-
-            // Left button: Reset single timer
-            final CharSequence title1 = context.getString(R.string.timer_stop);
-            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);
-            @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
-            final CharSequence title2 = context.getString(R.string.timer_plus_1_min);
-            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 Action.Builder(icon1, title1, intent1).build());
-        }
-
-        final long base = getChronometerBase(timer);
-
-        final String pname = context.getPackageName();
-
-        // Content intent shows the timer full screen when clicked.
-        final Intent content = new Intent(context, ExpiredTimersActivity.class);
-        final PendingIntent contentIntent = 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);
-
-        final Builder notification = new NotificationCompat.Builder(
-                context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
-                        .setOngoing(true)
-                        .setLocalOnly(true)
-                        .setShowWhen(false)
-                        .setAutoCancel(false)
-                        .setContentIntent(contentIntent)
-                        .setPriority(Notification.PRIORITY_MAX)
-                        .setDefaults(Notification.DEFAULT_LIGHTS)
-                        .setSmallIcon(R.drawable.stat_notify_timer)
-                        .setFullScreenIntent(pendingFullScreen, true)
-                        .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();
-    }
-
-    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 Resources res = context.getResources();
-
-        final Action action;
-
-        final CharSequence stateText;
-        if (count == 1) {
-            // Single timer is missed.
-            if (TextUtils.isEmpty(timer.getLabel())) {
-                stateText = res.getString(R.string.missed_timer_notification_label);
-            } else {
-                stateText = res.getString(R.string.missed_named_timer_notification_label,
-                        timer.getLabel());
-            }
-
-            // Reset button
-            final Intent reset = new Intent(context, TimerService.class)
-                    .setAction(TimerService.ACTION_RESET_TIMER)
-                    .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
-            @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);
-            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);
-
-            @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);
-            action = new Action.Builder(icon1, title1, intent1).build();
-        }
-
-        // 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, REQUEST_CODE_MISSING, showApp,
-                        PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
-        final Builder notification = new NotificationCompat.Builder(
-                context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
-                        .setLocalOnly(true)
-                        .setShowWhen(false)
-                        .setAutoCancel(false)
-                        .setContentIntent(pendingShowApp)
-                        .setPriority(Notification.PRIORITY_HIGH)
-                        .setCategory(NotificationCompat.CATEGORY_ALARM)
-                        .setSmallIcon(R.drawable.stat_notify_timer)
-                        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-                        .setSortKey(nm.getTimerNotificationMissedSortKey())
-                        .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();
-    }
-
-    /**
-     * @param timer the timer on which to base the chronometer display
-     * @return the time at which the chronometer will/did reach 0:00 in realtime
-     */
-    private static long getChronometerBase(Timer timer) {
-        // The in-app timer display rounds *up* to the next second for positive timer values. Mirror
-        // that behavior in the notification's Chronometer by padding in an extra second as needed.
-        final long remaining = timer.getRemainingTime();
-        final long adjustedRemaining = remaining < 0 ? remaining : remaining + SECOND_IN_MILLIS;
-
-        // 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/TimerNotificationBuilder.kt b/src/com/android/deskclock/data/TimerNotificationBuilder.kt
new file mode 100644
index 0000000..98e2a92
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerNotificationBuilder.kt
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.AlarmManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.os.SystemClock
+import android.text.TextUtils
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.SECOND_IN_MILLIS
+import android.widget.RemoteViews
+import androidx.annotation.DrawableRes
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.Action
+import androidx.core.app.NotificationCompat.Builder
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+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
+
+/**
+ * Builds notifications to reflect the latest state of the timers.
+ */
+internal class TimerNotificationBuilder {
+
+    fun buildChannel(context: Context, notificationManager: NotificationManagerCompat) {
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+            val channel = NotificationChannel(
+                    TIMER_MODEL_NOTIFICATION_CHANNEL_ID,
+                    context.getString(R.string.default_label),
+                    NotificationManagerCompat.IMPORTANCE_DEFAULT)
+            notificationManager.createNotificationChannel(channel)
+        }
+    }
+
+    fun build(context: Context, nm: NotificationModel, unexpired: List<Timer>): Notification {
+        val timer = unexpired[0]
+        val count = unexpired.size
+
+        // Compute some values required below.
+        val running = timer.isRunning
+        val res: Resources = context.getResources()
+
+        val base = getChronometerBase(timer)
+        val pname: String = context.getPackageName()
+
+        val actions: MutableList<Action> = ArrayList<Action>(2)
+
+        val stateText: CharSequence
+        if (count == 1) {
+            if (running) {
+                // Single timer is running.
+                stateText = if (timer.label.isNullOrEmpty()) {
+                    res.getString(R.string.timer_notification_label)
+                } else {
+                    timer.label
+                }
+
+                // Left button: Pause
+                val pause: Intent = Intent(context, TimerService::class.java)
+                        .setAction(TimerService.ACTION_PAUSE_TIMER)
+                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+                @DrawableRes val icon1: Int = R.drawable.ic_pause_24dp
+                val title1: CharSequence = res.getText(R.string.timer_pause)
+                val intent1: PendingIntent = Utils.pendingServiceIntent(context, pause)
+                actions.add(Action.Builder(icon1, title1, intent1).build())
+
+                // Right Button: +1 Minute
+                val addMinute: Intent = Intent(context, TimerService::class.java)
+                        .setAction(TimerService.ACTION_ADD_MINUTE_TIMER)
+                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+                @DrawableRes val icon2: Int = R.drawable.ic_add_24dp
+                val title2: CharSequence = res.getText(R.string.timer_plus_1_min)
+                val intent2: PendingIntent = Utils.pendingServiceIntent(context, addMinute)
+                actions.add(Action.Builder(icon2, title2, intent2).build())
+            } else {
+                // Single timer is paused.
+                stateText = res.getString(R.string.timer_paused)
+
+                // Left button: Start
+                val start: Intent = Intent(context, TimerService::class.java)
+                        .setAction(TimerService.ACTION_START_TIMER)
+                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+                @DrawableRes val icon1: Int = R.drawable.ic_start_24dp
+                val title1: CharSequence = res.getText(R.string.sw_resume_button)
+                val intent1: PendingIntent = Utils.pendingServiceIntent(context, start)
+                actions.add(Action.Builder(icon1, title1, intent1).build())
+
+                // Right Button: Reset
+                val reset: Intent = Intent(context, TimerService::class.java)
+                        .setAction(TimerService.ACTION_RESET_TIMER)
+                        .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+                @DrawableRes val icon2: Int = R.drawable.ic_reset_24dp
+                val title2: CharSequence = res.getText(R.string.sw_reset_button)
+                val intent2: PendingIntent = Utils.pendingServiceIntent(context, reset)
+                actions.add(Action.Builder(icon2, title2, intent2).build())
+            }
+        } else {
+            stateText = if (running) {
+                // At least one timer is running.
+                res.getString(R.string.timers_in_use, count)
+            } else {
+                // All timers are paused.
+                res.getString(R.string.timers_stopped, count)
+            }
+
+            val reset: Intent = TimerService.createResetUnexpiredTimersIntent(context)
+
+            @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp
+            val title1: CharSequence = res.getText(R.string.timer_reset_all)
+            val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+            actions.add(Action.Builder(icon1, title1, intent1).build())
+        }
+
+        // Intent to load the app and show the timer when the notification is tapped.
+        val showApp: Intent = Intent(context, TimerService::class.java)
+                .setAction(TimerService.ACTION_SHOW_TIMER)
+                .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification)
+
+        val pendingShowApp: PendingIntent =
+                PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp,
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+        val notification: Builder = Builder(
+                context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
+                .setOngoing(true)
+                .setLocalOnly(true)
+                .setShowWhen(false)
+                .setAutoCancel(false)
+                .setContentIntent(pendingShowApp)
+                .setPriority(NotificationManager.IMPORTANCE_HIGH)
+                .setCategory(NotificationCompat.CATEGORY_ALARM)
+                .setSmallIcon(R.drawable.stat_notify_timer)
+                .setSortKey(nm.timerNotificationSortKey)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+        for (action in actions) {
+            notification.addAction(action)
+        }
+
+        if (Utils.isNOrLater) {
+            notification.setCustomContentView(buildChronometer(pname, base, running, stateText))
+                    .setGroup(nm.timerNotificationGroupKey)
+        } else {
+            val contentTextPreN: CharSequence?
+            contentTextPreN = when {
+                count == 1 -> {
+                    TimerStringFormatter.formatTimeRemaining(context, timer.remainingTime, false)
+                }
+                running -> {
+                    val timeRemaining = TimerStringFormatter.formatTimeRemaining(context,
+                            timer.remainingTime, false)
+                    context.getString(R.string.next_timer_notif, timeRemaining)
+                }
+                else -> context.getString(R.string.all_timers_stopped_notif)
+            }
+
+            notification.setContentTitle(stateText).setContentText(contentTextPreN)
+
+            val am: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+            val updateNotification: Intent = TimerService.createUpdateNotificationIntent(context)
+            val remainingTime = timer.remainingTime
+            if (timer.isRunning && remainingTime > MINUTE_IN_MILLIS) {
+                // Schedule a callback to update the time-sensitive information of the running timer
+                val pi: PendingIntent =
+                        PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification,
+                        PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+                val nextMinuteChange: Long = remainingTime % MINUTE_IN_MILLIS
+                val triggerTime: Long = SystemClock.elapsedRealtime() + nextMinuteChange
+                TimerModel.schedulePendingIntent(am, triggerTime, pi)
+            } else {
+                // Cancel the update notification callback.
+                val pi: PendingIntent? = PendingIntent.getService(context, 0, updateNotification,
+                        PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE)
+                if (pi != null) {
+                    am.cancel(pi)
+                    pi.cancel()
+                }
+            }
+        }
+        return notification.build()
+    }
+
+    fun buildHeadsUp(context: Context, expired: List<Timer>): Notification {
+        val timer = expired[0]
+
+        // First action intent is to reset all timers.
+        @DrawableRes val icon1: Int = R.drawable.ic_stop_24dp
+        val reset: Intent = TimerService.createResetExpiredTimersIntent(context)
+        val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+
+        // Generate some descriptive text, a title, and an action name based on the timer count.
+        val stateText: CharSequence
+        val count = expired.size
+        val actions: MutableList<Action> = ArrayList<Action>(2)
+        if (count == 1) {
+            val label = timer.label
+            stateText = if (label.isNullOrEmpty()) {
+                context.getString(R.string.timer_times_up)
+            } else {
+                label
+            }
+
+            // Left button: Reset single timer
+            val title1: CharSequence = context.getString(R.string.timer_stop)
+            actions.add(Action.Builder(icon1, title1, intent1).build())
+
+            // Right button: Add minute
+            val addTime: Intent = TimerService.createAddMinuteTimerIntent(context, timer.id)
+            val intent2: PendingIntent = Utils.pendingServiceIntent(context, addTime)
+            @DrawableRes val icon2: Int = R.drawable.ic_add_24dp
+            val title2: CharSequence = context.getString(R.string.timer_plus_1_min)
+            actions.add(Action.Builder(icon2, title2, intent2).build())
+        } else {
+            stateText = context.getString(R.string.timer_multi_times_up, count)
+
+            // Left button: Reset all timers
+            val title1: CharSequence = context.getString(R.string.timer_stop_all)
+            actions.add(Action.Builder(icon1, title1, intent1).build())
+        }
+
+        val base = getChronometerBase(timer)
+
+        val pname: String = context.getPackageName()
+
+        // Content intent shows the timer full screen when clicked.
+        val content = Intent(context, ExpiredTimersActivity::class.java)
+        val contentIntent: PendingIntent = Utils.pendingActivityIntent(context, content)
+
+        // Full screen intent has flags so it is different than the content intent.
+        val fullScreen: Intent = Intent(context, ExpiredTimersActivity::class.java)
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
+        val pendingFullScreen: PendingIntent = Utils.pendingActivityIntent(context, fullScreen)
+
+        val notification: Builder = Builder(
+                context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
+                .setOngoing(true)
+                .setLocalOnly(true)
+                .setShowWhen(false)
+                .setAutoCancel(false)
+                .setContentIntent(contentIntent)
+                .setPriority(NotificationManager.IMPORTANCE_HIGH)
+                .setDefaults(Notification.DEFAULT_LIGHTS)
+                .setSmallIcon(R.drawable.stat_notify_timer)
+                .setFullScreenIntent(pendingFullScreen, true)
+                .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+        for (action in actions) {
+            notification.addAction(action)
+        }
+
+        if (Utils.isNOrLater) {
+            notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
+        } else {
+            val contentTextPreN: CharSequence = if (count == 1) {
+                context.getString(R.string.timer_times_up)
+            } else {
+                context.getString(R.string.timer_multi_times_up, count)
+            }
+            notification.setContentTitle(stateText).setContentText(contentTextPreN)
+        }
+
+        return notification.build()
+    }
+
+    fun buildMissed(
+        context: Context,
+        nm: NotificationModel,
+        missedTimers: List<Timer>
+    ): Notification {
+        val timer = missedTimers[0]
+        val count = missedTimers.size
+
+        // Compute some values required below.
+        val base = getChronometerBase(timer)
+        val pname: String = context.getPackageName()
+        val res: Resources = context.getResources()
+
+        val action: Action
+
+        val stateText: CharSequence
+        if (count == 1) {
+            // Single timer is missed.
+            stateText = if (TextUtils.isEmpty(timer.label)) {
+                res.getString(R.string.missed_timer_notification_label)
+            } else {
+                res.getString(R.string.missed_named_timer_notification_label,
+                        timer.label)
+            }
+
+            // Reset button
+            val reset: Intent = Intent(context, TimerService::class.java)
+                    .setAction(TimerService.ACTION_RESET_TIMER)
+                    .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+            @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp
+            val title1: CharSequence = res.getText(R.string.timer_reset)
+            val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+            action = Action.Builder(icon1, title1, intent1).build()
+        } else {
+            // Multiple missed timers.
+            stateText = res.getString(R.string.timer_multi_missed, count)
+
+            val reset: Intent = TimerService.createResetMissedTimersIntent(context)
+
+            @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp
+            val title1: CharSequence = res.getText(R.string.timer_reset_all)
+            val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+            action = Action.Builder(icon1, title1, intent1).build()
+        }
+
+        // Intent to load the app and show the timer when the notification is tapped.
+        val showApp: Intent = Intent(context, TimerService::class.java)
+                .setAction(TimerService.ACTION_SHOW_TIMER)
+                .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+                .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification)
+
+        val pendingShowApp: PendingIntent =
+                PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp,
+                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+        val notification: Builder = Builder(
+                context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
+                .setLocalOnly(true)
+                .setShowWhen(false)
+                .setAutoCancel(false)
+                .setContentIntent(pendingShowApp)
+                .setPriority(NotificationManager.IMPORTANCE_HIGH)
+                .setCategory(NotificationCompat.CATEGORY_ALARM)
+                .setSmallIcon(R.drawable.stat_notify_timer)
+                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+                .setSortKey(nm.timerNotificationMissedSortKey)
+                .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+                .addAction(action)
+                .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+        if (Utils.isNOrLater) {
+            notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
+                    .setGroup(nm.timerNotificationGroupKey)
+        } else {
+            val contentText: CharSequence = AlarmUtils.getFormattedTime(context,
+                    timer.wallClockExpirationTime)
+            notification.setContentText(contentText).setContentTitle(stateText)
+        }
+
+        return notification.build()
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun buildChronometer(
+        pname: String,
+        base: Long,
+        running: Boolean,
+        stateText: CharSequence
+    ): RemoteViews {
+        val content = 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
+    }
+
+    companion object {
+        /**
+         * Notification channel containing all TimerModel notifications.
+         */
+        private const val TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "TimerModelNotification"
+
+        private const val REQUEST_CODE_UPCOMING = 0
+        private const val REQUEST_CODE_MISSING = 1
+
+        /**
+         * @param timer the timer on which to base the chronometer display
+         * @return the time at which the chronometer will/did reach 0:00 in realtime
+         */
+        private fun getChronometerBase(timer: Timer): Long {
+            // The in-app timer display rounds *up* to the next second for positive timer values.
+            // Mirror that behavior in the notification's Chronometer by padding in an extra second
+            // as needed.
+            val remaining = timer.remainingTime
+            val adjustedRemaining = if (remaining < 0) remaining else remaining + SECOND_IN_MILLIS
+
+            // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now.
+            return SystemClock.elapsedRealtime() + adjustedRemaining
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerStringFormatter.java b/src/com/android/deskclock/data/TimerStringFormatter.java
deleted file mode 100644
index 6ce6881..0000000
--- a/src/com/android/deskclock/data/TimerStringFormatter.java
+++ /dev/null
@@ -1,123 +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.content.Context;
-import androidx.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/TimerStringFormatter.kt b/src/com/android/deskclock/data/TimerStringFormatter.kt
new file mode 100644
index 0000000..be5a983
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerStringFormatter.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.text.format.DateUtils.HOUR_IN_MILLIS
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.SECOND_IN_MILLIS
+import androidx.annotation.StringRes
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+object TimerStringFormatter {
+    /**
+     * Format "7 hours 52 minutes 14 seconds remaining"
+     */
+    @JvmStatic
+    fun formatTimeRemaining(
+        context: Context,
+        remainingTime: Long,
+        shouldShowSeconds: Boolean
+    ): String? {
+        var roundedHours = (remainingTime / HOUR_IN_MILLIS).toInt()
+        var roundedMinutes = (remainingTime / MINUTE_IN_MILLIS % 60).toInt()
+        var roundedSeconds = (remainingTime / SECOND_IN_MILLIS % 60).toInt()
+
+        val seconds: Int
+        val minutes: Int
+        val hours: Int
+        if (remainingTime % SECOND_IN_MILLIS != 0L && 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
+        }
+
+        val minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, minutes)
+        val hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, hours)
+        val secSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.seconds, seconds)
+
+        // The verb "remaining" may have to change tense for singular subjects in some languages.
+        val remainingSuffix: String =
+                context.getString(if (minutes > 1 || hours > 1 || seconds > 1) {
+                    R.string.timer_remaining_multiple
+                } else {
+                    R.string.timer_remaining_single
+                })
+
+        val showHours = hours > 0
+        val showMinutes = minutes > 0
+        val showSeconds = seconds > 0 && shouldShowSeconds
+
+        var formatStringId = -1
+        if (showHours) {
+            formatStringId = if (showMinutes) {
+                if (showSeconds) {
+                    R.string.timer_notifications_hours_minutes_seconds
+                } else {
+                    R.string.timer_notifications_hours_minutes
+                }
+            } else if (showSeconds) {
+                R.string.timer_notifications_hours_seconds
+            } else {
+                R.string.timer_notifications_hours
+            }
+        } else if (showMinutes) {
+            formatStringId = if (showSeconds) {
+                R.string.timer_notifications_minutes_seconds
+            } else {
+                R.string.timer_notifications_minutes
+            }
+        } else if (showSeconds) {
+            formatStringId = R.string.timer_notifications_seconds
+        } else if (!shouldShowSeconds) {
+            formatStringId = R.string.timer_notifications_less_min
+        }
+
+        return if (formatStringId == -1) {
+            null
+        } else {
+            String.format(context.getString(formatStringId), hourSeq, minSeq,
+                    remainingSuffix, secSeq)
+        }
+    }
+
+    @JvmStatic
+    fun formatString(
+        context: Context,
+        @StringRes stringResId: Int,
+        currentTime: Long,
+        shouldShowSeconds: Boolean
+    ): String {
+        return String.format(context.getString(stringResId),
+                formatTimeRemaining(context, currentTime, shouldShowSeconds))
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Weekdays.java b/src/com/android/deskclock/data/Weekdays.java
deleted file mode 100644
index b5ba73b..0000000
--- a/src/com/android/deskclock/data/Weekdays.java
+++ /dev/null
@@ -1,336 +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.content.Context;
-import androidx.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/Weekdays.kt b/src/com/android/deskclock/data/Weekdays.kt
new file mode 100644
index 0000000..3f792fc
--- /dev/null
+++ b/src/com/android/deskclock/data/Weekdays.kt
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.R
+
+import java.text.DateFormatSymbols
+import java.util.Calendar
+
+/**
+ * This class is responsible for encoding a weekly repeat cycle in a [bitset][.getBits]. It
+ * also converts between those bits and the [Calendar.DAY_OF_WEEK] values for easier mutation
+ * and querying.
+ */
+class Weekdays private constructor(bits: Int) {
+    /**
+     * The preferred starting day of the week can differ by locale. This enumerated value is used to
+     * describe the preferred ordering.
+     */
+    enum class Order(vararg calendarDays: Int) {
+        SAT_TO_FRI(Calendar.SATURDAY, Calendar.SUNDAY, Calendar.MONDAY,
+                Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY),
+        SUN_TO_SAT(Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+                Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY),
+        MON_TO_SUN(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY,
+                Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY);
+
+        val calendarDays: List<Int> = calendarDays.asList()
+    }
+
+    companion object {
+        /** All valid bits set.  */
+        private const val ALL_DAYS = 0x7F
+
+        /** An instance with all weekdays in the weekly repeat cycle.  */
+        @JvmField
+        val ALL = fromBits(ALL_DAYS)
+
+        /** An instance with no weekdays in the weekly repeat cycle.  */
+        @JvmField
+        val NONE = fromBits(0)
+
+        /** Maps calendar weekdays to the bit masks that represent them in this class.  */
+        private val sCalendarDayToBit: Map<Int, Int>
+
+        init {
+            val map: MutableMap<Int, Int> = mutableMapOf()
+            map[Calendar.MONDAY] = 0x01
+            map[Calendar.TUESDAY] = 0x02
+            map[Calendar.WEDNESDAY] = 0x04
+            map[Calendar.THURSDAY] = 0x08
+            map[Calendar.FRIDAY] = 0x10
+            map[Calendar.SATURDAY] = 0x20
+            map[Calendar.SUNDAY] = 0x40
+            sCalendarDayToBit = map
+        }
+
+        /**
+         * @param bits [bits][.getBits] representing the encoded weekly repeat schedule
+         * @return a Weekdays instance representing the same repeat schedule as the `bits`
+         */
+        @JvmStatic
+        fun fromBits(bits: Int): Weekdays {
+            return Weekdays(bits)
+        }
+
+        /**
+         * @param calendarDays an array containing any or all of the following values
+         *
+         *  * [Calendar.SUNDAY]
+         *  * [Calendar.MONDAY]
+         *  * [Calendar.TUESDAY]
+         *  * [Calendar.WEDNESDAY]
+         *  * [Calendar.THURSDAY]
+         *  * [Calendar.FRIDAY]
+         *  * [Calendar.SATURDAY]
+         *
+         * @return a Weekdays instance representing the given `calendarDays`
+         */
+        @JvmStatic
+        fun fromCalendarDays(vararg calendarDays: Int): Weekdays {
+            var bits = 0
+            for (calendarDay in calendarDays) {
+                val bit = sCalendarDayToBit[calendarDay]
+                if (bit != null) {
+                    bits = bits or bit
+                }
+            }
+            return Weekdays(bits)
+        }
+    }
+
+    /** An encoded form of a weekly repeat schedule.  */
+    val bits: Int = ALL_DAYS and bits
+
+    /**
+     * @param calendarDay any of the following values
+     *
+     *  * [Calendar.SUNDAY]
+     *  * [Calendar.MONDAY]
+     *  * [Calendar.TUESDAY]
+     *  * [Calendar.WEDNESDAY]
+     *  * [Calendar.THURSDAY]
+     *  * [Calendar.FRIDAY]
+     *  * [Calendar.SATURDAY]
+     *
+     * @param on `true` if the `calendarDay` is on; `false` otherwise
+     * @return a WeekDays instance with the `calendarDay` mutated
+     */
+    fun setBit(calendarDay: Int, on: Boolean): Weekdays {
+        val bit = sCalendarDayToBit[calendarDay] ?: return this
+        return Weekdays(if (on) bits or bit else bits and bit.inv())
+    }
+
+    /**
+     * @param calendarDay any of the following values
+     *
+     *  * [Calendar.SUNDAY]
+     *  * [Calendar.MONDAY]
+     *  * [Calendar.TUESDAY]
+     *  * [Calendar.WEDNESDAY]
+     *  * [Calendar.THURSDAY]
+     *  * [Calendar.FRIDAY]
+     *  * [Calendar.SATURDAY]
+     *
+     * @return `true` if the given `calendarDay`
+     */
+    fun isBitOn(calendarDay: Int): Boolean {
+        val bit = sCalendarDayToBit[calendarDay]
+                ?: throw IllegalArgumentException("$calendarDay is not a valid weekday")
+        return bits and bit > 0
+    }
+
+    /**
+     * @return `true` iff at least one weekday is enabled in the repeat schedule
+     */
+    val isRepeating: Boolean
+        get() = bits != 0
+
+    /**
+     * Note: only the day-of-week is read from the `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 `time` and the previous enabled weekday
+     * which is always between 1 and 7 inclusive; `-1` if no weekdays are enabled
+     */
+    fun getDistanceToPreviousDay(time: Calendar): Int {
+        var calendarDay = time[Calendar.DAY_OF_WEEK]
+        for (count in 1..7) {
+            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 `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 `time` and the next enabled weekday which
+     * is always between 0 and 6 inclusive; `-1` if no weekdays are enabled
+     */
+    fun getDistanceToNextDay(time: Calendar): Int {
+        var calendarDay = time[Calendar.DAY_OF_WEEK]
+        for (count in 0..6) {
+            if (isBitOn(calendarDay)) {
+                return count
+            }
+
+            calendarDay++
+            if (calendarDay > Calendar.SATURDAY) {
+                calendarDay = Calendar.SUNDAY
+            }
+        }
+
+        return -1
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || javaClass != other.javaClass) return false
+
+        val weekdays = other as Weekdays
+        return bits == weekdays.bits
+    }
+
+    override fun hashCode(): Int {
+        return bits
+    }
+
+    override fun toString(): String {
+        val builder = StringBuilder(19)
+        builder.append("[")
+        if (isBitOn(Calendar.MONDAY)) {
+            builder.append(if (builder.length > 1) " M" else "M")
+        }
+        if (isBitOn(Calendar.TUESDAY)) {
+            builder.append(if (builder.length > 1) " T" else "T")
+        }
+        if (isBitOn(Calendar.WEDNESDAY)) {
+            builder.append(if (builder.length > 1) " W" else "W")
+        }
+        if (isBitOn(Calendar.THURSDAY)) {
+            builder.append(if (builder.length > 1) " Th" else "Th")
+        }
+        if (isBitOn(Calendar.FRIDAY)) {
+            builder.append(if (builder.length > 1) " F" else "F")
+        }
+        if (isBitOn(Calendar.SATURDAY)) {
+            builder.append(if (builder.length > 1) " Sa" else "Sa")
+        }
+        if (isBitOn(Calendar.SUNDAY)) {
+            builder.append(if (builder.length > 1) " Su" else "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 `order`
+     */
+    fun toString(context: Context, order: Order): String {
+        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 `order` in a manner that
+     * is most appropriate for talk-back
+     */
+    fun toAccessibilityString(context: Context, order: Order): String {
+        return toString(context, order, true /* forceLongNames */)
+    }
+
+    @get:VisibleForTesting
+    val count: Int
+        get() {
+            var count = 0
+            for (calendarDay in Calendar.SUNDAY..Calendar.SATURDAY) {
+                if (isBitOn(calendarDay)) {
+                    count++
+                }
+            }
+            return count
+        }
+
+    /**
+     * @param context for accessing resources
+     * @param order the order in which to present the weekdays
+     * @param forceLongNames if `true` the un-abbreviated weekdays are used
+     * @return the enabled weekdays in the given `order`
+     */
+    private fun toString(context: Context, order: Order, forceLongNames: Boolean): String {
+        if (!isRepeating) {
+            return ""
+        }
+
+        if (bits == ALL_DAYS) {
+            return context.getString(R.string.every_day)
+        }
+
+        val longNames = forceLongNames || count <= 1
+        val dfs = DateFormatSymbols()
+        val weekdays = if (longNames) dfs.weekdays else dfs.shortWeekdays
+
+        val separator: String = context.getString(R.string.day_concat)
+
+        val builder = StringBuilder(40)
+        for (calendarDay in order.calendarDays) {
+            if (isBitOn(calendarDay)) {
+                if (builder.isNotEmpty()) {
+                    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
deleted file mode 100644
index da7dc19..0000000
--- a/src/com/android/deskclock/data/WidgetDAO.java
+++ /dev/null
@@ -1,47 +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.data;
-
-import android.content.SharedPreferences;
-
-/**
- * This class encapsulates the transfer of data between widget objects and their permanent storage
- * in {@link SharedPreferences}.
- */
-final class WidgetDAO {
-
-    /** Suffix for a key to a preference that stores the instance count for a given widget type. */
-    private static final String WIDGET_COUNT = "_widget_count";
-
-    private WidgetDAO() {}
-
-    /**
-     * @param widgetProviderClass indicates the type of widget being counted
-     * @param count the number of widgets of the given type
-     * @return the delta between the new count and the old count
-     */
-    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) {
-            prefs.edit().remove(key).apply();
-        } else {
-            prefs.edit().putInt(key, count).apply();
-        }
-        return count - oldCount;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/WidgetDAO.kt b/src/com/android/deskclock/data/WidgetDAO.kt
new file mode 100644
index 0000000..3259971
--- /dev/null
+++ b/src/com/android/deskclock/data/WidgetDAO.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+/**
+ * This class encapsulates the transfer of data between widget objects and their permanent storage
+ * in [SharedPreferences].
+ */
+internal object WidgetDAO {
+    /** Suffix for a key to a preference that stores the instance count for a given widget type.  */
+    private const val WIDGET_COUNT = "_widget_count"
+
+    /**
+     * @param widgetProviderClass indicates the type of widget being counted
+     * @param count the number of widgets of the given type
+     * @return the delta between the new count and the old count
+     */
+    fun updateWidgetCount(
+        prefs: SharedPreferences,
+        widgetProviderClass: Class<*>,
+        count: Int
+    ): Int {
+        val key = widgetProviderClass.simpleName + WIDGET_COUNT
+        val oldCount: Int = prefs.getInt(key, 0)
+        if (count == 0) {
+            prefs.edit().remove(key).apply()
+        } else {
+            prefs.edit().putInt(key, count).apply()
+        }
+        return count - oldCount
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/WidgetModel.java b/src/com/android/deskclock/data/WidgetModel.kt
similarity index 60%
rename from src/com/android/deskclock/data/WidgetModel.java
rename to src/com/android/deskclock/data/WidgetModel.kt
index e05b8ff..b63014f 100644
--- a/src/com/android/deskclock/data/WidgetModel.java
+++ b/src/com/android/deskclock/data/WidgetModel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,37 +14,32 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.data;
+package com.android.deskclock.data
 
-import android.content.SharedPreferences;
-import androidx.annotation.StringRes;
+import android.content.SharedPreferences
+import androidx.annotation.StringRes
 
-import com.android.deskclock.R;
-import com.android.deskclock.events.Events;
+import com.android.deskclock.R
+import com.android.deskclock.events.Events
 
 /**
  * All widget data is accessed via this model.
  */
-final class WidgetModel {
-
-    private final SharedPreferences mPrefs;
-
-    WidgetModel(SharedPreferences prefs) {
-        mPrefs = prefs;
-    }
-
+internal class WidgetModel(private val mPrefs: SharedPreferences) {
     /**
      * @param widgetClass indicates the type of widget being counted
      * @param count the number of widgets of the given type
      * @param eventCategoryId identifies the category of event to send
      */
-    void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
-        int delta = WidgetDAO.updateWidgetCount(mPrefs, widgetClass, count);
-        for (; delta > 0; delta--) {
-            Events.sendEvent(eventCategoryId, R.string.action_create, 0);
+    fun updateWidgetCount(widgetClass: Class<*>, count: Int, @StringRes eventCategoryId: Int) {
+        var delta = WidgetDAO.updateWidgetCount(mPrefs, widgetClass, count)
+        while (delta > 0) {
+            Events.sendEvent(eventCategoryId, R.string.action_create, 0)
+            delta--
         }
-        for (; delta < 0; delta++) {
-            Events.sendEvent(eventCategoryId, R.string.action_delete, 0);
+        while (delta < 0) {
+            Events.sendEvent(eventCategoryId, R.string.action_delete, 0)
+            delta++
         }
     }
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/EventTracker.java b/src/com/android/deskclock/events/EventTracker.kt
similarity index 73%
rename from src/com/android/deskclock/events/EventTracker.java
rename to src/com/android/deskclock/events/EventTracker.kt
index e92d657..39a4f11 100644
--- a/src/com/android/deskclock/events/EventTracker.java
+++ b/src/com/android/deskclock/events/EventTracker.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,18 +14,19 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.events;
+package com.android.deskclock.events
 
-import androidx.annotation.StringRes;
+import androidx.annotation.StringRes
 
-public interface EventTracker {
+interface EventTracker {
+
     /**
      * Record the event in some form or fashion.
      *
      * @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
+     * Notification, etc.; 0 indicates no label could be established
      */
-    void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label);
+    fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/Events.java b/src/com/android/deskclock/events/Events.java
deleted file mode 100644
index 5e5129c..0000000
--- a/src/com/android/deskclock/events/Events.java
+++ /dev/null
@@ -1,95 +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.events;
-
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.R;
-import com.android.deskclock.controller.Controller;
-
-/**
- * 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";
-
-    /**
-     * Tracks an alarm event.
-     *
-     * @param action resource id of event action
-     * @param label resource id of event label
-     */
-    public static void sendAlarmEvent(@StringRes int action, @StringRes int label) {
-        sendEvent(R.string.category_alarm, action, label);
-    }
-
-    /**
-     * Tracks a clock event.
-     *
-     * @param action resource id of event action
-     * @param label resource id of event label
-     */
-    public static void sendClockEvent(@StringRes int action, @StringRes int label) {
-        sendEvent(R.string.category_clock, action, label);
-    }
-
-    /**
-     * Tracks a timer event.
-     *
-     * @param action resource id of event action
-     * @param label resource id of event label
-     */
-    public static void sendTimerEvent(@StringRes int action, @StringRes int label) {
-        sendEvent(R.string.category_timer, action, label);
-    }
-
-    /**
-     * Tracks a stopwatch event.
-     *
-     * @param action resource id of event action
-     * @param label resource id of event label
-     */
-    public static void sendStopwatchEvent(@StringRes int action, @StringRes int label) {
-        sendEvent(R.string.category_stopwatch, action, label);
-    }
-
-    /**
-     * Tracks a screensaver event.
-     *
-     * @param action resource id of event action
-     * @param label resource id of event label
-     */
-    public static void sendScreensaverEvent(@StringRes int action, @StringRes int label) {
-        sendEvent(R.string.category_screensaver, action, label);
-    }
-
-    /**
-     * Tracks an event. Events have a category, action, label and value. This
-     * method can be used to track events such as button presses or other user
-     * interactions with your application (value is not used in this app).
-     *
-     * @param category resource id of event category
-     * @param action resource id of event action
-     * @param label resource id of event label
-     */
-    public static void sendEvent(@StringRes int category, @StringRes int action,
-            @StringRes int label) {
-        Controller.getController().sendEvent(category, action, label);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/Events.kt b/src/com/android/deskclock/events/Events.kt
new file mode 100644
index 0000000..d4b116e
--- /dev/null
+++ b/src/com/android/deskclock/events/Events.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.events
+
+import androidx.annotation.StringRes
+
+import com.android.deskclock.R
+import com.android.deskclock.controller.Controller
+
+/**
+ * This thin layer over [Controller.sendEvent] eases the API usage.
+ */
+object Events {
+    /** Extra describing the entity responsible for the action being performed.  */
+    const val EXTRA_EVENT_LABEL = "com.android.deskclock.extra.EVENT_LABEL"
+
+    /**
+     * Tracks an alarm event.
+     *
+     * @param action resource id of event action
+     * @param label resource id of event label
+     */
+    @JvmStatic
+    fun sendAlarmEvent(@StringRes action: Int, @StringRes label: Int) {
+        sendEvent(R.string.category_alarm, action, label)
+    }
+
+    /**
+     * Tracks a clock event.
+     *
+     * @param action resource id of event action
+     * @param label resource id of event label
+     */
+    @JvmStatic
+    fun sendClockEvent(@StringRes action: Int, @StringRes label: Int) {
+        sendEvent(R.string.category_clock, action, label)
+    }
+
+    /**
+     * Tracks a timer event.
+     *
+     * @param action resource id of event action
+     * @param label resource id of event label
+     */
+    @JvmStatic
+    fun sendTimerEvent(@StringRes action: Int, @StringRes label: Int) {
+        sendEvent(R.string.category_timer, action, label)
+    }
+
+    /**
+     * Tracks a stopwatch event.
+     *
+     * @param action resource id of event action
+     * @param label resource id of event label
+     */
+    @JvmStatic
+    fun sendStopwatchEvent(@StringRes action: Int, @StringRes label: Int) {
+        sendEvent(R.string.category_stopwatch, action, label)
+    }
+
+    /**
+     * Tracks a screensaver event.
+     *
+     * @param action resource id of event action
+     * @param label resource id of event label
+     */
+    @JvmStatic
+    fun sendScreensaverEvent(@StringRes action: Int, @StringRes label: Int) {
+        sendEvent(R.string.category_screensaver, action, label)
+    }
+
+    /**
+     * Tracks an event. Events have a category, action, label and value. This
+     * method can be used to track events such as button presses or other user
+     * interactions with your application (value is not used in this app).
+     *
+     * @param category resource id of event category
+     * @param action resource id of event action
+     * @param label resource id of event label
+     */
+    @JvmStatic
+    fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int) {
+        Controller.getController().sendEvent(category, action, label)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/LogEventTracker.java b/src/com/android/deskclock/events/LogEventTracker.java
deleted file mode 100644
index 870f8a3..0000000
--- a/src/com/android/deskclock/events/LogEventTracker.java
+++ /dev/null
@@ -1,49 +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.events;
-
-import android.content.Context;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.LogUtils;
-
-public final class LogEventTracker implements EventTracker {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Events");
-
-    private final Context mContext;
-
-    public LogEventTracker(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    public void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
-        if (label == 0) {
-            LOGGER.d("[%s] [%s]", safeGetString(category), safeGetString(action));
-        } else {
-            LOGGER.d("[%s] [%s] [%s]", safeGetString(category), safeGetString(action),
-                    safeGetString(label));
-        }
-    }
-
-    /**
-     * @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);
-    }
-}
diff --git a/src/com/android/deskclock/events/LogEventTracker.kt b/src/com/android/deskclock/events/LogEventTracker.kt
new file mode 100644
index 0000000..c7cdedc
--- /dev/null
+++ b/src/com/android/deskclock/events/LogEventTracker.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.events
+
+import android.content.Context
+import androidx.annotation.StringRes
+
+import com.android.deskclock.LogUtils
+
+class LogEventTracker(val context: Context) : EventTracker {
+
+    override fun sendEvent(
+        @StringRes category: Int,
+        @StringRes action: Int,
+        @StringRes label: Int
+    ) {
+        if (label == 0) {
+            LOGGER.d("[%s] [%s]", safeGetString(category), safeGetString(action))
+        } else {
+            LOGGER.d("[%s] [%s] [%s]", safeGetString(category), safeGetString(action),
+                    safeGetString(label))
+        }
+    }
+
+    /**
+     * @return Resource string represented by a given resource id, null if resId is invalid (0).
+     */
+    private fun safeGetString(@StringRes resId: Int): String? {
+        return if (resId == 0) null else context.getString(resId)
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("Events")
+    }
+}
diff --git a/src/com/android/deskclock/events/ShortcutEventTracker.java b/src/com/android/deskclock/events/ShortcutEventTracker.java
deleted file mode 100644
index 4b956ce..0000000
--- a/src/com/android/deskclock/events/ShortcutEventTracker.java
+++ /dev/null
@@ -1,54 +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.events;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.pm.ShortcutManager;
-import android.os.Build;
-import androidx.annotation.StringRes;
-import android.util.ArraySet;
-
-import com.android.deskclock.R;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.Set;
-
-@TargetApi(Build.VERSION_CODES.N_MR1)
-public final class ShortcutEventTracker implements EventTracker {
-
-    private final ShortcutManager mShortcutManager;
-    private final Set<String> shortcuts = new ArraySet<>(5);
-
-    public ShortcutEventTracker(Context context) {
-        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));
-        shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_pause));
-        shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_start));
-        shortcuts.add(uidm.getShortcutId(R.string.category_screensaver, R.string.action_show));
-    }
-
-    @Override
-    public void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
-        final String shortcutId = UiDataModel.getUiDataModel().getShortcutId(category, action);
-        if (shortcuts.contains(shortcutId)) {
-            mShortcutManager.reportShortcutUsed(shortcutId);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/ShortcutEventTracker.kt b/src/com/android/deskclock/events/ShortcutEventTracker.kt
new file mode 100644
index 0000000..2b2bdf3
--- /dev/null
+++ b/src/com/android/deskclock/events/ShortcutEventTracker.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.events
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.pm.ShortcutManager
+import android.os.Build
+import android.util.ArraySet
+import androidx.annotation.StringRes
+
+import com.android.deskclock.R
+import com.android.deskclock.uidata.UiDataModel
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+class ShortcutEventTracker(context: Context) : EventTracker {
+    private val mShortcutManager: ShortcutManager =
+            context.getSystemService(ShortcutManager::class.java)
+    private val shortcuts: MutableSet<String> = ArraySet(5)
+
+    init {
+        val uidm = UiDataModel.uiDataModel
+        shortcuts.add(uidm.getShortcutId(R.string.category_alarm, R.string.action_create))
+        shortcuts.add(uidm.getShortcutId(R.string.category_timer, R.string.action_create))
+        shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_pause))
+        shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_start))
+        shortcuts.add(uidm.getShortcutId(R.string.category_screensaver, R.string.action_show))
+    }
+
+    override fun sendEvent(
+        @StringRes category: Int,
+        @StringRes action: Int,
+        @StringRes label: Int
+    ) {
+        val shortcutId = UiDataModel.uiDataModel.getShortcutId(category, action)
+        if (shortcuts.contains(shortcutId)) {
+            mShortcutManager.reportShortcutUsed(shortcutId)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/Alarm.java b/src/com/android/deskclock/provider/Alarm.java
deleted file mode 100644
index fc8aebd..0000000
--- a/src/com/android/deskclock/provider/Alarm.java
+++ /dev/null
@@ -1,468 +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.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.CursorLoader;
-import android.content.Intent;
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-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;
-import java.util.List;
-
-public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
-    /**
-     * Alarms start with an invalid id when it hasn't been saved to the database.
-     */
-    public static final long INVALID_ID = -1;
-
-    /**
-     * The default sort order for this table
-     */
-    private static final String DEFAULT_SORT_ORDER =
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + ", " +
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." +  MINUTES + " ASC" + ", " +
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ClockContract.AlarmsColumns._ID + " DESC";
-
-    private static final String[] QUERY_COLUMNS = {
-            _ID,
-            HOUR,
-            MINUTES,
-            DAYS_OF_WEEK,
-            ENABLED,
-            VIBRATE,
-            LABEL,
-            RINGTONE,
-            DELETE_AFTER_USE
-    };
-
-    private static final String[] QUERY_ALARMS_WITH_INSTANCES_COLUMNS = {
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DAYS_OF_WEEK,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + VIBRATE,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + LABEL,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + RINGTONE,
-            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DELETE_AFTER_USE,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "."
-                    + ClockContract.InstancesColumns.ALARM_STATE,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns._ID,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.YEAR,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MONTH,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.DAY,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.HOUR,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MINUTES,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.LABEL,
-            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.VIBRATE
-    };
-
-    /**
-     * These save calls to cursor.getColumnIndexOrThrow()
-     * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
-     */
-    private static final int ID_INDEX = 0;
-    private static final int HOUR_INDEX = 1;
-    private static final int MINUTES_INDEX = 2;
-    private static final int DAYS_OF_WEEK_INDEX = 3;
-    private static final int ENABLED_INDEX = 4;
-    private static final int VIBRATE_INDEX = 5;
-    private static final int LABEL_INDEX = 6;
-    private static final int RINGTONE_INDEX = 7;
-    private static final int DELETE_AFTER_USE_INDEX = 8;
-    private static final int INSTANCE_STATE_INDEX = 9;
-    public static final int INSTANCE_ID_INDEX = 10;
-    public static final int INSTANCE_YEAR_INDEX = 11;
-    public static final int INSTANCE_MONTH_INDEX = 12;
-    public static final int INSTANCE_DAY_INDEX = 13;
-    public static final int INSTANCE_HOUR_INDEX = 14;
-    public static final int INSTANCE_MINUTE_INDEX = 15;
-    public static final int INSTANCE_LABEL_INDEX = 16;
-    public static final int INSTANCE_VIBRATE_INDEX = 17;
-
-    private static final int COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1;
-    private static final int ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1;
-
-    public static ContentValues createContentValues(Alarm alarm) {
-        ContentValues values = new ContentValues(COLUMN_COUNT);
-        if (alarm.id != INVALID_ID) {
-            values.put(ClockContract.AlarmsColumns._ID, alarm.id);
-        }
-
-        values.put(ENABLED, alarm.enabled ? 1 : 0);
-        values.put(HOUR, alarm.hour);
-        values.put(MINUTES, alarm.minutes);
-        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);
-        if (alarm.alert == null) {
-            // We want to put null, so default alarm changes
-            values.putNull(RINGTONE);
-        } else {
-            values.put(RINGTONE, alarm.alert.toString());
-        }
-
-        return values;
-    }
-
-    public static Intent createIntent(Context context, Class<?> cls, long alarmId) {
-        return new Intent(context, cls).setData(getContentUri(alarmId));
-    }
-
-    public static Uri getContentUri(long alarmId) {
-        return ContentUris.withAppendedId(CONTENT_URI, alarmId);
-    }
-
-    public static long getId(Uri contentUri) {
-        return ContentUris.parseId(contentUri);
-    }
-
-    /**
-     * Get alarm cursor loader for all alarms.
-     *
-     * @param context to query the database.
-     * @return cursor loader with all the alarms.
-     */
-    public static CursorLoader getAlarmsCursorLoader(Context context) {
-        return new CursorLoader(context, ALARMS_WITH_INSTANCES_URI,
-                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();
-            }
-        };
-    }
-
-    /**
-     * Get alarm by id.
-     *
-     * @param cr provides access to the content model
-     * @param alarmId for the desired alarm.
-     * @return alarm if found, null otherwise
-     */
-    public static Alarm getAlarm(ContentResolver cr, long alarmId) {
-        try (Cursor cursor = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)) {
-            if (cursor.moveToFirst()) {
-                return new Alarm(cursor);
-            }
-        }
-
-        return null;
-    }
-    /**
-     * Get alarm for the {@code contentUri}.
-     *
-     * @param cr provides access to the content model
-     * @param contentUri the {@link #getContentUri deeplink} for the desired alarm
-     * @return instance if found, null otherwise
-     */
-    public static Alarm getAlarm(ContentResolver cr, Uri contentUri) {
-        return getAlarm(cr, ContentUris.parseId(contentUri));
-    }
-
-    /**
-     * Get all alarms given conditions.
-     *
-     * @param cr provides access to the content model
-     * @param selection A filter declaring which rows to return, formatted as an
-     *         SQL WHERE clause (excluding the WHERE itself). Passing null will
-     *         return all rows for the given URI.
-     * @param selectionArgs You may include ?s in selection, which will be
-     *         replaced by the values from selectionArgs, in the order that they
-     *         appear in the selection. The values will be bound as Strings.
-     * @return list of alarms matching where clause or empty list if none found.
-     */
-    public static List<Alarm> getAlarms(ContentResolver cr, String selection,
-            String... selectionArgs) {
-        final List<Alarm> result = new LinkedList<>();
-        try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
-            if (cursor != null && cursor.moveToFirst()) {
-                do {
-                    result.add(new Alarm(cursor));
-                } while (cursor.moveToNext());
-            }
-        }
-
-        return result;
-    }
-
-    public static boolean isTomorrow(Alarm alarm, Calendar now) {
-        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) {
-        ContentValues values = createContentValues(alarm);
-        Uri uri = contentResolver.insert(CONTENT_URI, values);
-        alarm.id = getId(uri);
-        return alarm;
-    }
-
-    public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) {
-        if (alarm.id == Alarm.INVALID_ID) return false;
-        ContentValues values = createContentValues(alarm);
-        long rowsUpdated = contentResolver.update(getContentUri(alarm.id), values, null, null);
-        return rowsUpdated == 1;
-    }
-
-    public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) {
-        if (alarmId == INVALID_ID) return false;
-        int deletedRows = contentResolver.delete(getContentUri(alarmId), "", null);
-        return deletedRows == 1;
-    }
-
-    public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
-        public Alarm createFromParcel(Parcel p) {
-            return new Alarm(p);
-        }
-
-        public Alarm[] newArray(int size) {
-            return new Alarm[size];
-        }
-    };
-
-    // Public fields
-    // TODO: Refactor instance names
-    public long id;
-    public boolean enabled;
-    public int hour;
-    public int minutes;
-    public Weekdays daysOfWeek;
-    public boolean vibrate;
-    public String label;
-    public Uri alert;
-    public boolean deleteAfterUse;
-    public int instanceState;
-    public int instanceId;
-
-    // Creates a default alarm at the current time.
-    public Alarm() {
-        this(0, 0);
-    }
-
-    public Alarm(int hour, int minutes) {
-        this.id = INVALID_ID;
-        this.hour = hour;
-        this.minutes = minutes;
-        this.vibrate = true;
-        this.daysOfWeek = Weekdays.NONE;
-        this.label = "";
-        this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
-        this.deleteAfterUse = false;
-    }
-
-    public Alarm(Cursor c) {
-        id = c.getLong(ID_INDEX);
-        enabled = c.getInt(ENABLED_INDEX) == 1;
-        hour = c.getInt(HOUR_INDEX);
-        minutes = c.getInt(MINUTES_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;
-
-        if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
-            instanceState = c.getInt(INSTANCE_STATE_INDEX);
-            instanceId = c.getInt(INSTANCE_ID_INDEX);
-        }
-
-        if (c.isNull(RINGTONE_INDEX)) {
-            // Should we be saving this with the current ringtone or leave it null
-            // so it changes when user changes default ringtone?
-            alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
-        } else {
-            alert = Uri.parse(c.getString(RINGTONE_INDEX));
-        }
-    }
-
-    Alarm(Parcel p) {
-        id = p.readLong();
-        enabled = p.readInt() == 1;
-        hour = p.readInt();
-        minutes = p.readInt();
-        daysOfWeek = Weekdays.fromBits(p.readInt());
-        vibrate = p.readInt() == 1;
-        label = p.readString();
-        alert = p.readParcelable(null);
-        deleteAfterUse = p.readInt() == 1;
-    }
-
-    /**
-     * @return the deeplink that identifies this alarm
-     */
-    public Uri getContentUri() {
-        return getContentUri(id);
-    }
-
-    public String getLabelOrDefault(Context context) {
-        return label.isEmpty() ? context.getString(R.string.default_label) : label;
-    }
-
-    /**
-     * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
-     * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
-     */
-    public boolean canPreemptivelyDismiss() {
-        return instanceState == AlarmInstance.SNOOZE_STATE
-                || instanceState == AlarmInstance.HIGH_NOTIFICATION_STATE
-                || instanceState == AlarmInstance.LOW_NOTIFICATION_STATE
-                || instanceState == AlarmInstance.HIDE_NOTIFICATION_STATE;
-    }
-
-    public void writeToParcel(Parcel p, int flags) {
-        p.writeLong(id);
-        p.writeInt(enabled ? 1 : 0);
-        p.writeInt(hour);
-        p.writeInt(minutes);
-        p.writeInt(daysOfWeek.getBits());
-        p.writeInt(vibrate ? 1 : 0);
-        p.writeString(label);
-        p.writeParcelable(alert, flags);
-        p.writeInt(deleteAfterUse ? 1 : 0);
-    }
-
-    public int describeContents() {
-        return 0;
-    }
-
-    public AlarmInstance createInstanceAfter(Calendar time) {
-        Calendar nextInstanceTime = getNextAlarmTime(time);
-        AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
-        result.mVibrate = vibrate;
-        result.mLabel = label;
-        result.mRingtone = alert;
-        return result;
-    }
-
-    /**
-     *
-     * @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());
-        previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
-        previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
-        previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
-        previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
-        previousInstanceTime.set(Calendar.MINUTE, minutes);
-        previousInstanceTime.set(Calendar.SECOND, 0);
-        previousInstanceTime.set(Calendar.MILLISECOND, 0);
-
-        final int subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime);
-        if (subtractDays > 0) {
-            previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays);
-            return previousInstanceTime;
-        } else {
-            return null;
-        }
-    }
-
-    public Calendar getNextAlarmTime(Calendar currentTime) {
-        final Calendar nextInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
-        nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
-        nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
-        nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
-        nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
-        nextInstanceTime.set(Calendar.MINUTE, minutes);
-        nextInstanceTime.set(Calendar.SECOND, 0);
-        nextInstanceTime.set(Calendar.MILLISECOND, 0);
-
-        // If we are still behind the passed in currentTime, then add a day
-        if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) {
-            nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
-        }
-
-        // The day of the week might be invalid, so find next valid one
-        final int addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime);
-        if (addDays > 0) {
-            nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
-        }
-
-        // Daylight Savings Time can alter the hours and minutes when adjusting the day above.
-        // Reset the desired hour and minute now that the correct day has been chosen.
-        nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
-        nextInstanceTime.set(Calendar.MINUTE, minutes);
-
-        return nextInstanceTime;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (!(o instanceof Alarm)) return false;
-        final Alarm other = (Alarm) o;
-        return id == other.id;
-    }
-
-    @Override
-    public int hashCode() {
-        return Long.valueOf(id).hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return "Alarm{" +
-                "alert=" + alert +
-                ", id=" + id +
-                ", enabled=" + enabled +
-                ", hour=" + hour +
-                ", minutes=" + minutes +
-                ", daysOfWeek=" + daysOfWeek +
-                ", vibrate=" + vibrate +
-                ", label='" + label + '\'' +
-                ", deleteAfterUse=" + deleteAfterUse +
-                '}';
-    }
-}
diff --git a/src/com/android/deskclock/provider/Alarm.kt b/src/com/android/deskclock/provider/Alarm.kt
new file mode 100644
index 0000000..7999b5c
--- /dev/null
+++ b/src/com/android/deskclock/provider/Alarm.kt
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.database.Cursor
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+import android.provider.BaseColumns
+import androidx.loader.content.CursorLoader
+
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Weekdays
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.util.Calendar
+import java.util.LinkedList
+
+class Alarm : Parcelable, AlarmsColumns {
+    // Public fields
+    // TODO: Refactor instance names
+    @JvmField
+    var id: Long
+
+    @JvmField
+    var enabled = false
+
+    @JvmField
+    var hour: Int
+
+    @JvmField
+    var minutes: Int
+
+    @JvmField
+    var daysOfWeek: Weekdays
+
+    @JvmField
+    var vibrate: Boolean
+
+    @JvmField
+    var label: String?
+
+    @JvmField
+    var alert: Uri? = null
+
+    @JvmField
+    var deleteAfterUse: Boolean
+
+    @JvmField
+    var instanceState = 0
+
+    var instanceId = 0
+
+    // Creates a default alarm at the current time.
+    @JvmOverloads
+    constructor(hour: Int = 0, minutes: Int = 0) {
+        id = INVALID_ID
+        this.hour = hour
+        this.minutes = minutes
+        vibrate = true
+        daysOfWeek = Weekdays.NONE
+        label = ""
+        alert = DataModel.dataModel.defaultAlarmRingtoneUri
+        deleteAfterUse = false
+    }
+
+    constructor(c: Cursor) {
+        id = c.getLong(ID_INDEX)
+        enabled = c.getInt(ENABLED_INDEX) == 1
+        hour = c.getInt(HOUR_INDEX)
+        minutes = c.getInt(MINUTES_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
+
+        if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
+            instanceState = c.getInt(INSTANCE_STATE_INDEX)
+            instanceId = c.getInt(INSTANCE_ID_INDEX)
+        }
+
+        alert = if (c.isNull(RINGTONE_INDEX)) {
+            // Should we be saving this with the current ringtone or leave it null
+            // so it changes when user changes default ringtone?
+            RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+        } else {
+            Uri.parse(c.getString(RINGTONE_INDEX))
+        }
+    }
+
+    internal constructor(p: Parcel) {
+        id = p.readLong()
+        enabled = p.readInt() == 1
+        hour = p.readInt()
+        minutes = p.readInt()
+        daysOfWeek = Weekdays.fromBits(p.readInt())
+        vibrate = p.readInt() == 1
+        label = p.readString()
+        alert = p.readParcelable(null)
+        deleteAfterUse = p.readInt() == 1
+    }
+
+    /**
+     * @return the deeplink that identifies this alarm
+     */
+    val contentUri: Uri
+        get() = getContentUri(id)
+
+    fun getLabelOrDefault(context: Context): String {
+        return if (label.isNullOrEmpty()) context.getString(R.string.default_label) else label!!
+    }
+
+    /**
+     * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
+     * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
+     */
+    fun canPreemptivelyDismiss(): Boolean {
+        return instanceState == InstancesColumns.SNOOZE_STATE ||
+                instanceState == InstancesColumns.HIGH_NOTIFICATION_STATE ||
+                instanceState == InstancesColumns.LOW_NOTIFICATION_STATE ||
+                instanceState == InstancesColumns.HIDE_NOTIFICATION_STATE
+    }
+
+    override fun writeToParcel(p: Parcel, flags: Int) {
+        p.writeLong(id)
+        p.writeInt(if (enabled) 1 else 0)
+        p.writeInt(hour)
+        p.writeInt(minutes)
+        p.writeInt(daysOfWeek.bits)
+        p.writeInt(if (vibrate) 1 else 0)
+        p.writeString(label)
+        p.writeParcelable(alert, flags)
+        p.writeInt(if (deleteAfterUse) 1 else 0)
+    }
+
+    override fun describeContents(): Int = 0
+
+    fun createInstanceAfter(time: Calendar): AlarmInstance {
+        val nextInstanceTime = getNextAlarmTime(time)
+        val result = AlarmInstance(nextInstanceTime, id)
+        result.mVibrate = vibrate
+        result.mLabel = label
+        result.mRingtone = alert
+        return result
+    }
+
+    /**
+     *
+     * @param currentTime the current time
+     * @return previous firing time, or null if this is a one-time alarm.
+     */
+    fun getPreviousAlarmTime(currentTime: Calendar): Calendar? {
+        val previousInstanceTime = Calendar.getInstance(currentTime.timeZone)
+        previousInstanceTime[Calendar.YEAR] = currentTime[Calendar.YEAR]
+        previousInstanceTime[Calendar.MONTH] = currentTime[Calendar.MONTH]
+        previousInstanceTime[Calendar.DAY_OF_MONTH] = currentTime[Calendar.DAY_OF_MONTH]
+        previousInstanceTime[Calendar.HOUR_OF_DAY] = hour
+        previousInstanceTime[Calendar.MINUTE] = minutes
+        previousInstanceTime[Calendar.SECOND] = 0
+        previousInstanceTime[Calendar.MILLISECOND] = 0
+
+        val subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime)
+        return if (subtractDays > 0) {
+            previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays)
+            previousInstanceTime
+        } else {
+            null
+        }
+    }
+
+    fun getNextAlarmTime(currentTime: Calendar): Calendar {
+        val nextInstanceTime = Calendar.getInstance(currentTime.timeZone)
+        nextInstanceTime[Calendar.YEAR] = currentTime[Calendar.YEAR]
+        nextInstanceTime[Calendar.MONTH] = currentTime[Calendar.MONTH]
+        nextInstanceTime[Calendar.DAY_OF_MONTH] = currentTime[Calendar.DAY_OF_MONTH]
+        nextInstanceTime[Calendar.HOUR_OF_DAY] = hour
+        nextInstanceTime[Calendar.MINUTE] = minutes
+        nextInstanceTime[Calendar.SECOND] = 0
+        nextInstanceTime[Calendar.MILLISECOND] = 0
+
+        // If we are still behind the passed in currentTime, then add a day
+        if (nextInstanceTime.timeInMillis <= currentTime.timeInMillis) {
+            nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1)
+        }
+
+        // The day of the week might be invalid, so find next valid one
+        val addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime)
+        if (addDays > 0) {
+            nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays)
+        }
+
+        // Daylight Savings Time can alter the hours and minutes when adjusting the day above.
+        // Reset the desired hour and minute now that the correct day has been chosen.
+        nextInstanceTime[Calendar.HOUR_OF_DAY] = hour
+        nextInstanceTime[Calendar.MINUTE] = minutes
+
+        return nextInstanceTime
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other !is Alarm) return false
+        return id == other.id
+    }
+
+    override fun hashCode(): Int {
+        return java.lang.Long.valueOf(id).hashCode()
+    }
+
+    override fun toString(): String {
+        return "Alarm{" +
+                "alert=" + alert +
+                ", id=" + id +
+                ", enabled=" + enabled +
+                ", hour=" + hour +
+                ", minutes=" + minutes +
+                ", daysOfWeek=" + daysOfWeek +
+                ", vibrate=" + vibrate +
+                ", label='" + label + '\'' +
+                ", deleteAfterUse=" + deleteAfterUse +
+                '}'
+    }
+
+    companion object {
+        /**
+         * Alarms start with an invalid id when it hasn't been saved to the database.
+         */
+        const val INVALID_ID: Long = -1
+
+        /**
+         * The default sort order for this table
+         */
+        private val DEFAULT_SORT_ORDER = ClockDatabaseHelper.ALARMS_TABLE_NAME + "." +
+                AlarmsColumns.HOUR + ", " + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." +
+                AlarmsColumns.MINUTES + " ASC" + ", " +
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + BaseColumns._ID + " DESC"
+
+        private val QUERY_COLUMNS = arrayOf(
+                BaseColumns._ID,
+                AlarmsColumns.HOUR,
+                AlarmsColumns.MINUTES,
+                AlarmsColumns.DAYS_OF_WEEK,
+                AlarmsColumns.ENABLED,
+                AlarmSettingColumns.VIBRATE,
+                AlarmSettingColumns.LABEL,
+                AlarmSettingColumns.RINGTONE,
+                AlarmsColumns.DELETE_AFTER_USE
+        )
+
+        private val QUERY_ALARMS_WITH_INSTANCES_COLUMNS = arrayOf(
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + BaseColumns._ID,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE,
+                ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + BaseColumns._ID,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL,
+                ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
+        )
+
+        /**
+         * These save calls to cursor.getColumnIndexOrThrow()
+         * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
+         */
+        private const val ID_INDEX = 0
+        private const val HOUR_INDEX = 1
+        private const val MINUTES_INDEX = 2
+        private const val DAYS_OF_WEEK_INDEX = 3
+        private const val ENABLED_INDEX = 4
+        private const val VIBRATE_INDEX = 5
+        private const val LABEL_INDEX = 6
+        private const val RINGTONE_INDEX = 7
+        private const val DELETE_AFTER_USE_INDEX = 8
+        private const val INSTANCE_STATE_INDEX = 9
+        const val INSTANCE_ID_INDEX = 10
+        const val INSTANCE_YEAR_INDEX = 11
+        const val INSTANCE_MONTH_INDEX = 12
+        const val INSTANCE_DAY_INDEX = 13
+        const val INSTANCE_HOUR_INDEX = 14
+        const val INSTANCE_MINUTE_INDEX = 15
+        const val INSTANCE_LABEL_INDEX = 16
+        const val INSTANCE_VIBRATE_INDEX = 17
+
+        private const val COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1
+        private const val ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1
+
+        @JvmStatic
+        fun createContentValues(alarm: Alarm): ContentValues {
+            val values = ContentValues(COLUMN_COUNT)
+            if (alarm.id != INVALID_ID) {
+                values.put(BaseColumns._ID, alarm.id)
+            }
+
+            values.put(AlarmsColumns.ENABLED, if (alarm.enabled) 1 else 0)
+            values.put(AlarmsColumns.HOUR, alarm.hour)
+            values.put(AlarmsColumns.MINUTES, alarm.minutes)
+            values.put(AlarmsColumns.DAYS_OF_WEEK, alarm.daysOfWeek.bits)
+            values.put(AlarmSettingColumns.VIBRATE, if (alarm.vibrate) 1 else 0)
+            values.put(AlarmSettingColumns.LABEL, alarm.label)
+            values.put(AlarmsColumns.DELETE_AFTER_USE, alarm.deleteAfterUse)
+            if (alarm.alert == null) {
+                // We want to put null, so default alarm changes
+                values.putNull(AlarmSettingColumns.RINGTONE)
+            } else {
+                values.put(AlarmSettingColumns.RINGTONE, alarm.alert.toString())
+            }
+            return values
+        }
+
+        @JvmStatic
+        fun createIntent(context: Context?, cls: Class<*>?, alarmId: Long): Intent {
+            return Intent(context, cls).setData(getContentUri(alarmId))
+        }
+
+        fun getContentUri(alarmId: Long): Uri {
+            return ContentUris.withAppendedId(AlarmsColumns.CONTENT_URI, alarmId)
+        }
+
+        fun getId(contentUri: Uri): Long {
+            return ContentUris.parseId(contentUri)
+        }
+
+        /**
+         * Get alarm cursor loader for all alarms.
+         *
+         * @param context to query the database.
+         * @return cursor loader with all the alarms.
+         */
+        @JvmStatic
+        fun getAlarmsCursorLoader(context: Context): CursorLoader {
+            return object : CursorLoader(context, AlarmsColumns.ALARMS_WITH_INSTANCES_URI,
+                    QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) {
+
+                override fun 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 fun loadInBackground(): Cursor? {
+                    // Prime the ringtone title cache for later access. Most alarms will refer to
+                    // system ringtones.
+                    DataModel.dataModel.loadRingtoneTitles()
+                    return super.loadInBackground()
+                }
+            }
+        }
+
+        /**
+         * Get alarm by id.
+         *
+         * @param cr provides access to the content model
+         * @param alarmId for the desired alarm.
+         * @return alarm if found, null otherwise
+         */
+        @JvmStatic
+        fun getAlarm(cr: ContentResolver, alarmId: Long): Alarm? {
+            val cursor: Cursor? = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)
+            cursor?.let {
+                if (cursor.moveToFirst()) {
+                    return Alarm(cursor)
+                }
+            }
+
+            return null
+        }
+
+        /**
+         * Get all alarms given conditions.
+         *
+         * @param cr provides access to the content model
+         * @param selection A filter declaring which rows to return, formatted as an
+         * SQL WHERE clause (excluding the WHERE itself). Passing null will
+         * return all rows for the given URI.
+         * @param selectionArgs You may include ?s in selection, which will be
+         * replaced by the values from selectionArgs, in the order that they
+         * appear in the selection. The values will be bound as Strings.
+         * @return list of alarms matching where clause or empty list if none found.
+         */
+        @JvmStatic
+        fun getAlarms(
+            cr: ContentResolver,
+            selection: String?,
+            vararg selectionArgs: String?
+        ): List<Alarm> {
+            val result: MutableList<Alarm> = LinkedList()
+            val cursor: Cursor? =
+                    cr.query(AlarmsColumns.CONTENT_URI, QUERY_COLUMNS,
+                            selection, selectionArgs, null)
+            cursor?.let {
+                if (cursor.moveToFirst()) {
+                    do {
+                        result.add(Alarm(cursor))
+                    } while (cursor.moveToNext())
+                }
+            }
+
+            return result
+        }
+
+        @JvmStatic
+        fun isTomorrow(alarm: Alarm, now: Calendar): Boolean {
+            if (alarm.instanceState == InstancesColumns.SNOOZE_STATE) {
+                return false
+            }
+
+            val totalAlarmMinutes = alarm.hour * 60 + alarm.minutes
+            val totalNowMinutes = now[Calendar.HOUR_OF_DAY] * 60 + now[Calendar.MINUTE]
+            return totalAlarmMinutes <= totalNowMinutes
+        }
+
+        @JvmStatic
+        fun addAlarm(contentResolver: ContentResolver, alarm: Alarm): Alarm {
+            val values: ContentValues = createContentValues(alarm)
+            val uri: Uri = contentResolver.insert(AlarmsColumns.CONTENT_URI, values)!!
+            alarm.id = getId(uri)
+            return alarm
+        }
+
+        @JvmStatic
+        fun updateAlarm(contentResolver: ContentResolver, alarm: Alarm): Boolean {
+            if (alarm.id == INVALID_ID) return false
+            val values: ContentValues = createContentValues(alarm)
+            val rowsUpdated: Long =
+                    contentResolver.update(getContentUri(alarm.id), values, null, null).toLong()
+            return rowsUpdated == 1L
+        }
+
+        @JvmStatic
+        fun deleteAlarm(contentResolver: ContentResolver, alarmId: Long): Boolean {
+            if (alarmId == INVALID_ID) return false
+            val deletedRows: Int = contentResolver.delete(getContentUri(alarmId), "", null)
+            return deletedRows == 1
+        }
+
+        val CREATOR: Parcelable.Creator<Alarm> = object : Parcelable.Creator<Alarm> {
+            override fun createFromParcel(p: Parcel): Alarm {
+                return Alarm(p)
+            }
+
+            override fun newArray(size: Int): Array<Alarm?> {
+                return arrayOfNulls(size)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/AlarmInstance.java b/src/com/android/deskclock/provider/AlarmInstance.java
deleted file mode 100644
index 9fb7a7b..0000000
--- a/src/com/android/deskclock/provider/AlarmInstance.java
+++ /dev/null
@@ -1,476 +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.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.data.DataModel;
-
-import java.util.Calendar;
-import java.util.LinkedList;
-import java.util.List;
-
-public final class AlarmInstance implements ClockContract.InstancesColumns {
-    /**
-     * Offset from alarm time to show low priority notification
-     */
-    public static final int LOW_NOTIFICATION_HOUR_OFFSET = -2;
-
-    /**
-     * Offset from alarm time to show high priority notification
-     */
-    public static final int HIGH_NOTIFICATION_MINUTE_OFFSET = -30;
-
-    /**
-     * Offset from alarm time to stop showing missed notification.
-     */
-    private static final int MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12;
-
-    /**
-     * AlarmInstances start with an invalid id when it hasn't been saved to the database.
-     */
-    public static final long INVALID_ID = -1;
-
-    private static final String[] QUERY_COLUMNS = {
-            _ID,
-            YEAR,
-            MONTH,
-            DAY,
-            HOUR,
-            MINUTES,
-            LABEL,
-            VIBRATE,
-            RINGTONE,
-            ALARM_ID,
-            ALARM_STATE
-    };
-
-    /**
-     * These save calls to cursor.getColumnIndexOrThrow()
-     * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
-     */
-    private static final int ID_INDEX = 0;
-    private static final int YEAR_INDEX = 1;
-    private static final int MONTH_INDEX = 2;
-    private static final int DAY_INDEX = 3;
-    private static final int HOUR_INDEX = 4;
-    private static final int MINUTES_INDEX = 5;
-    private static final int LABEL_INDEX = 6;
-    private static final int VIBRATE_INDEX = 7;
-    private static final int RINGTONE_INDEX = 8;
-    private static final int ALARM_ID_INDEX = 9;
-    private static final int ALARM_STATE_INDEX = 10;
-
-    private static final int COLUMN_COUNT = ALARM_STATE_INDEX + 1;
-
-    public static ContentValues createContentValues(AlarmInstance instance) {
-        ContentValues values = new ContentValues(COLUMN_COUNT);
-        if (instance.mId != INVALID_ID) {
-            values.put(_ID, instance.mId);
-        }
-
-        values.put(YEAR, instance.mYear);
-        values.put(MONTH, instance.mMonth);
-        values.put(DAY, instance.mDay);
-        values.put(HOUR, instance.mHour);
-        values.put(MINUTES, instance.mMinute);
-        values.put(LABEL, instance.mLabel);
-        values.put(VIBRATE, instance.mVibrate ? 1 : 0);
-        if (instance.mRingtone == null) {
-            // We want to put null in the database, so we'll be able
-            // to pick up on changes to the default alarm
-            values.putNull(RINGTONE);
-        } else {
-            values.put(RINGTONE, instance.mRingtone.toString());
-        }
-        values.put(ALARM_ID, instance.mAlarmId);
-        values.put(ALARM_STATE, instance.mAlarmState);
-        return values;
-    }
-
-    public static Intent createIntent(String action, long instanceId) {
-        return new Intent(action).setData(getContentUri(instanceId));
-    }
-
-    public static Intent createIntent(Context context, Class<?> cls, long instanceId) {
-        return new Intent(context, cls).setData(getContentUri(instanceId));
-    }
-
-    public static long getId(Uri contentUri) {
-        return ContentUris.parseId(contentUri);
-    }
-
-    /**
-     * @return the {@link Uri} identifying the alarm instance
-     */
-    public static Uri getContentUri(long instanceId) {
-        return ContentUris.withAppendedId(CONTENT_URI, instanceId);
-    }
-
-    /**
-     * Get alarm instance from instanceId.
-     *
-     * @param cr provides access to the content model
-     * @param instanceId for the desired instance.
-     * @return instance if found, null otherwise
-     */
-    public static AlarmInstance getInstance(ContentResolver cr, long instanceId) {
-        try (Cursor cursor = cr.query(getContentUri(instanceId), QUERY_COLUMNS, null, null, null)) {
-            if (cursor != null && cursor.moveToFirst()) {
-                return new AlarmInstance(cursor, false /* joinedTable */);
-            }
-        }
-
-        return null;
-    }
-
-    /**
-     * Get alarm instance for the {@code contentUri}.
-     *
-     * @param cr provides access to the content model
-     * @param contentUri the {@link #getContentUri deeplink} for the desired instance
-     * @return instance if found, null otherwise
-     */
-    public static AlarmInstance getInstance(ContentResolver cr, Uri contentUri) {
-        final long instanceId = ContentUris.parseId(contentUri);
-        return getInstance(cr, instanceId);
-    }
-
-    /**
-     * Get an alarm instances by alarmId.
-     *
-     * @param contentResolver provides access to the content model
-     * @param alarmId of instances desired.
-     * @return list of alarms instances that are owned by alarmId.
-     */
-    public static List<AlarmInstance> getInstancesByAlarmId(ContentResolver contentResolver,
-            long alarmId) {
-        return getInstances(contentResolver, ALARM_ID + "=" + alarmId);
-    }
-
-    /**
-     * Get the next instance of an alarm given its alarmId
-     * @param contentResolver provides access to the content model
-     * @param alarmId of instance desired
-     * @return the next instance of an alarm by alarmId.
-     */
-    public static AlarmInstance getNextUpcomingInstanceByAlarmId(ContentResolver contentResolver,
-                                                                 long alarmId) {
-        final List<AlarmInstance> alarmInstances = getInstancesByAlarmId(contentResolver, alarmId);
-        if (alarmInstances.isEmpty()) {
-            return null;
-        }
-        AlarmInstance nextAlarmInstance = alarmInstances.get(0);
-        for (AlarmInstance instance : alarmInstances) {
-            if (instance.getAlarmTime().before(nextAlarmInstance.getAlarmTime())) {
-                nextAlarmInstance = instance;
-            }
-        }
-        return nextAlarmInstance;
-    }
-
-    /**
-     * Get alarm instance by id and state.
-     */
-    public static List<AlarmInstance> getInstancesByInstanceIdAndState(
-            ContentResolver contentResolver, long alarmInstanceId, int state) {
-        return getInstances(contentResolver, _ID + "=" + alarmInstanceId + " AND " + ALARM_STATE +
-                "=" + state);
-    }
-
-    /**
-     * Get alarm instances in the specified state.
-     */
-    public static List<AlarmInstance> getInstancesByState(
-            ContentResolver contentResolver, int state) {
-        return getInstances(contentResolver, ALARM_STATE + "=" + state);
-    }
-
-    /**
-     * Get a list of instances given selection.
-     *
-     * @param cr provides access to the content model
-     * @param selection A filter declaring which rows to return, formatted as an
-     *         SQL WHERE clause (excluding the WHERE itself). Passing null will
-     *         return all rows for the given URI.
-     * @param selectionArgs You may include ?s in selection, which will be
-     *         replaced by the values from selectionArgs, in the order that they
-     *         appear in the selection. The values will be bound as Strings.
-     * @return list of alarms matching where clause or empty list if none found.
-     */
-    public static List<AlarmInstance> getInstances(ContentResolver cr, String selection,
-                                                   String... selectionArgs) {
-        final List<AlarmInstance> result = new LinkedList<>();
-        try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
-            if (cursor != null && cursor.moveToFirst()) {
-                do {
-                    result.add(new AlarmInstance(cursor, false /* joinedTable */));
-                } while (cursor.moveToNext());
-            }
-        }
-
-        return result;
-    }
-
-    public static AlarmInstance addInstance(ContentResolver contentResolver,
-            AlarmInstance instance) {
-        // Make sure we are not adding a duplicate instances. This is not a
-        // fix and should never happen. This is only a safe guard against bad code, and you
-        // should fix the root issue if you see the error message.
-        String dupSelector = AlarmInstance.ALARM_ID + " = " + instance.mAlarmId;
-        for (AlarmInstance otherInstances : getInstances(contentResolver, dupSelector)) {
-            if (otherInstances.getAlarmTime().equals(instance.getAlarmTime())) {
-                LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to "
-                        + instance);
-                // Copy over the new instance values and update the db
-                instance.mId = otherInstances.mId;
-                updateInstance(contentResolver, instance);
-                return instance;
-            }
-        }
-
-        ContentValues values = createContentValues(instance);
-        Uri uri = contentResolver.insert(CONTENT_URI, values);
-        instance.mId = getId(uri);
-        return instance;
-    }
-
-    public static boolean updateInstance(ContentResolver contentResolver, AlarmInstance instance) {
-        if (instance.mId == INVALID_ID) return false;
-        ContentValues values = createContentValues(instance);
-        long rowsUpdated = contentResolver.update(getContentUri(instance.mId), values, null, null);
-        return rowsUpdated == 1;
-    }
-
-    public static boolean deleteInstance(ContentResolver contentResolver, long instanceId) {
-        if (instanceId == INVALID_ID) return false;
-        int deletedRows = contentResolver.delete(getContentUri(instanceId), "", null);
-        return deletedRows == 1;
-    }
-
-    public static void deleteOtherInstances(Context context, ContentResolver contentResolver,
-            long alarmId, long instanceId) {
-        final List<AlarmInstance> instances = getInstancesByAlarmId(contentResolver, alarmId);
-        for (AlarmInstance instance : instances) {
-            if (instance.mId != instanceId) {
-                AlarmStateManager.unregisterInstance(context, instance);
-                deleteInstance(contentResolver, instance.mId);
-            }
-        }
-    }
-
-    // Public fields
-    public long mId;
-    public int mYear;
-    public int mMonth;
-    public int mDay;
-    public int mHour;
-    public int mMinute;
-    public String mLabel;
-    public boolean mVibrate;
-    public Uri mRingtone;
-    public Long mAlarmId;
-    public int mAlarmState;
-
-    public AlarmInstance(Calendar calendar, Long alarmId) {
-        this(calendar);
-        mAlarmId = alarmId;
-    }
-
-    public AlarmInstance(Calendar calendar) {
-        mId = INVALID_ID;
-        setAlarmTime(calendar);
-        mLabel = "";
-        mVibrate = false;
-        mRingtone = null;
-        mAlarmState = SILENT_STATE;
-    }
-
-    public AlarmInstance(AlarmInstance instance) {
-         this.mId = instance.mId;
-         this.mYear = instance.mYear;
-         this.mMonth = instance.mMonth;
-         this.mDay = instance.mDay;
-         this.mHour = instance.mHour;
-         this.mMinute = instance.mMinute;
-         this.mLabel = instance.mLabel;
-         this.mVibrate = instance.mVibrate;
-         this.mRingtone = instance.mRingtone;
-         this.mAlarmId = instance.mAlarmId;
-         this.mAlarmState = instance.mAlarmState;
-    }
-
-    public AlarmInstance(Cursor c, boolean joinedTable) {
-        if (joinedTable) {
-            mId = c.getLong(Alarm.INSTANCE_ID_INDEX);
-            mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX);
-            mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX);
-            mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX);
-            mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX);
-            mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX);
-            mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX);
-            mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1;
-        } else {
-            mId = c.getLong(ID_INDEX);
-            mYear = c.getInt(YEAR_INDEX);
-            mMonth = c.getInt(MONTH_INDEX);
-            mDay = c.getInt(DAY_INDEX);
-            mHour = c.getInt(HOUR_INDEX);
-            mMinute = c.getInt(MINUTES_INDEX);
-            mLabel = c.getString(LABEL_INDEX);
-            mVibrate = c.getInt(VIBRATE_INDEX) == 1;
-        }
-        if (c.isNull(RINGTONE_INDEX)) {
-            // Should we be saving this with the current ringtone or leave it null
-            // so it changes when user changes default ringtone?
-            mRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
-        } else {
-            mRingtone = Uri.parse(c.getString(RINGTONE_INDEX));
-        }
-
-        if (!c.isNull(ALARM_ID_INDEX)) {
-            mAlarmId = c.getLong(ALARM_ID_INDEX);
-        }
-        mAlarmState = c.getInt(ALARM_STATE_INDEX);
-    }
-
-    /**
-     * @return the deeplink that identifies this alarm instance
-     */
-    public Uri getContentUri() {
-        return getContentUri(mId);
-    }
-
-    public String getLabelOrDefault(Context context) {
-        return mLabel.isEmpty() ? context.getString(R.string.default_label) : mLabel;
-    }
-
-    public void setAlarmTime(Calendar calendar) {
-        mYear = calendar.get(Calendar.YEAR);
-        mMonth = calendar.get(Calendar.MONTH);
-        mDay = calendar.get(Calendar.DAY_OF_MONTH);
-        mHour = calendar.get(Calendar.HOUR_OF_DAY);
-        mMinute = calendar.get(Calendar.MINUTE);
-    }
-
-    /**
-     * Return the time when a alarm should fire.
-     *
-     * @return the time
-     */
-    public Calendar getAlarmTime() {
-        Calendar calendar = Calendar.getInstance();
-        calendar.set(Calendar.YEAR, mYear);
-        calendar.set(Calendar.MONTH, mMonth);
-        calendar.set(Calendar.DAY_OF_MONTH, mDay);
-        calendar.set(Calendar.HOUR_OF_DAY, mHour);
-        calendar.set(Calendar.MINUTE, mMinute);
-        calendar.set(Calendar.SECOND, 0);
-        calendar.set(Calendar.MILLISECOND, 0);
-        return calendar;
-    }
-
-    /**
-     * Return the time when a low priority notification should be shown.
-     *
-     * @return the time
-     */
-    public Calendar getLowNotificationTime() {
-        Calendar calendar = getAlarmTime();
-        calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET);
-        return calendar;
-    }
-
-    /**
-     * Return the time when a high priority notification should be shown.
-     *
-     * @return the time
-     */
-    public Calendar getHighNotificationTime() {
-        Calendar calendar = getAlarmTime();
-        calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET);
-        return calendar;
-    }
-
-    /**
-     * Return the time when a missed notification should be removed.
-     *
-     * @return the time
-     */
-    public Calendar getMissedTimeToLive() {
-        Calendar calendar = getAlarmTime();
-        calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET);
-        return calendar;
-    }
-
-    /**
-     * Return the time when the alarm should stop firing and be marked as missed.
-     *
-     * @return the time when alarm should be silence, or null if never
-     */
-    public Calendar getTimeout() {
-        final int timeoutMinutes = DataModel.getDataModel().getAlarmTimeout();
-
-        // Alarm silence has been set to "None"
-        if (timeoutMinutes < 0) {
-            return null;
-        }
-
-        Calendar calendar = getAlarmTime();
-        calendar.add(Calendar.MINUTE, timeoutMinutes);
-        return calendar;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (!(o instanceof AlarmInstance)) return false;
-        final AlarmInstance other = (AlarmInstance) o;
-        return mId == other.mId;
-    }
-
-    @Override
-    public int hashCode() {
-        return Long.valueOf(mId).hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return "AlarmInstance{" +
-                "mId=" + mId +
-                ", mYear=" + mYear +
-                ", mMonth=" + mMonth +
-                ", mDay=" + mDay +
-                ", mHour=" + mHour +
-                ", mMinute=" + mMinute +
-                ", mLabel=" + mLabel +
-                ", mVibrate=" + mVibrate +
-                ", mRingtone=" + mRingtone +
-                ", mAlarmId=" + mAlarmId +
-                ", mAlarmState=" + mAlarmState +
-                '}';
-    }
-}
diff --git a/src/com/android/deskclock/provider/AlarmInstance.kt b/src/com/android/deskclock/provider/AlarmInstance.kt
new file mode 100644
index 0000000..d69ae5b
--- /dev/null
+++ b/src/com/android/deskclock/provider/AlarmInstance.kt
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.database.Cursor
+import android.media.RingtoneManager
+import android.net.Uri
+import android.provider.BaseColumns._ID
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.util.Calendar
+import java.util.LinkedList
+
+class AlarmInstance : InstancesColumns {
+    // Public fields
+    var mYear = 0
+    var mMonth = 0
+    var mDay = 0
+    var mHour = 0
+    var mMinute = 0
+
+    @JvmField
+    var mId: Long = 0
+
+    @JvmField
+    var mLabel: String? = null
+
+    @JvmField
+    var mVibrate = false
+
+    @JvmField
+    var mRingtone: Uri? = null
+
+    @JvmField
+    var mAlarmId: Long? = null
+
+    @JvmField
+    var mAlarmState: Int
+
+    constructor(calendar: Calendar, alarmId: Long?) : this(calendar) {
+        mAlarmId = alarmId
+    }
+
+    constructor(calendar: Calendar) {
+        mId = INVALID_ID
+        alarmTime = calendar
+        mLabel = ""
+        mVibrate = false
+        mRingtone = null
+        mAlarmState = InstancesColumns.SILENT_STATE
+    }
+
+    constructor(instance: AlarmInstance) {
+        mId = instance.mId
+        mYear = instance.mYear
+        mMonth = instance.mMonth
+        mDay = instance.mDay
+        mHour = instance.mHour
+        mMinute = instance.mMinute
+        mLabel = instance.mLabel
+        mVibrate = instance.mVibrate
+        mRingtone = instance.mRingtone
+        mAlarmId = instance.mAlarmId
+        mAlarmState = instance.mAlarmState
+    }
+
+    constructor(c: Cursor, joinedTable: Boolean) {
+        if (joinedTable) {
+            mId = c.getLong(Alarm.INSTANCE_ID_INDEX)
+            mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX)
+            mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX)
+            mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX)
+            mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX)
+            mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX)
+            mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX)
+            mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1
+        } else {
+            mId = c.getLong(ID_INDEX)
+            mYear = c.getInt(YEAR_INDEX)
+            mMonth = c.getInt(MONTH_INDEX)
+            mDay = c.getInt(DAY_INDEX)
+            mHour = c.getInt(HOUR_INDEX)
+            mMinute = c.getInt(MINUTES_INDEX)
+            mLabel = c.getString(LABEL_INDEX)
+            mVibrate = c.getInt(VIBRATE_INDEX) == 1
+        }
+        mRingtone = if (c.isNull(RINGTONE_INDEX)) {
+            // Should we be saving this with the current ringtone or leave it null
+            // so it changes when user changes default ringtone?
+            RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+        } else {
+            Uri.parse(c.getString(RINGTONE_INDEX))
+        }
+
+        if (!c.isNull(ALARM_ID_INDEX)) {
+            mAlarmId = c.getLong(ALARM_ID_INDEX)
+        }
+        mAlarmState = c.getInt(ALARM_STATE_INDEX)
+    }
+
+    /**
+     * @return the deeplink that identifies this alarm instance
+     */
+    val contentUri: Uri
+        get() = getContentUri(mId)
+
+    fun getLabelOrDefault(context: Context): String {
+        return if (mLabel.isNullOrEmpty()) context.getString(R.string.default_label) else mLabel!!
+    }
+
+    /**
+     * Return the time when a alarm should fire.
+     *
+     * @return the time
+     */
+    var alarmTime: Calendar
+        get() {
+            val calendar = Calendar.getInstance()
+            calendar[Calendar.YEAR] = mYear
+            calendar[Calendar.MONTH] = mMonth
+            calendar[Calendar.DAY_OF_MONTH] = mDay
+            calendar[Calendar.HOUR_OF_DAY] = mHour
+            calendar[Calendar.MINUTE] = mMinute
+            calendar[Calendar.SECOND] = 0
+            calendar[Calendar.MILLISECOND] = 0
+            return calendar
+        }
+        set(calendar) {
+            mYear = calendar[Calendar.YEAR]
+            mMonth = calendar[Calendar.MONTH]
+            mDay = calendar[Calendar.DAY_OF_MONTH]
+            mHour = calendar[Calendar.HOUR_OF_DAY]
+            mMinute = calendar[Calendar.MINUTE]
+        }
+
+    /**
+     * Return the time when a low priority notification should be shown.
+     *
+     * @return the time
+     */
+    val lowNotificationTime: Calendar
+        get() {
+            val calendar = alarmTime
+            calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET)
+            return calendar
+        }
+
+    /**
+     * Return the time when a high priority notification should be shown.
+     *
+     * @return the time
+     */
+    val highNotificationTime: Calendar
+        get() {
+            val calendar = alarmTime
+            calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET)
+            return calendar
+        }
+
+    /**
+     * Return the time when a missed notification should be removed.
+     *
+     * @return the time
+     */
+    val missedTimeToLive: Calendar
+        get() {
+            val calendar = alarmTime
+            calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET)
+            return calendar
+        }
+
+    /**
+     * Return the time when the alarm should stop firing and be marked as missed.
+     *
+     * @return the time when alarm should be silence, or null if never
+     */
+    val timeout: Calendar?
+        get() {
+            val timeoutMinutes = DataModel.dataModel.alarmTimeout
+
+            // Alarm silence has been set to "None"
+            if (timeoutMinutes < 0) {
+                return null
+            }
+
+            val calendar = alarmTime
+            calendar.add(Calendar.MINUTE, timeoutMinutes)
+            return calendar
+        }
+
+    override fun equals(other: Any?): Boolean {
+        if (other !is AlarmInstance) return false
+        return mId == other.mId
+    }
+
+    override fun hashCode(): Int {
+        return java.lang.Long.valueOf(mId).hashCode()
+    }
+
+    override fun toString(): String {
+        return "AlarmInstance{" +
+                "mId=" + mId +
+                ", mYear=" + mYear +
+                ", mMonth=" + mMonth +
+                ", mDay=" + mDay +
+                ", mHour=" + mHour +
+                ", mMinute=" + mMinute +
+                ", mLabel=" + mLabel +
+                ", mVibrate=" + mVibrate +
+                ", mRingtone=" + mRingtone +
+                ", mAlarmId=" + mAlarmId +
+                ", mAlarmState=" + mAlarmState +
+                '}'
+    }
+
+    companion object {
+        /**
+         * Offset from alarm time to show low priority notification
+         */
+        const val LOW_NOTIFICATION_HOUR_OFFSET = -2
+
+        /**
+         * Offset from alarm time to show high priority notification
+         */
+        const val HIGH_NOTIFICATION_MINUTE_OFFSET = -30
+
+        /**
+         * Offset from alarm time to stop showing missed notification.
+         */
+        private const val MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12
+
+        /**
+         * AlarmInstances start with an invalid id when it hasn't been saved to the database.
+         */
+        const val INVALID_ID: Long = -1
+
+        private val QUERY_COLUMNS = arrayOf(
+                _ID,
+                InstancesColumns.YEAR,
+                InstancesColumns.MONTH,
+                InstancesColumns.DAY,
+                InstancesColumns.HOUR,
+                InstancesColumns.MINUTES,
+                AlarmSettingColumns.LABEL,
+                AlarmSettingColumns.VIBRATE,
+                AlarmSettingColumns.RINGTONE,
+                InstancesColumns.ALARM_ID,
+                InstancesColumns.ALARM_STATE
+        )
+
+        /**
+         * These save calls to cursor.getColumnIndexOrThrow()
+         * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
+         */
+        private const val ID_INDEX = 0
+        private const val YEAR_INDEX = 1
+        private const val MONTH_INDEX = 2
+        private const val DAY_INDEX = 3
+        private const val HOUR_INDEX = 4
+        private const val MINUTES_INDEX = 5
+        private const val LABEL_INDEX = 6
+        private const val VIBRATE_INDEX = 7
+        private const val RINGTONE_INDEX = 8
+        private const val ALARM_ID_INDEX = 9
+        private const val ALARM_STATE_INDEX = 10
+
+        private const val COLUMN_COUNT = ALARM_STATE_INDEX + 1
+
+        @JvmStatic
+        fun createContentValues(instance: AlarmInstance): ContentValues {
+            val values = ContentValues(COLUMN_COUNT)
+            if (instance.mId != INVALID_ID) {
+                values.put(_ID, instance.mId)
+            }
+
+            values.put(InstancesColumns.YEAR, instance.mYear)
+            values.put(InstancesColumns.MONTH, instance.mMonth)
+            values.put(InstancesColumns.DAY, instance.mDay)
+            values.put(InstancesColumns.HOUR, instance.mHour)
+            values.put(InstancesColumns.MINUTES, instance.mMinute)
+            values.put(AlarmSettingColumns.LABEL, instance.mLabel)
+            values.put(AlarmSettingColumns.VIBRATE, if (instance.mVibrate) 1 else 0)
+            if (instance.mRingtone == null) {
+                // We want to put null in the database, so we'll be able
+                // to pick up on changes to the default alarm
+                values.putNull(AlarmSettingColumns.RINGTONE)
+            } else {
+                values.put(AlarmSettingColumns.RINGTONE, instance.mRingtone.toString())
+            }
+            values.put(InstancesColumns.ALARM_ID, instance.mAlarmId)
+            values.put(InstancesColumns.ALARM_STATE, instance.mAlarmState)
+            return values
+        }
+
+        fun createIntent(action: String?, instanceId: Long): Intent {
+            return Intent(action).setData(getContentUri(instanceId))
+        }
+
+        @JvmStatic
+        fun createIntent(context: Context?, cls: Class<*>?, instanceId: Long): Intent {
+            return Intent(context, cls).setData(getContentUri(instanceId))
+        }
+
+        @JvmStatic
+        fun getId(contentUri: Uri): Long {
+            return ContentUris.parseId(contentUri)
+        }
+
+        /**
+         * @return the [Uri] identifying the alarm instance
+         */
+        fun getContentUri(instanceId: Long): Uri {
+            return ContentUris.withAppendedId(InstancesColumns.CONTENT_URI, instanceId)
+        }
+
+        /**
+         * Get alarm instance from instanceId.
+         *
+         * @param cr provides access to the content model
+         * @param instanceId for the desired instance.
+         * @return instance if found, null otherwise
+         */
+        @JvmStatic
+        fun getInstance(cr: ContentResolver, instanceId: Long): AlarmInstance? {
+            val cursor: Cursor? =
+                    cr.query(getContentUri(instanceId), QUERY_COLUMNS, null, null, null)
+            cursor?.let {
+                if (cursor.moveToFirst()) {
+                    return AlarmInstance(cursor, false /* joinedTable */)
+                }
+            }
+            return null
+        }
+
+        /**
+         * Get alarm instance for the `contentUri`.
+         *
+         * @param cr provides access to the content model
+         * @param contentUri the [deeplink][.getContentUri] for the desired instance
+         * @return instance if found, null otherwise
+         */
+        fun getInstance(cr: ContentResolver, contentUri: Uri): AlarmInstance? {
+            val instanceId: Long = ContentUris.parseId(contentUri)
+            return getInstance(cr, instanceId)
+        }
+
+        /**
+         * Get an alarm instances by alarmId.
+         *
+         * @param contentResolver provides access to the content model
+         * @param alarmId of instances desired.
+         * @return list of alarms instances that are owned by alarmId.
+         */
+        @JvmStatic
+        fun getInstancesByAlarmId(
+            contentResolver: ContentResolver,
+            alarmId: Long
+        ): List<AlarmInstance> {
+            return getInstances(contentResolver, InstancesColumns.ALARM_ID + "=" + alarmId)
+        }
+
+        /**
+         * Get the next instance of an alarm given its alarmId
+         * @param contentResolver provides access to the content model
+         * @param alarmId of instance desired
+         * @return the next instance of an alarm by alarmId.
+         */
+        @JvmStatic
+        fun getNextUpcomingInstanceByAlarmId(
+            contentResolver: ContentResolver,
+            alarmId: Long
+        ): AlarmInstance? {
+            val alarmInstances = getInstancesByAlarmId(contentResolver, alarmId)
+            if (alarmInstances.isEmpty()) {
+                return null
+            }
+            var nextAlarmInstance = alarmInstances[0]
+            for (instance in alarmInstances) {
+                if (instance.alarmTime.before(nextAlarmInstance.alarmTime)) {
+                    nextAlarmInstance = instance
+                }
+            }
+            return nextAlarmInstance
+        }
+
+        /**
+         * Get alarm instance by id and state.
+         */
+        fun getInstancesByInstanceIdAndState(
+            contentResolver: ContentResolver,
+            alarmInstanceId: Long,
+            state: Int
+        ): List<AlarmInstance> {
+            return getInstances(contentResolver,
+                    _ID.toString() + "=" + alarmInstanceId + " AND " +
+                            InstancesColumns.ALARM_STATE + "=" + state)
+        }
+
+        /**
+         * Get alarm instances in the specified state.
+         */
+        @JvmStatic
+        fun getInstancesByState(
+            contentResolver: ContentResolver,
+            state: Int
+        ): List<AlarmInstance> {
+            return getInstances(contentResolver,
+                    InstancesColumns.ALARM_STATE + "=" + state)
+        }
+
+        /**
+         * Get a list of instances given selection.
+         *
+         * @param cr provides access to the content model
+         * @param selection A filter declaring which rows to return, formatted as an
+         * SQL WHERE clause (excluding the WHERE itself). Passing null will
+         * return all rows for the given URI.
+         * @param selectionArgs You may include ?s in selection, which will be
+         * replaced by the values from selectionArgs, in the order that they
+         * appear in the selection. The values will be bound as Strings.
+         * @return list of alarms matching where clause or empty list if none found.
+         */
+        @JvmStatic
+        fun getInstances(
+            cr: ContentResolver,
+            selection: String?,
+            vararg selectionArgs: String?
+        ): MutableList<AlarmInstance> {
+            val result: MutableList<AlarmInstance> = LinkedList()
+            val cursor: Cursor? =
+                    cr.query(InstancesColumns.CONTENT_URI, QUERY_COLUMNS,
+                            selection, selectionArgs, null)
+            cursor?.let {
+                if (cursor.moveToFirst()) {
+                    do {
+                        result.add(AlarmInstance(cursor, false /* joinedTable */))
+                    } while (cursor.moveToNext())
+                }
+            }
+
+            return result
+        }
+
+        @JvmStatic
+        fun addInstance(
+            contentResolver: ContentResolver,
+            instance: AlarmInstance
+        ): AlarmInstance {
+            // Make sure we are not adding a duplicate instances. This is not a
+            // fix and should never happen. This is only a safe guard against bad code, and you
+            // should fix the root issue if you see the error message.
+            val dupSelector = InstancesColumns.ALARM_ID + " = " + instance.mAlarmId
+            for (otherInstances in getInstances(contentResolver, dupSelector)) {
+                if (otherInstances.alarmTime == instance.alarmTime) {
+                    LogUtils.i("Detected duplicate instance in DB. Updating " +
+                            otherInstances + " to " + instance)
+                    // Copy over the new instance values and update the db
+                    instance.mId = otherInstances.mId
+                    updateInstance(contentResolver, instance)
+                    return instance
+                }
+            }
+
+            val values: ContentValues = createContentValues(instance)
+            val uri: Uri = contentResolver.insert(InstancesColumns.CONTENT_URI, values)!!
+            instance.mId = getId(uri)
+            return instance
+        }
+
+        @JvmStatic
+        fun updateInstance(contentResolver: ContentResolver, instance: AlarmInstance): Boolean {
+            if (instance.mId == INVALID_ID) return false
+            val values: ContentValues = createContentValues(instance)
+            val rowsUpdated: Long =
+                    contentResolver.update(getContentUri(instance.mId), values, null, null).toLong()
+            return rowsUpdated == 1L
+        }
+
+        @JvmStatic
+        fun deleteInstance(contentResolver: ContentResolver, instanceId: Long): Boolean {
+            if (instanceId == INVALID_ID) return false
+            val deletedRows: Int = contentResolver.delete(getContentUri(instanceId), "", null)
+            return deletedRows == 1
+        }
+
+        @JvmStatic
+        fun deleteOtherInstances(
+            context: Context,
+            contentResolver: ContentResolver,
+            alarmId: Long,
+            instanceId: Long
+        ) {
+            val instances = getInstancesByAlarmId(contentResolver, alarmId)
+            for (instance in instances) {
+                if (instance.mId != instanceId) {
+                    AlarmStateManager.unregisterInstance(context, instance)
+                    deleteInstance(contentResolver, instance.mId)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/ClockContract.java b/src/com/android/deskclock/provider/ClockContract.java
deleted file mode 100644
index 5335ccc..0000000
--- a/src/com/android/deskclock/provider/ClockContract.java
+++ /dev/null
@@ -1,264 +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.net.Uri;
-import android.provider.BaseColumns;
-
-import com.android.deskclock.BuildConfig;
-
-/**
- * <p>
- * The contract between the clock provider and desk clock. Contains
- * definitions for the supported URIs and data columns.
- * </p>
- * <h3>Overview</h3>
- * <p>
- * ClockContract defines the data model of clock related information.
- * This data is stored in a number of tables:
- * </p>
- * <ul>
- * <li>The {@link AlarmsColumns} table holds the user created alarms</li>
- * <li>The {@link InstancesColumns} table holds the current state of each
- * alarm in the AlarmsColumn table.
- * </li>
- * </ul>
- */
-public final class ClockContract {
-    /**
-     * This authority is used for writing to or querying from the clock
-     * provider.
-     */
-    public static final String AUTHORITY = BuildConfig.APPLICATION_ID;
-
-    /**
-     * This utility class cannot be instantiated
-     */
-    private ClockContract() {}
-
-    /**
-     * Constants for tables with AlarmSettings.
-     */
-    private interface AlarmSettingColumns extends BaseColumns {
-        /**
-         * This string is used to indicate no ringtone.
-         */
-        Uri NO_RINGTONE_URI = Uri.EMPTY;
-
-        /**
-         * This string is used to indicate no ringtone.
-         */
-        String NO_RINGTONE = NO_RINGTONE_URI.toString();
-
-        /**
-         * True if alarm should vibrate
-         * <p>Type: BOOLEAN</p>
-         */
-        String VIBRATE = "vibrate";
-
-        /**
-         * Alarm label.
-         *
-         * <p>Type: STRING</p>
-         */
-        String LABEL = "label";
-
-        /**
-         * Audio alert to play when alarm triggers. Null entry
-         * means use system default and entry that equal
-         * Uri.EMPTY.toString() means no ringtone.
-         *
-         * <p>Type: STRING</p>
-         */
-        String RINGTONE = "ringtone";
-    }
-
-    /**
-     * Constants for the Alarms table, which contains the user created alarms.
-     */
-    protected interface AlarmsColumns extends AlarmSettingColumns, BaseColumns {
-        /**
-         * The content:// style URL for this table.
-         */
-        Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/alarms");
-
-        /**
-         * The content:// style URL for the alarms with instance tables, which is used to get the
-         * next firing instance and the current state of an alarm.
-         */
-        Uri ALARMS_WITH_INSTANCES_URI = Uri.parse("content://" + AUTHORITY
-                + "/alarms_with_instances");
-
-        /**
-         * Hour in 24-hour localtime 0 - 23.
-         * <p>Type: INTEGER</p>
-         */
-        String HOUR = "hour";
-
-        /**
-         * Minutes in localtime 0 - 59.
-         * <p>Type: INTEGER</p>
-         */
-        String MINUTES = "minutes";
-
-        /**
-         * Days of the week encoded as a bit set.
-         * <p>Type: INTEGER</p>
-         *
-         * {@link com.android.deskclock.data.Weekdays}
-         */
-        String DAYS_OF_WEEK = "daysofweek";
-
-        /**
-         * True if alarm is active.
-         * <p>Type: BOOLEAN</p>
-         */
-        String ENABLED = "enabled";
-
-        /**
-         * Determine if alarm is deleted after it has been used.
-         * <p>Type: INTEGER</p>
-         */
-        String DELETE_AFTER_USE = "delete_after_use";
-    }
-
-    /**
-     * Constants for the Instance table, which contains the state of each alarm.
-     */
-    protected interface InstancesColumns extends AlarmSettingColumns, BaseColumns {
-        /**
-         * The content:// style URL for this table.
-         */
-        Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/instances");
-
-        /**
-         * Alarm state when to show no notification.
-         *
-         * Can transitions to:
-         * LOW_NOTIFICATION_STATE
-         */
-        int SILENT_STATE = 0;
-
-        /**
-         * Alarm state to show low priority alarm notification.
-         *
-         * Can transitions to:
-         * HIDE_NOTIFICATION_STATE
-         * HIGH_NOTIFICATION_STATE
-         * DISMISSED_STATE
-         */
-        int LOW_NOTIFICATION_STATE = 1;
-
-        /**
-         * Alarm state to hide low priority alarm notification.
-         *
-         * Can transitions to:
-         * HIGH_NOTIFICATION_STATE
-         */
-        int HIDE_NOTIFICATION_STATE = 2;
-
-        /**
-         * Alarm state to show high priority alarm notification.
-         *
-         * Can transitions to:
-         * DISMISSED_STATE
-         * FIRED_STATE
-         */
-        int HIGH_NOTIFICATION_STATE = 3;
-
-        /**
-         * Alarm state when alarm is in snooze.
-         *
-         * Can transitions to:
-         * DISMISSED_STATE
-         * FIRED_STATE
-         */
-        int SNOOZE_STATE = 4;
-
-        /**
-         * Alarm state when alarm is being fired.
-         *
-         * Can transitions to:
-         * DISMISSED_STATE
-         * SNOOZED_STATE
-         * MISSED_STATE
-         */
-        int FIRED_STATE = 5;
-
-        /**
-         * Alarm state when alarm has been missed.
-         *
-         * Can transitions to:
-         * DISMISSED_STATE
-         */
-        int MISSED_STATE = 6;
-
-        /**
-         * Alarm state when alarm is done.
-         */
-        int DISMISSED_STATE = 7;
-
-        /**
-         * Alarm state when alarm has been dismissed before its intended firing time.
-         */
-        int PREDISMISSED_STATE = 8;
-
-        /**
-         * Alarm year.
-         *
-         * <p>Type: INTEGER</p>
-         */
-        String YEAR = "year";
-
-        /**
-         * Alarm month in year.
-         *
-         * <p>Type: INTEGER</p>
-         */
-        String MONTH = "month";
-
-        /**
-         * Alarm day in month.
-         *
-         * <p>Type: INTEGER</p>
-         */
-        String DAY = "day";
-
-        /**
-         * Alarm hour in 24-hour localtime 0 - 23.
-         * <p>Type: INTEGER</p>
-         */
-        String HOUR = "hour";
-
-        /**
-         * Alarm minutes in localtime 0 - 59
-         * <p>Type: INTEGER</p>
-         */
-        String MINUTES = "minutes";
-
-        /**
-         * Foreign key to Alarms table
-         * <p>Type: INTEGER (long)</p>
-         */
-        String ALARM_ID = "alarm_id";
-
-        /**
-         * Alarm state
-         * <p>Type: INTEGER</p>
-         */
-        String ALARM_STATE = "alarm_state";
-    }
-}
diff --git a/src/com/android/deskclock/provider/ClockContract.kt b/src/com/android/deskclock/provider/ClockContract.kt
new file mode 100644
index 0000000..ee922ce
--- /dev/null
+++ b/src/com/android/deskclock/provider/ClockContract.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.net.Uri
+import android.provider.BaseColumns
+
+import com.android.deskclock.BuildConfig
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+/**
+ * The contract between the clock provider and desk clock. Contains
+ * definitions for the supported URIs and data columns.
+ *
+ * <h3>Overview</h3>
+ *
+ * ClockContract defines the data model of clock related information.
+ * This data is stored in a number of tables:
+ *
+ *  * The [AlarmsColumns] table holds the user created alarms
+ *  * The [InstancesColumns] table holds the current state of each
+ * alarm in the AlarmsColumn table.
+ */
+object ClockContract {
+    /**
+     * This authority is used for writing to or querying from the clock
+     * provider.
+     */
+    @JvmField
+    val AUTHORITY: String = BuildConfig.APPLICATION_ID
+
+    /**
+     * Constants for tables with AlarmSettings.
+     */
+    interface AlarmSettingColumns : BaseColumns {
+        companion object {
+            /**
+             * This string is used to indicate no ringtone.
+             */
+            @JvmField
+            val NO_RINGTONE_URI: Uri = Uri.EMPTY
+
+            /**
+             * This string is used to indicate no ringtone.
+             */
+            @JvmField
+            val NO_RINGTONE: String = NO_RINGTONE_URI.toString()
+
+            /**
+             * True if alarm should vibrate
+             *
+             * Type: BOOLEAN
+             */
+            @JvmField
+            val VIBRATE = "vibrate"
+
+            /**
+             * Alarm label.
+             *
+             * Type: STRING
+             */
+            @JvmField
+            val LABEL = "label"
+
+            /**
+             * Audio alert to play when alarm triggers. Null entry
+             * means use system default and entry that equal
+             * Uri.EMPTY.toString() means no ringtone.
+             *
+             * Type: STRING
+             */
+            @JvmField
+            val RINGTONE = "ringtone"
+        }
+    }
+
+    /**
+     * Constants for the Alarms table, which contains the user created alarms.
+     */
+    interface AlarmsColumns : AlarmSettingColumns, BaseColumns {
+        companion object {
+            /**
+             * The content:// style URL for this table.
+             */
+            val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/alarms")
+
+            /**
+             * The content:// style URL for the alarms with instance tables, which is used to get the
+             * next firing instance and the current state of an alarm.
+             */
+            val ALARMS_WITH_INSTANCES_URI: Uri = Uri.parse("content://" + AUTHORITY +
+                    "/alarms_with_instances")
+
+            /**
+             * Hour in 24-hour localtime 0 - 23.
+             *
+             * Type: INTEGER
+             */
+            const val HOUR = "hour"
+
+            /**
+             * Minutes in localtime 0 - 59.
+             *
+             * Type: INTEGER
+             */
+            const val MINUTES = "minutes"
+
+            /**
+             * Days of the week encoded as a bit set.
+             *
+             * Type: INTEGER
+             *
+             * [com.android.deskclock.data.Weekdays]
+             */
+            const val DAYS_OF_WEEK = "daysofweek"
+
+            /**
+             * True if alarm is active.
+             *
+             * Type: BOOLEAN
+             */
+            const val ENABLED = "enabled"
+
+            /**
+             * Determine if alarm is deleted after it has been used.
+             *
+             * Type: INTEGER
+             */
+            const val DELETE_AFTER_USE = "delete_after_use"
+        }
+    }
+
+    /**
+     * Constants for the Instance table, which contains the state of each alarm.
+     */
+    interface InstancesColumns : AlarmSettingColumns, BaseColumns {
+        companion object {
+            /**
+             * The content:// style URL for this table.
+             */
+            val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/instances")
+
+            /**
+             * Alarm state when to show no notification.
+             *
+             * Can transitions to:
+             * LOW_NOTIFICATION_STATE
+             */
+            const val SILENT_STATE = 0
+
+            /**
+             * Alarm state to show low priority alarm notification.
+             *
+             * Can transitions to:
+             * HIDE_NOTIFICATION_STATE
+             * HIGH_NOTIFICATION_STATE
+             * DISMISSED_STATE
+             */
+            const val LOW_NOTIFICATION_STATE = 1
+
+            /**
+             * Alarm state to hide low priority alarm notification.
+             *
+             * Can transitions to:
+             * HIGH_NOTIFICATION_STATE
+             */
+            const val HIDE_NOTIFICATION_STATE = 2
+
+            /**
+             * Alarm state to show high priority alarm notification.
+             *
+             * Can transitions to:
+             * DISMISSED_STATE
+             * FIRED_STATE
+             */
+            const val HIGH_NOTIFICATION_STATE = 3
+
+            /**
+             * Alarm state when alarm is in snooze.
+             *
+             * Can transitions to:
+             * DISMISSED_STATE
+             * FIRED_STATE
+             */
+            const val SNOOZE_STATE = 4
+
+            /**
+             * Alarm state when alarm is being fired.
+             *
+             * Can transitions to:
+             * DISMISSED_STATE
+             * SNOOZED_STATE
+             * MISSED_STATE
+             */
+            const val FIRED_STATE = 5
+
+            /**
+             * Alarm state when alarm has been missed.
+             *
+             * Can transitions to:
+             * DISMISSED_STATE
+             */
+            const val MISSED_STATE = 6
+
+            /**
+             * Alarm state when alarm is done.
+             */
+            const val DISMISSED_STATE = 7
+
+            /**
+             * Alarm state when alarm has been dismissed before its intended firing time.
+             */
+            const val PREDISMISSED_STATE = 8
+
+            /**
+             * Alarm year.
+             *
+             * Type: INTEGER
+             */
+            const val YEAR = "year"
+
+            /**
+             * Alarm month in year.
+             *
+             * Type: INTEGER
+             */
+            const val MONTH = "month"
+
+            /**
+             * Alarm day in month.
+             *
+             * Type: INTEGER
+             */
+            const val DAY = "day"
+
+            /**
+             * Alarm hour in 24-hour localtime 0 - 23.
+             *
+             * Type: INTEGER
+             */
+            const val HOUR = "hour"
+
+            /**
+             * Alarm minutes in localtime 0 - 59
+             *
+             * Type: INTEGER
+             */
+            const val MINUTES = "minutes"
+
+            /**
+             * Foreign key to Alarms table
+             *
+             * Type: INTEGER (long)
+             */
+            const val ALARM_ID = "alarm_id"
+
+            /**
+             * Alarm state
+             *
+             * Type: INTEGER
+             */
+            const val ALARM_STATE = "alarm_state"
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/ClockDatabaseHelper.java b/src/com/android/deskclock/provider/ClockDatabaseHelper.java
deleted file mode 100644
index b6fc900..0000000
--- a/src/com/android/deskclock/provider/ClockDatabaseHelper.java
+++ /dev/null
@@ -1,232 +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.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.text.TextUtils;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.data.Weekdays;
-
-import java.util.Calendar;
-
-/**
- * Helper class for opening the database from multiple providers.  Also provides
- * some common functionality.
- */
-class ClockDatabaseHelper extends SQLiteOpenHelper {
-    /**
-     * Original Clock Database.
-     **/
-    private static final int VERSION_5 = 5;
-
-    /**
-     * Added alarm_instances table
-     * Added selected_cities table
-     * Added DELETE_AFTER_USE column to alarms table
-     */
-    private static final int VERSION_6 = 6;
-
-    /**
-     * Added alarm settings to instance table.
-     */
-    private static final int VERSION_7 = 7;
-
-    /**
-     * Removed selected_cities table.
-     */
-    private static final int VERSION_8 = 8;
-
-    // This creates a default alarm at 8:30 for every Mon,Tue,Wed,Thu,Fri
-    private static final String DEFAULT_ALARM_1 = "(8, 30, 31, 0, 1, '', NULL, 0);";
-
-    // This creates a default alarm at 9:30 for every Sat,Sun
-    private static final String DEFAULT_ALARM_2 = "(9, 00, 96, 0, 1, '', NULL, 0);";
-
-    // Database and table names
-    static final String DATABASE_NAME = "alarms.db";
-    static final String OLD_ALARMS_TABLE_NAME = "alarms";
-    static final String ALARMS_TABLE_NAME = "alarm_templates";
-    static final String INSTANCES_TABLE_NAME = "alarm_instances";
-    private static final String SELECTED_CITIES_TABLE_NAME = "selected_cities";
-
-    private static void createAlarmsTable(SQLiteDatabase db) {
-        db.execSQL("CREATE TABLE " + ALARMS_TABLE_NAME + " (" +
-                ClockContract.AlarmsColumns._ID + " INTEGER PRIMARY KEY," +
-                ClockContract.AlarmsColumns.HOUR + " INTEGER NOT NULL, " +
-                ClockContract.AlarmsColumns.MINUTES + " INTEGER NOT NULL, " +
-                ClockContract.AlarmsColumns.DAYS_OF_WEEK + " INTEGER NOT NULL, " +
-                ClockContract.AlarmsColumns.ENABLED + " INTEGER NOT NULL, " +
-                ClockContract.AlarmsColumns.VIBRATE + " INTEGER NOT NULL, " +
-                ClockContract.AlarmsColumns.LABEL + " TEXT NOT NULL, " +
-                ClockContract.AlarmsColumns.RINGTONE + " TEXT, " +
-                ClockContract.AlarmsColumns.DELETE_AFTER_USE + " INTEGER NOT NULL DEFAULT 0);");
-        LogUtils.i("Alarms Table created");
-    }
-
-    private static void createInstanceTable(SQLiteDatabase db) {
-        db.execSQL("CREATE TABLE " + INSTANCES_TABLE_NAME + " (" +
-                ClockContract.InstancesColumns._ID + " INTEGER PRIMARY KEY," +
-                ClockContract.InstancesColumns.YEAR + " INTEGER NOT NULL, " +
-                ClockContract.InstancesColumns.MONTH + " INTEGER NOT NULL, " +
-                ClockContract.InstancesColumns.DAY + " INTEGER NOT NULL, " +
-                ClockContract.InstancesColumns.HOUR + " INTEGER NOT NULL, " +
-                ClockContract.InstancesColumns.MINUTES + " INTEGER NOT NULL, " +
-                ClockContract.InstancesColumns.VIBRATE + " INTEGER NOT NULL, " +
-                ClockContract.InstancesColumns.LABEL + " TEXT NOT NULL, " +
-                ClockContract.InstancesColumns.RINGTONE + " TEXT, " +
-                ClockContract.InstancesColumns.ALARM_STATE + " INTEGER NOT NULL, " +
-                ClockContract.InstancesColumns.ALARM_ID + " INTEGER REFERENCES " +
-                    ALARMS_TABLE_NAME + "(" + ClockContract.AlarmsColumns._ID + ") " +
-                    "ON UPDATE CASCADE ON DELETE CASCADE" +
-                ");");
-        LogUtils.i("Instance table created");
-    }
-
-    public ClockDatabaseHelper(Context context) {
-        super(context, DATABASE_NAME, null, VERSION_8);
-    }
-
-    @Override
-    public void onCreate(SQLiteDatabase db) {
-        createAlarmsTable(db);
-        createInstanceTable(db);
-
-        // insert default alarms
-        LogUtils.i("Inserting default alarms");
-        String cs = ", "; //comma and space
-        String insertMe = "INSERT INTO " + ALARMS_TABLE_NAME + " (" +
-                ClockContract.AlarmsColumns.HOUR + cs +
-                ClockContract.AlarmsColumns.MINUTES + cs +
-                ClockContract.AlarmsColumns.DAYS_OF_WEEK + cs +
-                ClockContract.AlarmsColumns.ENABLED + cs +
-                ClockContract.AlarmsColumns.VIBRATE + cs +
-                ClockContract.AlarmsColumns.LABEL + cs +
-                ClockContract.AlarmsColumns.RINGTONE + cs +
-                ClockContract.AlarmsColumns.DELETE_AFTER_USE + ") VALUES ";
-        db.execSQL(insertMe + DEFAULT_ALARM_1);
-        db.execSQL(insertMe + DEFAULT_ALARM_2);
-    }
-
-    @Override
-    public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
-        LogUtils.v("Upgrading alarms database from version %d to %d", oldVersion, currentVersion);
-
-        if (oldVersion <= VERSION_7) {
-            // This was not used in VERSION_7 or prior, so we can just drop it.
-            db.execSQL("DROP TABLE IF EXISTS " + SELECTED_CITIES_TABLE_NAME + ";");
-        }
-
-        if (oldVersion <= VERSION_6) {
-            // This was not used in VERSION_6 or prior, so we can just drop it.
-            db.execSQL("DROP TABLE IF EXISTS " + INSTANCES_TABLE_NAME + ";");
-
-            // Create new alarms table and copy over the data
-            createAlarmsTable(db);
-            createInstanceTable(db);
-
-            LogUtils.i("Copying old alarms to new table");
-            final String[] OLD_TABLE_COLUMNS = {
-                    "_id",
-                    "hour",
-                    "minutes",
-                    "daysofweek",
-                    "enabled",
-                    "vibrate",
-                    "message",
-                    "alert",
-            };
-            try (Cursor cursor = db.query(OLD_ALARMS_TABLE_NAME, OLD_TABLE_COLUMNS,
-                    null, null, null, null, null)) {
-                final Calendar currentTime = Calendar.getInstance();
-                while (cursor != null && cursor.moveToNext()) {
-                    final Alarm alarm = new Alarm();
-                    alarm.id = cursor.getLong(0);
-                    alarm.hour = cursor.getInt(1);
-                    alarm.minutes = cursor.getInt(2);
-                    alarm.daysOfWeek = Weekdays.fromBits(cursor.getInt(3));
-                    alarm.enabled = cursor.getInt(4) == 1;
-                    alarm.vibrate = cursor.getInt(5) == 1;
-                    alarm.label = cursor.getString(6);
-
-                    final String alertString = cursor.getString(7);
-                    if ("silent".equals(alertString)) {
-                        alarm.alert = Alarm.NO_RINGTONE_URI;
-                    } else {
-                        alarm.alert =
-                                TextUtils.isEmpty(alertString) ? null : Uri.parse(alertString);
-                    }
-
-                    // Save new version of alarm and create alarm instance for it
-                    db.insert(ALARMS_TABLE_NAME, null, Alarm.createContentValues(alarm));
-                    if (alarm.enabled) {
-                        AlarmInstance newInstance = alarm.createInstanceAfter(currentTime);
-                        db.insert(INSTANCES_TABLE_NAME, null,
-                                AlarmInstance.createContentValues(newInstance));
-                    }
-                }
-            }
-
-            LogUtils.i("Dropping old alarm table");
-            db.execSQL("DROP TABLE IF EXISTS " + OLD_ALARMS_TABLE_NAME + ";");
-        }
-    }
-
-    long fixAlarmInsert(ContentValues values) {
-        // Why are we doing this? Is this not a programming bug if we try to
-        // insert an already used id?
-        final SQLiteDatabase db = getWritableDatabase();
-        db.beginTransaction();
-        long rowId = -1;
-        try {
-            // Check if we are trying to re-use an existing id.
-            final Object value = values.get(ClockContract.AlarmsColumns._ID);
-            if (value != null) {
-                long id = (Long) value;
-                if (id > -1) {
-                    final String[] columns = {ClockContract.AlarmsColumns._ID};
-                    final String selection = ClockContract.AlarmsColumns._ID + " = ?";
-                    final String[] selectionArgs = {String.valueOf(id)};
-                    try (Cursor cursor = db.query(ALARMS_TABLE_NAME, columns, selection,
-                            selectionArgs, null, null, null)) {
-                        if (cursor.moveToFirst()) {
-                            // Record exists. Remove the id so sqlite can generate a new one.
-                            values.putNull(ClockContract.AlarmsColumns._ID);
-                        }
-                    }
-                }
-            }
-
-            rowId = db.insert(ALARMS_TABLE_NAME, ClockContract.AlarmsColumns.RINGTONE, values);
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
-        if (rowId < 0) {
-            throw new SQLException("Failed to insert row");
-        }
-        LogUtils.v("Added alarm rowId = " + rowId);
-
-        return rowId;
-    }
-}
diff --git a/src/com/android/deskclock/provider/ClockDatabaseHelper.kt b/src/com/android/deskclock/provider/ClockDatabaseHelper.kt
new file mode 100644
index 0000000..d37e1a0
--- /dev/null
+++ b/src/com/android/deskclock/provider/ClockDatabaseHelper.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.database.SQLException
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+import android.net.Uri
+import android.provider.BaseColumns
+import android.text.TextUtils
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.data.Weekdays
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.util.Calendar
+
+/**
+ * Helper class for opening the database from multiple providers.  Also provides
+ * some common functionality.
+ */
+class ClockDatabaseHelper(context: Context)
+    : SQLiteOpenHelper(context, DATABASE_NAME, null, VERSION_8) {
+
+    override fun onCreate(db: SQLiteDatabase) {
+        createAlarmsTable(db)
+        createInstanceTable(db)
+
+        // insert default alarms
+        LogUtils.i("Inserting default alarms")
+        val cs: String = ", " // comma and space
+        val insertMe: String = "INSERT INTO " + ALARMS_TABLE_NAME + " (" +
+                AlarmsColumns.HOUR + cs +
+                AlarmsColumns.MINUTES + cs +
+                AlarmsColumns.DAYS_OF_WEEK + cs +
+                AlarmsColumns.ENABLED + cs +
+                AlarmSettingColumns.VIBRATE + cs +
+                AlarmSettingColumns.LABEL + cs +
+                AlarmSettingColumns.RINGTONE + cs +
+                AlarmsColumns.DELETE_AFTER_USE + ") VALUES "
+        db.execSQL(insertMe + DEFAULT_ALARM_1)
+        db.execSQL(insertMe + DEFAULT_ALARM_2)
+    }
+
+    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, currentVersion: Int) {
+        LogUtils.v("Upgrading alarms database from version %d to %d",
+                oldVersion, currentVersion)
+
+        if (oldVersion <= VERSION_7) {
+            // This was not used in VERSION_7 or prior, so we can just drop it.
+            db.execSQL("DROP TABLE IF EXISTS " + SELECTED_CITIES_TABLE_NAME + ";")
+        }
+
+        if (oldVersion <= VERSION_6) {
+            // This was not used in VERSION_6 or prior, so we can just drop it.
+            db.execSQL("DROP TABLE IF EXISTS " + INSTANCES_TABLE_NAME + ";")
+
+            // Create new alarms table and copy over the data
+            createAlarmsTable(db)
+            createInstanceTable(db)
+
+            LogUtils.i("Copying old alarms to new table")
+            val OLD_TABLE_COLUMNS: Array<String> = arrayOf(
+                    "_id",
+                    "hour",
+                    "minutes",
+                    "daysofweek",
+                    "enabled",
+                    "vibrate",
+                    "message",
+                    "alert"
+            )
+            val cursor: Cursor? =
+                    db.query(OLD_ALARMS_TABLE_NAME, OLD_TABLE_COLUMNS, null, null, null, null, null)
+            val currentTime: Calendar = Calendar.getInstance()
+            while (cursor != null && cursor.moveToNext()) {
+                val alarm = Alarm()
+                alarm.id = cursor.getLong(0)
+                alarm.hour = cursor.getInt(1)
+                alarm.minutes = cursor.getInt(2)
+                alarm.daysOfWeek = Weekdays.fromBits(cursor.getInt(3))
+                alarm.enabled = cursor.getInt(4) == 1
+                alarm.vibrate = cursor.getInt(5) == 1
+                alarm.label = cursor.getString(6)
+
+                val alertString: String = cursor.getString(7)
+                if ("silent" == alertString) {
+                    alarm.alert = AlarmSettingColumns.NO_RINGTONE_URI
+                } else {
+                    alarm.alert = if (TextUtils.isEmpty(alertString)) {
+                        null
+                    } else {
+                        Uri.parse(alertString)
+                    }
+                }
+
+                // Save new version of alarm and create alarm instance for it
+                db.insert(ALARMS_TABLE_NAME, null, Alarm.createContentValues(alarm))
+                if (alarm.enabled) {
+                    val newInstance: AlarmInstance = alarm.createInstanceAfter(currentTime)
+                    db.insert(INSTANCES_TABLE_NAME, null,
+                            AlarmInstance.createContentValues(newInstance))
+                }
+            }
+
+            LogUtils.i("Dropping old alarm table")
+            db.execSQL("DROP TABLE IF EXISTS " + OLD_ALARMS_TABLE_NAME + ";")
+        }
+    }
+
+    fun fixAlarmInsert(values: ContentValues): Long {
+        // Why are we doing this? Is this not a programming bug if we try to
+        // insert an already used id?
+        val db: SQLiteDatabase = getWritableDatabase()
+        db.beginTransaction()
+        val rowId: Long
+        try {
+            // Check if we are trying to re-use an existing id.
+            val value = values.get(BaseColumns._ID)
+            if (value != null) {
+                val id: Long = value as Long
+                if (id > -1) {
+                    val columns: Array<String> = arrayOf(BaseColumns._ID)
+                    val selection: String = BaseColumns._ID + " = ?"
+                    val selectionArgs: Array<String> = arrayOf(id.toString())
+                    val cursor: Cursor =
+                            db.query(ALARMS_TABLE_NAME, columns,
+                                    selection, selectionArgs, null, null, null)
+                    if (cursor.moveToFirst()) {
+                        // Record exists. Remove the id so sqlite can generate a new one.
+                        values.putNull(BaseColumns._ID)
+                    }
+                }
+            }
+
+            rowId = db.insert(ALARMS_TABLE_NAME, AlarmSettingColumns.RINGTONE, values)
+            db.setTransactionSuccessful()
+        } finally {
+            db.endTransaction()
+        }
+
+        if (rowId < 0) {
+            throw SQLException("Failed to insert row")
+        }
+        LogUtils.v("Added alarm rowId = " + rowId)
+
+        return rowId
+    }
+
+    companion object {
+        /**
+         * Original Clock Database.
+         **/
+        private const val VERSION_5: Int = 5
+
+        /**
+         * Added alarm_instances table
+         * Added selected_cities table
+         * Added DELETE_AFTER_USE column to alarms table
+         */
+        private const val VERSION_6: Int = 6
+
+        /**
+         * Added alarm settings to instance table.
+         */
+        private const val VERSION_7: Int = 7
+
+        /**
+         * Removed selected_cities table.
+         */
+        private const val VERSION_8: Int = 8
+
+        // This creates a default alarm at 8:30 for every Mon,Tue,Wed,Thu,Fri
+        private const val DEFAULT_ALARM_1: String = "(8, 30, 31, 0, 1, '', NULL, 0);"
+
+        // This creates a default alarm at 9:30 for every Sat,Sun
+        private const val DEFAULT_ALARM_2: String = "(9, 00, 96, 0, 1, '', NULL, 0);"
+
+        // Database and table names
+        const val DATABASE_NAME: String = "alarms.db"
+        const val OLD_ALARMS_TABLE_NAME: String = "alarms"
+        const val ALARMS_TABLE_NAME: String = "alarm_templates"
+        const val INSTANCES_TABLE_NAME: String = "alarm_instances"
+        private const val SELECTED_CITIES_TABLE_NAME: String = "selected_cities"
+
+        private fun createAlarmsTable(db: SQLiteDatabase) {
+            db.execSQL("CREATE TABLE " + ALARMS_TABLE_NAME + " (" +
+                    BaseColumns._ID + " INTEGER PRIMARY KEY," +
+                    AlarmsColumns.HOUR + " INTEGER NOT NULL, " +
+                    AlarmsColumns.MINUTES + " INTEGER NOT NULL, " +
+                    AlarmsColumns.DAYS_OF_WEEK + " INTEGER NOT NULL, " +
+                    AlarmsColumns.ENABLED + " INTEGER NOT NULL, " +
+                    AlarmSettingColumns.VIBRATE + " INTEGER NOT NULL, " +
+                    AlarmSettingColumns.LABEL + " TEXT NOT NULL, " +
+                    AlarmSettingColumns.RINGTONE + " TEXT, " +
+                    AlarmsColumns.DELETE_AFTER_USE + " INTEGER NOT NULL DEFAULT 0);")
+            LogUtils.i("Alarms Table created")
+        }
+
+        private fun createInstanceTable(db: SQLiteDatabase) {
+            db.execSQL("CREATE TABLE " + INSTANCES_TABLE_NAME + " (" +
+                    BaseColumns._ID + " INTEGER PRIMARY KEY," +
+                    InstancesColumns.YEAR + " INTEGER NOT NULL, " +
+                    InstancesColumns.MONTH + " INTEGER NOT NULL, " +
+                    InstancesColumns.DAY + " INTEGER NOT NULL, " +
+                    InstancesColumns.HOUR + " INTEGER NOT NULL, " +
+                    InstancesColumns.MINUTES + " INTEGER NOT NULL, " +
+                    AlarmSettingColumns.VIBRATE + " INTEGER NOT NULL, " +
+                    AlarmSettingColumns.LABEL + " TEXT NOT NULL, " +
+                    AlarmSettingColumns.RINGTONE + " TEXT, " +
+                    InstancesColumns.ALARM_STATE + " INTEGER NOT NULL, " +
+                    InstancesColumns.ALARM_ID + " INTEGER REFERENCES " +
+                    ALARMS_TABLE_NAME + "(" + BaseColumns._ID + ") " +
+                    "ON UPDATE CASCADE ON DELETE CASCADE" +
+                    ");")
+            LogUtils.i("Instance table created")
+        }
+    }
+}
diff --git a/src/com/android/deskclock/provider/ClockProvider.java b/src/com/android/deskclock/provider/ClockProvider.java
deleted file mode 100644
index 83480f3..0000000
--- a/src/com/android/deskclock/provider/ClockProvider.java
+++ /dev/null
@@ -1,306 +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.annotation.TargetApi;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.os.Build;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-import android.util.ArrayMap;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.Utils;
-
-import java.util.Map;
-
-import static com.android.deskclock.provider.ClockContract.AlarmsColumns;
-import static com.android.deskclock.provider.ClockContract.InstancesColumns;
-import static com.android.deskclock.provider.ClockDatabaseHelper.ALARMS_TABLE_NAME;
-import static com.android.deskclock.provider.ClockDatabaseHelper.INSTANCES_TABLE_NAME;
-
-public class ClockProvider extends ContentProvider {
-
-    private ClockDatabaseHelper mOpenHelper;
-
-    private static final int ALARMS = 1;
-    private static final int ALARMS_ID = 2;
-    private static final int INSTANCES = 3;
-    private static final int INSTANCES_ID = 4;
-    private static final int ALARMS_WITH_INSTANCES = 5;
-
-    /**
-     * Projection map used by query for snoozed alarms.
-     */
-    private static final Map<String, String> sAlarmsWithInstancesProjection = new ArrayMap<>();
-    static {
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns._ID,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns._ID);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.VIBRATE,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.VIBRATE);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.LABEL,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.LABEL);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.RINGTONE,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.RINGTONE);
-        sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE,
-                ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "."
-                + InstancesColumns.ALARM_STATE,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns._ID,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns._ID);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.LABEL,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.LABEL);
-        sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.VIBRATE,
-                INSTANCES_TABLE_NAME + "." + InstancesColumns.VIBRATE);
-    }
-
-    private static final String ALARM_JOIN_INSTANCE_TABLE_STATEMENT =
-            ALARMS_TABLE_NAME + " LEFT JOIN " + INSTANCES_TABLE_NAME + " ON (" +
-            ALARMS_TABLE_NAME + "." + AlarmsColumns._ID + " = " + InstancesColumns.ALARM_ID + ")";
-
-    private static final String ALARM_JOIN_INSTANCE_WHERE_STATEMENT =
-            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 {
-        sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms", ALARMS);
-        sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms/#", ALARMS_ID);
-        sURIMatcher.addURI(ClockContract.AUTHORITY, "instances", INSTANCES);
-        sURIMatcher.addURI(ClockContract.AUTHORITY, "instances/#", INSTANCES_ID);
-        sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms_with_instances", ALARMS_WITH_INSTANCES);
-    }
-
-    public ClockProvider() {
-    }
-
-    @Override
-    @TargetApi(Build.VERSION_CODES.N)
-    public boolean onCreate() {
-        final Context context = getContext();
-        final Context storageContext;
-        if (Utils.isNOrLater()) {
-            // All N devices have split storage areas, but we may need to
-            // migrate existing database into the new device encrypted
-            // storage area, which is where our data lives from now on.
-            storageContext = context.createDeviceProtectedStorageContext();
-            if (!storageContext.moveDatabaseFrom(context, ClockDatabaseHelper.DATABASE_NAME)) {
-                LogUtils.wtf("Failed to migrate database: %s", ClockDatabaseHelper.DATABASE_NAME);
-            }
-        } else {
-            storageContext = context;
-        }
-
-        mOpenHelper = new ClockDatabaseHelper(storageContext);
-        return true;
-    }
-
-    @Override
-    public Cursor query(@NonNull Uri uri, String[] projectionIn, String selection,
-            String[] selectionArgs, String sort) {
-        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
-
-        // Generate the body of the query
-        int match = sURIMatcher.match(uri);
-        switch (match) {
-            case ALARMS:
-                qb.setTables(ALARMS_TABLE_NAME);
-                break;
-            case ALARMS_ID:
-                qb.setTables(ALARMS_TABLE_NAME);
-                qb.appendWhere(AlarmsColumns._ID + "=");
-                qb.appendWhere(uri.getLastPathSegment());
-                break;
-            case INSTANCES:
-                qb.setTables(INSTANCES_TABLE_NAME);
-                break;
-            case INSTANCES_ID:
-                qb.setTables(INSTANCES_TABLE_NAME);
-                qb.appendWhere(InstancesColumns._ID + "=");
-                qb.appendWhere(uri.getLastPathSegment());
-                break;
-            case ALARMS_WITH_INSTANCES:
-                qb.setTables(ALARM_JOIN_INSTANCE_TABLE_STATEMENT);
-                qb.appendWhere(ALARM_JOIN_INSTANCE_WHERE_STATEMENT);
-                qb.setProjectionMap(sAlarmsWithInstancesProjection);
-                break;
-            default:
-                throw new IllegalArgumentException("Unknown URI " + uri);
-        }
-
-        Cursor ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);
-
-        if (ret == null) {
-            LogUtils.e("Alarms.query: failed");
-        } else {
-            ret.setNotificationUri(getContext().getContentResolver(), uri);
-        }
-
-        return ret;
-    }
-
-    @Override
-    public String getType(@NonNull Uri uri) {
-        int match = sURIMatcher.match(uri);
-        switch (match) {
-            case ALARMS:
-                return "vnd.android.cursor.dir/alarms";
-            case ALARMS_ID:
-                return "vnd.android.cursor.item/alarms";
-            case INSTANCES:
-                return "vnd.android.cursor.dir/instances";
-            case INSTANCES_ID:
-                return "vnd.android.cursor.item/instances";
-            default:
-                throw new IllegalArgumentException("Unknown URI");
-        }
-    }
-
-    @Override
-    public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) {
-        int count;
-        String alarmId;
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        switch (sURIMatcher.match(uri)) {
-            case ALARMS_ID:
-                alarmId = uri.getLastPathSegment();
-                count = db.update(ALARMS_TABLE_NAME, values,
-                        AlarmsColumns._ID + "=" + alarmId,
-                        null);
-                break;
-            case INSTANCES_ID:
-                alarmId = uri.getLastPathSegment();
-                count = db.update(INSTANCES_TABLE_NAME, values,
-                        InstancesColumns._ID + "=" + alarmId,
-                        null);
-                break;
-            default: {
-                throw new UnsupportedOperationException("Cannot update URI: " + uri);
-            }
-        }
-        LogUtils.v("*** notifyChange() id: " + alarmId + " url " + uri);
-        notifyChange(getContext().getContentResolver(), uri);
-        return count;
-    }
-
-    @Override
-    public Uri insert(@NonNull Uri uri, ContentValues initialValues) {
-        long rowId;
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        switch (sURIMatcher.match(uri)) {
-            case ALARMS:
-                rowId = mOpenHelper.fixAlarmInsert(initialValues);
-                break;
-            case INSTANCES:
-                rowId = db.insert(INSTANCES_TABLE_NAME, null, initialValues);
-                break;
-            default:
-                throw new IllegalArgumentException("Cannot insert from URI: " + uri);
-        }
-
-        Uri uriResult = ContentUris.withAppendedId(uri, rowId);
-        notifyChange(getContext().getContentResolver(), uriResult);
-        return uriResult;
-    }
-
-    @Override
-    public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
-        int count;
-        String primaryKey;
-        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
-        switch (sURIMatcher.match(uri)) {
-            case ALARMS:
-                count = db.delete(ALARMS_TABLE_NAME, where, whereArgs);
-                break;
-            case ALARMS_ID:
-                primaryKey = uri.getLastPathSegment();
-                if (TextUtils.isEmpty(where)) {
-                    where = AlarmsColumns._ID + "=" + primaryKey;
-                } else {
-                    where = AlarmsColumns._ID + "=" + primaryKey + " AND (" + where + ")";
-                }
-                count = db.delete(ALARMS_TABLE_NAME, where, whereArgs);
-                break;
-            case INSTANCES:
-                count = db.delete(INSTANCES_TABLE_NAME, where, whereArgs);
-                break;
-            case INSTANCES_ID:
-                primaryKey = uri.getLastPathSegment();
-                if (TextUtils.isEmpty(where)) {
-                    where = InstancesColumns._ID + "=" + primaryKey;
-                } else {
-                    where = InstancesColumns._ID + "=" + primaryKey + " AND (" + where + ")";
-                }
-                count = db.delete(INSTANCES_TABLE_NAME, where, whereArgs);
-                break;
-            default:
-                throw new IllegalArgumentException("Cannot delete from URI: " + uri);
-        }
-
-        notifyChange(getContext().getContentResolver(), uri);
-        return count;
-    }
-
-    /**
-     * Notify affected URIs of changes.
-     */
-    private void notifyChange(ContentResolver resolver, Uri uri) {
-        resolver.notifyChange(uri, null);
-
-        final int match = sURIMatcher.match(uri);
-        // Also notify the joined table of changes to instances or alarms.
-        if (match == ALARMS || match == INSTANCES || match == ALARMS_ID || match == INSTANCES_ID) {
-            resolver.notifyChange(AlarmsColumns.ALARMS_WITH_INSTANCES_URI, null);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/provider/ClockProvider.kt b/src/com/android/deskclock/provider/ClockProvider.kt
new file mode 100644
index 0000000..effd531
--- /dev/null
+++ b/src/com/android/deskclock/provider/ClockProvider.kt
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.TargetApi
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.UriMatcher
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteQueryBuilder
+import android.net.Uri
+import android.os.Build
+import android.provider.BaseColumns
+import android.text.TextUtils
+import android.util.ArrayMap
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.provider.ClockDatabaseHelper.Companion.ALARMS_TABLE_NAME
+import com.android.deskclock.provider.ClockDatabaseHelper.Companion.INSTANCES_TABLE_NAME
+
+class ClockProvider : ContentProvider() {
+
+    private lateinit var mOpenHelper: ClockDatabaseHelper
+
+    companion object {
+        private const val ALARMS = 1
+        private const val ALARMS_ID = 2
+        private const val INSTANCES = 3
+        private const val INSTANCES_ID = 4
+        private const val ALARMS_WITH_INSTANCES = 5
+
+        private val ALARM_JOIN_INSTANCE_TABLE_STATEMENT =
+                ALARMS_TABLE_NAME + " LEFT JOIN " +
+                        INSTANCES_TABLE_NAME + " ON (" +
+                        ALARMS_TABLE_NAME + "." +
+                        BaseColumns._ID + " = " + InstancesColumns.ALARM_ID + ")"
+
+        private val ALARM_JOIN_INSTANCE_WHERE_STATEMENT = INSTANCES_TABLE_NAME +
+                "." + BaseColumns._ID + " IS NULL OR " +
+                INSTANCES_TABLE_NAME + "." + BaseColumns._ID + " = (" +
+                "SELECT " + BaseColumns._ID +
+                " FROM " + INSTANCES_TABLE_NAME +
+                " WHERE " + InstancesColumns.ALARM_ID +
+                " = " + ALARMS_TABLE_NAME + "." + BaseColumns._ID +
+                " ORDER BY " + InstancesColumns.ALARM_STATE + ", " +
+                InstancesColumns.YEAR + ", " + InstancesColumns.MONTH + ", " +
+                InstancesColumns.DAY + " LIMIT 1)"
+
+        /**
+         * Projection map used by query for snoozed alarms.
+         */
+        private val sAlarmsWithInstancesProjection: MutableMap<String, String> = ArrayMap()
+
+        private val sURIMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH)
+
+        init {
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + BaseColumns._ID] =
+                    ALARMS_TABLE_NAME + "." + BaseColumns._ID
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR] =
+                    ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES] =
+                    ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK] =
+                    ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED] =
+                    ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE] =
+                    ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL] =
+                    ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE] =
+                    ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE
+            sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." +
+                    AlarmsColumns.DELETE_AFTER_USE] =
+                    ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." +
+                    InstancesColumns.ALARM_STATE] =
+                    INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + BaseColumns._ID] =
+                    INSTANCES_TABLE_NAME + "." + BaseColumns._ID
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR] =
+                    INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH] =
+                    INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY] =
+                    INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR] =
+                    INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES] =
+                    INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL] =
+                    INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL
+            sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." +
+                    AlarmSettingColumns.VIBRATE] =
+                    INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
+
+            sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms", ALARMS)
+            sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms/#", ALARMS_ID)
+            sURIMatcher.addURI(ClockContract.AUTHORITY, "instances", INSTANCES)
+            sURIMatcher.addURI(ClockContract.AUTHORITY, "instances/#", INSTANCES_ID)
+            sURIMatcher.addURI(ClockContract.AUTHORITY,
+                    "alarms_with_instances", ALARMS_WITH_INSTANCES)
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    override fun onCreate(): Boolean {
+        val context: Context = getContext()!!
+        val storageContext: Context
+        if (Utils.isNOrLater) {
+            // All N devices have split storage areas, but we may need to
+            // migrate existing database into the new device encrypted
+            // storage area, which is where our data lives from now on.
+            storageContext = context.createDeviceProtectedStorageContext()
+            if (!storageContext.moveDatabaseFrom(context, ClockDatabaseHelper.DATABASE_NAME)) {
+                LogUtils.wtf("Failed to migrate database: %s",
+                        ClockDatabaseHelper.DATABASE_NAME)
+            }
+        } else {
+            storageContext = context
+        }
+
+        mOpenHelper = ClockDatabaseHelper(storageContext)
+        return true
+    }
+
+    override fun query(
+        uri: Uri,
+        projectionIn: Array<String?>?,
+        selection: String?,
+        selectionArgs: Array<String?>?,
+        sort: String?
+    ): Cursor? {
+        val qb = SQLiteQueryBuilder()
+        val db: SQLiteDatabase = mOpenHelper.getReadableDatabase()
+
+        // Generate the body of the query
+        when (sURIMatcher.match(uri)) {
+            ALARMS -> qb.setTables(ALARMS_TABLE_NAME)
+            ALARMS_ID -> {
+                qb.setTables(ALARMS_TABLE_NAME)
+                qb.appendWhere(BaseColumns._ID.toString() + "=")
+                qb.appendWhere(uri.getLastPathSegment()!!)
+            }
+            INSTANCES -> qb.setTables(INSTANCES_TABLE_NAME)
+            INSTANCES_ID -> {
+                qb.setTables(INSTANCES_TABLE_NAME)
+                qb.appendWhere(BaseColumns._ID.toString() + "=")
+                qb.appendWhere(uri.getLastPathSegment()!!)
+            }
+            ALARMS_WITH_INSTANCES -> {
+                qb.setTables(ALARM_JOIN_INSTANCE_TABLE_STATEMENT)
+                qb.appendWhere(ALARM_JOIN_INSTANCE_WHERE_STATEMENT)
+                qb.setProjectionMap(sAlarmsWithInstancesProjection)
+            }
+            else -> throw IllegalArgumentException("Unknown URI $uri")
+        }
+
+        val ret: Cursor? = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort)
+        if (ret == null) {
+            LogUtils.e("Alarms.query: failed")
+        } else {
+            ret.setNotificationUri(getContext()!!.getContentResolver(), uri)
+        }
+
+        return ret
+    }
+
+    override fun getType(uri: Uri): String {
+        return when (sURIMatcher.match(uri)) {
+            ALARMS -> "vnd.android.cursor.dir/alarms"
+            ALARMS_ID -> "vnd.android.cursor.item/alarms"
+            INSTANCES -> "vnd.android.cursor.dir/instances"
+            INSTANCES_ID -> "vnd.android.cursor.item/instances"
+            else -> throw IllegalArgumentException("Unknown URI")
+        }
+    }
+
+    override fun update(
+        uri: Uri,
+        values: ContentValues?,
+        where: String?,
+        whereArgs: Array<String?>?
+    ): Int {
+        val count: Int
+        val alarmId: String?
+        val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
+        when (sURIMatcher.match(uri)) {
+            ALARMS_ID -> {
+                alarmId = uri.getLastPathSegment()
+                count = db.update(ALARMS_TABLE_NAME, values,
+                        BaseColumns._ID.toString() + "=" + alarmId,
+                        null)
+            }
+            INSTANCES_ID -> {
+                alarmId = uri.getLastPathSegment()
+                count = db.update(INSTANCES_TABLE_NAME, values,
+                        BaseColumns._ID.toString() + "=" + alarmId,
+                        null)
+            }
+            else -> {
+                throw UnsupportedOperationException("Cannot update URI: $uri")
+            }
+        }
+        LogUtils.v("*** notifyChange() id: $alarmId url $uri")
+        notifyChange(getContext()!!.getContentResolver(), uri)
+        return count
+    }
+
+    override fun insert(uri: Uri, initialValues: ContentValues?): Uri? {
+        val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
+        val rowId: Long = when (sURIMatcher.match(uri)) {
+            ALARMS -> mOpenHelper.fixAlarmInsert(initialValues!!)
+            INSTANCES -> db.insert(INSTANCES_TABLE_NAME, null, initialValues)
+            else -> throw IllegalArgumentException("Cannot insert from URI: $uri")
+        }
+
+        val uriResult: Uri = ContentUris.withAppendedId(uri, rowId)
+        notifyChange(getContext()!!.getContentResolver(), uriResult)
+        return uriResult
+    }
+
+    override fun delete(uri: Uri, where: String?, whereArgs: Array<String>?): Int {
+        var whereString = where
+        val count: Int
+        val primaryKey: String?
+        val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
+        when (sURIMatcher.match(uri)) {
+            ALARMS -> count =
+                    db.delete(ALARMS_TABLE_NAME, whereString, whereArgs)
+            ALARMS_ID -> {
+                primaryKey = uri.getLastPathSegment()
+                whereString = if (TextUtils.isEmpty(whereString)) {
+                    BaseColumns._ID.toString() + "=" + primaryKey
+                } else {
+                    BaseColumns._ID.toString() + "=" + primaryKey + " AND (" + whereString + ")"
+                }
+                count = db.delete(ALARMS_TABLE_NAME, whereString, whereArgs)
+            }
+            INSTANCES -> count =
+                    db.delete(INSTANCES_TABLE_NAME, whereString, whereArgs)
+            INSTANCES_ID -> {
+                primaryKey = uri.getLastPathSegment()
+                whereString = if (TextUtils.isEmpty(whereString)) {
+                    BaseColumns._ID.toString() + "=" + primaryKey
+                } else {
+                    BaseColumns._ID.toString() + "=" + primaryKey + " AND (" + whereString + ")"
+                }
+                count = db.delete(INSTANCES_TABLE_NAME, whereString, whereArgs)
+            }
+            else -> throw IllegalArgumentException("Cannot delete from URI: $uri")
+        }
+
+        notifyChange(getContext()!!.getContentResolver(), uri)
+        return count
+    }
+
+    /**
+     * Notify affected URIs of changes.
+     */
+    private fun notifyChange(resolver: ContentResolver, uri: Uri) {
+        resolver.notifyChange(uri, null)
+
+        val match: Int = sURIMatcher.match(uri)
+        // Also notify the joined table of changes to instances or alarms.
+        if (match == ALARMS || match == INSTANCES || match == ALARMS_ID || match == INSTANCES_ID) {
+            resolver.notifyChange(AlarmsColumns.ALARMS_WITH_INSTANCES_URI, null)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java
deleted file mode 100644
index ff37551..0000000
--- a/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java
+++ /dev/null
@@ -1,35 +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.ringtone;
-
-import android.net.Uri;
-
-import com.android.deskclock.ItemAdapter;
-
-import static androidx.recyclerview.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/AddCustomRingtoneHolder.kt b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.kt
new file mode 100644
index 0000000..d80d41c
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+
+internal class AddCustomRingtoneHolder : ItemHolder<Uri?>(null, NO_ID) {
+    override fun getItemViewType(): Int {
+        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
deleted file mode 100644
index 5cc6fad..0000000
--- a/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.java
+++ /dev/null
@@ -1,71 +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.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/AddCustomRingtoneViewHolder.kt b/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.kt
new file mode 100644
index 0000000..b70df2f
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+internal class AddCustomRingtoneViewHolder private constructor(itemView: View)
+    : ItemViewHolder<AddCustomRingtoneHolder>(itemView), View.OnClickListener {
+
+    init {
+        itemView.setOnClickListener(this)
+        val selectedView = itemView.findViewById<View>(R.id.sound_image_selected)
+        selectedView.visibility = View.GONE
+        val nameView = itemView.findViewById<View>(R.id.ringtone_name) as TextView
+        nameView.text = itemView.context.getString(R.string.add_new_sound)
+        nameView.alpha = 0.63f
+        val imageView = itemView.findViewById<View>(R.id.ringtone_image) as ImageView
+        imageView.setImageResource(R.drawable.ic_add_white_24dp)
+        imageView.alpha = 0.63f
+    }
+
+    override fun onClick(view: View) {
+        notifyItemClicked(CLICK_ADD_NEW)
+    }
+
+    class Factory internal constructor(private val mInflater: LayoutInflater)
+        : ItemViewHolder.Factory {
+        override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+            val itemView =
+                    mInflater.inflate(R.layout.ringtone_item_sound, parent, false)
+            return AddCustomRingtoneViewHolder(itemView)
+        }
+    }
+
+    companion object {
+        const val VIEW_TYPE_ADD_NEW = Int.MIN_VALUE
+        const val CLICK_ADD_NEW = VIEW_TYPE_ADD_NEW
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java
deleted file mode 100644
index 619d45f..0000000
--- a/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java
+++ /dev/null
@@ -1,31 +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.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/CustomRingtoneHolder.kt b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.kt
new file mode 100644
index 0000000..bd880ad
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+internal class CustomRingtoneHolder(ringtone: CustomRingtone)
+    : RingtoneHolder(ringtone.uri, ringtone.title, ringtone.hasPermissions()) {
+
+    override fun getItemViewType(): Int {
+        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
deleted file mode 100644
index 4c52efe..0000000
--- a/src/com/android/deskclock/ringtone/HeaderHolder.java
+++ /dev/null
@@ -1,43 +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.ringtone;
-
-import android.net.Uri;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.ItemAdapter;
-
-import static androidx.recyclerview.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/HeaderHolder.kt b/src/com/android/deskclock/ringtone/HeaderHolder.kt
new file mode 100644
index 0000000..98836b0
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/HeaderHolder.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.annotation.StringRes
+import androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+
+internal class HeaderHolder(
+    @field:StringRes @get:StringRes
+    @param:StringRes val textResId: Int
+) : ItemHolder<Uri?>(null, NO_ID) {
+
+    override fun getItemViewType(): Int {
+        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
deleted file mode 100644
index eb88bb0..0000000
--- a/src/com/android/deskclock/ringtone/HeaderViewHolder.java
+++ /dev/null
@@ -1,56 +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.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/HeaderViewHolder.kt b/src/com/android/deskclock/ringtone/HeaderViewHolder.kt
new file mode 100644
index 0000000..3dd287f
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/HeaderViewHolder.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ItemViewHolder
+import com.android.deskclock.R
+
+internal class HeaderViewHolder private constructor(itemView: View)
+    : ItemViewHolder<HeaderHolder>(itemView) {
+    private val mItemHeader: TextView =
+            itemView.findViewById<View>(R.id.ringtone_item_header) as TextView
+
+    override fun onBindItemView(itemHolder: HeaderHolder) {
+        mItemHeader.setText(itemHolder.textResId)
+    }
+
+    class Factory internal constructor(private val mInflater: LayoutInflater)
+        : ItemViewHolder.Factory {
+        override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+            return HeaderViewHolder(mInflater.inflate(viewType, parent, false))
+        }
+    }
+
+    companion object {
+        const val VIEW_TYPE_ITEM_HEADER = R.layout.ringtone_item_header
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneHolder.java b/src/com/android/deskclock/ringtone/RingtoneHolder.java
deleted file mode 100644
index 37c52b6..0000000
--- a/src/com/android/deskclock/ringtone/RingtoneHolder.java
+++ /dev/null
@@ -1,59 +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.ringtone;
-
-import android.net.Uri;
-
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-import static androidx.recyclerview.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/RingtoneHolder.kt b/src/com/android/deskclock/ringtone/RingtoneHolder.kt
new file mode 100644
index 0000000..85e3d2c
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneHolder.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+internal abstract class RingtoneHolder @JvmOverloads constructor(
+    uri: Uri,
+    private val mName: String?,
+    private val mHasPermissions: Boolean = true
+) : ItemHolder<Uri?>(uri, NO_ID) {
+    var isSelected = false
+    var isPlaying = false
+
+    val id: Long
+        get() = itemId
+
+    fun hasPermissions(): Boolean {
+        return mHasPermissions
+    }
+
+    val uri: Uri
+        get() = item!!
+
+    val isSilent: Boolean
+        get() = Utils.RINGTONE_SILENT == uri
+
+    val name: String?
+        get() = mName ?: DataModel.dataModel.getRingtoneTitle(uri)
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneLoader.java b/src/com/android/deskclock/ringtone/RingtoneLoader.java
deleted file mode 100644
index bace949..0000000
--- a/src/com/android/deskclock/ringtone/RingtoneLoader.java
+++ /dev/null
@@ -1,118 +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.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/RingtoneLoader.kt b/src/com/android/deskclock/ringtone/RingtoneLoader.kt
new file mode 100644
index 0000000..981dd31
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneLoader.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context
+import android.database.MatrixCursor
+import android.media.AudioManager
+import android.media.RingtoneManager
+import android.net.Uri
+import androidx.loader.content.AsyncTaskLoader
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.CustomRingtone
+import com.android.deskclock.data.DataModel
+
+/**
+ * Assembles the list of ItemHolders that back the RecyclerView used to choose a ringtone.
+ */
+internal class RingtoneLoader(
+    context: Context,
+    private val mDefaultRingtoneUri: Uri,
+    private val mDefaultRingtoneTitle: String
+) : AsyncTaskLoader<List<ItemHolder<Uri?>>>(context) {
+    private var mCustomRingtones: List<CustomRingtone>? = null
+
+    override fun onStartLoading() {
+        super.onStartLoading()
+
+        mCustomRingtones = DataModel.dataModel.customRingtones
+        forceLoad()
+    }
+
+    override fun loadInBackground(): List<ItemHolder<Uri?>> {
+        // Prime the ringtone title cache for later access.
+        DataModel.dataModel.loadRingtoneTitles()
+        DataModel.dataModel.loadRingtonePermissions()
+
+        // Fetch the standard system ringtones.
+        val ringtoneManager = RingtoneManager(context)
+        ringtoneManager.setType(AudioManager.STREAM_ALARM)
+
+        val systemRingtoneCursor = try {
+            ringtoneManager.cursor
+        } catch (e: Exception) {
+            LogUtils.e("Could not get system ringtone cursor")
+            MatrixCursor(arrayOf())
+        }
+        val systemRingtoneCount = systemRingtoneCursor.count
+        // item count = # system ringtones + # custom ringtones + 2 headers + Add new music item
+        val itemCount = systemRingtoneCount + mCustomRingtones!!.size + 3
+
+        val itemHolders: MutableList<ItemHolder<Uri?>> = ArrayList(itemCount)
+
+        // Add the item holder for the Music heading.
+        itemHolders.add(HeaderHolder(R.string.your_sounds))
+
+        // Add an item holder for each custom ringtone and also cache a pretty name.
+        for (ringtone in mCustomRingtones!!) {
+            itemHolders.add(CustomRingtoneHolder(ringtone))
+        }
+
+        // Add an item holder for the "Add new" music ringtone.
+        itemHolders.add(AddCustomRingtoneHolder())
+
+        // Add an item holder for the Ringtones heading.
+        itemHolders.add(HeaderHolder(R.string.device_sounds))
+
+        // Add an item holder for the silent ringtone.
+        itemHolders.add(SystemRingtoneHolder(Utils.RINGTONE_SILENT, null))
+
+        // Add an item holder for the system default alarm sound.
+        itemHolders.add(SystemRingtoneHolder(mDefaultRingtoneUri, mDefaultRingtoneTitle))
+
+        // Add an item holder for each system ringtone.
+        for (i in 0 until systemRingtoneCount) {
+            val ringtoneUri = ringtoneManager.getRingtoneUri(i)
+            itemHolders.add(SystemRingtoneHolder(ringtoneUri, null))
+        }
+
+        return itemHolders
+    }
+
+    override fun 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
deleted file mode 100644
index 40ddb04..0000000
--- a/src/com/android/deskclock/ringtone/RingtonePickerActivity.java
+++ /dev/null
@@ -1,674 +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.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 androidx.annotation.VisibleForTesting;
-import androidx.appcompat.app.AlertDialog;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.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/RingtonePickerActivity.kt b/src/com/android/deskclock/ringtone/RingtonePickerActivity.kt
new file mode 100644
index 0000000..1dadc69
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtonePickerActivity.kt
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Context
+import android.content.ContentResolver
+import android.content.DialogInterface
+import android.content.Intent
+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.provider.OpenableColumns
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.annotation.Keep
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+import androidx.loader.app.LoaderManager
+import androidx.loader.app.LoaderManager.LoaderCallbacks
+import androidx.loader.content.Loader
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.DropShadowController
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.RingtonePreviewKlaxon
+import com.android.deskclock.ItemAdapter
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.ItemAdapter.OnItemClickedListener
+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
+
+/**
+ * This activity presents a set of ringtones from which the user may select one. The set includes:
+ *
+ *  * system ringtones from the Android framework
+ *  * a ringtone representing pure silence
+ *  * a ringtone representing a default ringtone
+ *  * user-selected audio files available as ringtones
+ *
+ */
+// TODO(b/165664115) Replace deprecated AsyncTask calls
+class RingtonePickerActivity : BaseActivity(), LoaderCallbacks<List<ItemHolder<Uri?>>> {
+    /** The controller that shows the drop shadow when content is not scrolled to the top.  */
+    private var mDropShadowController: DropShadowController? = null
+
+    /** Generates the items in the activity context menu.  */
+    private lateinit var mOptionsMenuManager: OptionsMenuManager
+
+    /** Displays a set of selectable ringtones.  */
+    private lateinit var mRecyclerView: RecyclerView
+
+    /** Stores the set of ItemHolders that wrap the selectable ringtones.  */
+    private lateinit var mRingtoneAdapter: ItemAdapter<ItemHolder<Uri?>>
+
+    /** The title of the default ringtone.  */
+    private var mDefaultRingtoneTitle: String? = null
+
+    /** The uri of the default ringtone.  */
+    private var mDefaultRingtoneUri: Uri? = null
+
+    /** The uri of the ringtone to select after data is loaded.  */
+    private var mSelectedRingtoneUri: Uri? = null
+
+    /** `true` indicates the [.mSelectedRingtoneUri] must be played after data load.  */
+    private var mIsPlaying = false
+
+    /** Identifies the alarm to receive the selected ringtone; -1 indicates there is no alarm.  */
+    private var mAlarmId: Long = -1
+
+    /** The location of the custom ringtone to be removed.  */
+    private var mIndexOfRingtoneToRemove: Int = RecyclerView.NO_POSITION
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.ringtone_picker)
+        setVolumeControlStream(AudioManager.STREAM_ALARM)
+
+        mOptionsMenuManager = OptionsMenuManager()
+        mOptionsMenuManager.addMenuItemController(NavUpMenuItemController(this))
+                .addMenuItemController(*MenuItemControllerFactory.buildMenuItemControllers(this))
+
+        val context: Context = getApplicationContext()
+        val 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)
+        val defaultRingtoneTitleId = intent.getIntExtra(EXTRA_DEFAULT_RINGTONE_NAME, 0)
+        mDefaultRingtoneTitle = context.getString(defaultRingtoneTitleId)
+
+        val inflater: LayoutInflater = getLayoutInflater()
+        val listener: OnItemClickedListener = ItemClickWatcher()
+        val ringtoneFactory: ItemViewHolder.Factory = RingtoneViewHolder.Factory(inflater)
+        val headerFactory: ItemViewHolder.Factory = HeaderViewHolder.Factory(inflater)
+        val addNewFactory: ItemViewHolder.Factory = AddCustomRingtoneViewHolder.Factory(inflater)
+        mRingtoneAdapter = ItemAdapter()
+        mRingtoneAdapter
+                .withViewTypes(headerFactory, null, HeaderViewHolder.VIEW_TYPE_ITEM_HEADER)
+                .withViewTypes(addNewFactory, listener,
+                        AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW)
+                .withViewTypes(ringtoneFactory, listener, RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND)
+                .withViewTypes(ringtoneFactory, listener, RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND)
+
+        mRecyclerView = findViewById(R.id.ringtone_content) as RecyclerView
+        mRecyclerView.setLayoutManager(LinearLayoutManager(context))
+        mRecyclerView.setAdapter(mRingtoneAdapter)
+        mRecyclerView.setItemAnimator(null)
+
+        mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+                if (mIndexOfRingtoneToRemove != RecyclerView.NO_POSITION) {
+                    closeContextMenu()
+                }
+            }
+        })
+
+        val titleResourceId = intent.getIntExtra(EXTRA_TITLE, 0)
+        setTitle(context.getString(titleResourceId))
+
+        LoaderManager.getInstance(this).initLoader(0 /* id */, Bundle.EMPTY /* args */,
+                this /* callback */)
+
+        registerForContextMenu(mRecyclerView)
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        val dropShadow: View = findViewById(R.id.drop_shadow)
+        mDropShadowController = DropShadowController(dropShadow, mRecyclerView)
+    }
+
+    override fun onPause() {
+        mDropShadowController!!.stop()
+        mDropShadowController = null
+
+        mSelectedRingtoneUri?.let {
+            if (mAlarmId != -1L) {
+                val context: Context = getApplicationContext()
+                val cr: ContentResolver = getContentResolver()
+
+                // Start a background task to fetch the alarm whose ringtone must be updated.
+                object : AsyncTask<Void?, Void?, Alarm>() {
+                    override fun doInBackground(vararg parameters: Void?): Alarm? {
+                        val alarm = Alarm.getAlarm(cr, mAlarmId)
+                        if (alarm != null) {
+                            alarm.alert = it
+                        }
+                        return alarm
+                    }
+
+                    override fun onPostExecute(alarm: Alarm) {
+                        // Update the default ringtone for future new alarms.
+                        DataModel.dataModel.defaultAlarmRingtoneUri = alarm.alert!!
+
+                        // Start a second background task to persist the updated alarm.
+                        AlarmUpdateHandler(context, mScrollHandler = null, mSnackbarAnchor = null)
+                                .asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+                    }
+                }.execute()
+            } else {
+                DataModel.dataModel.timerRingtoneUri = it
+            }
+        }
+
+        super.onPause()
+    }
+
+    override fun onStop() {
+        if (!isChangingConfigurations()) {
+            stopPlayingRingtone(selectedRingtoneHolder, false)
+        }
+        super.onStop()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+
+        outState.putBoolean(STATE_KEY_PLAYING, mIsPlaying)
+        outState.putParcelable(EXTRA_RINGTONE_URI, mSelectedRingtoneUri)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        mOptionsMenuManager.onCreateOptionsMenu(menu)
+        return true
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+        mOptionsMenuManager.onPrepareOptionsMenu(menu)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return mOptionsMenuManager.onOptionsItemSelected(item) ||
+                super.onOptionsItemSelected(item)
+    }
+
+    override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ItemHolder<Uri?>>> {
+        return RingtoneLoader(getApplicationContext(), mDefaultRingtoneUri!!,
+                mDefaultRingtoneTitle!!)
+    }
+
+    override fun onLoadFinished(
+        loader: Loader<List<ItemHolder<Uri?>>>,
+        itemHolders: List<ItemHolder<Uri?>>
+    ) {
+        // Update the adapter with fresh data.
+        mRingtoneAdapter.setItems(itemHolders)
+
+        // Attempt to select the requested ringtone.
+        val toSelect = getRingtoneHolder(mSelectedRingtoneUri)
+        if (toSelect != null) {
+            toSelect.isSelected = true
+            mSelectedRingtoneUri = toSelect.uri
+            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 fun onLoaderReset(loader: Loader<List<ItemHolder<Uri?>>>) {
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        if (resultCode != RESULT_OK) {
+            return
+        }
+
+        val uri = data?.data ?: return
+
+        // Bail if the permission to read (playback) the audio at the uri was not granted.
+        val flags = data.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION
+        if (flags != Intent.FLAG_GRANT_READ_URI_PERMISSION) {
+            return
+        }
+
+        // Start a task to fetch the display name of the audio content and add the custom ringtone.
+        AddCustomRingtoneTask(uri).execute()
+    }
+
+    override fun onContextItemSelected(item: MenuItem): Boolean {
+        // Find the ringtone to be removed.
+        val items = mRingtoneAdapter.items
+        val toRemove = items!![mIndexOfRingtoneToRemove] as RingtoneHolder
+        mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION
+
+        // Launch the confirmation dialog.
+        val manager: FragmentManager = supportFragmentManager
+        val hasPermissions = toRemove.hasPermissions()
+        ConfirmRemoveCustomRingtoneDialogFragment.show(manager, toRemove.uri, hasPermissions)
+        return true
+    }
+
+    private fun getRingtoneHolder(uri: Uri?): RingtoneHolder? {
+        for (itemHolder in mRingtoneAdapter.items!!) {
+            if (itemHolder is RingtoneHolder) {
+                if (itemHolder.uri == uri) {
+                    return itemHolder
+                }
+            }
+        }
+
+        return null
+    }
+
+    @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal val selectedRingtoneHolder: RingtoneHolder?
+        get() = getRingtoneHolder(mSelectedRingtoneUri)
+
+    /**
+     * The given `ringtone` will be selected as a side-effect of playing the ringtone.
+     *
+     * @param ringtone the ringtone to be played
+     */
+    private fun startPlayingRingtone(ringtone: RingtoneHolder) {
+        if (!ringtone.isPlaying && !ringtone.isSilent) {
+            RingtonePreviewKlaxon.start(getApplicationContext(), ringtone.uri)
+            ringtone.isPlaying = true
+            mIsPlaying = true
+        }
+        if (!ringtone.isSelected) {
+            ringtone.isSelected = true
+            mSelectedRingtoneUri = ringtone.uri
+        }
+        ringtone.notifyItemChanged()
+    }
+
+    /**
+     * @param ringtone the ringtone to stop playing
+     * @param deselect `true` indicates the ringtone should also be deselected;
+     * `false` indicates its selection state should remain unchanged
+     */
+    private fun stopPlayingRingtone(ringtone: RingtoneHolder?, deselect: Boolean) {
+        if (ringtone == null) {
+            return
+        }
+
+        if (ringtone.isPlaying) {
+            RingtonePreviewKlaxon.stop(this)
+            ringtone.isPlaying = false
+            mIsPlaying = false
+        }
+        if (deselect && ringtone.isSelected) {
+            ringtone.isSelected = 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 fun removeCustomRingtone(toRemove: Uri) {
+        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.
+     */
+    class ConfirmRemoveCustomRingtoneDialogFragment : DialogFragment() {
+        override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+            val arguments = requireArguments()
+            val toRemove = arguments.getParcelable<Uri>(ARG_RINGTONE_URI_TO_REMOVE)
+
+            val okListener = DialogInterface.OnClickListener { _, _ ->
+                (activity as RingtonePickerActivity).removeCustomRingtone(toRemove!!)
+            }
+
+            return if (arguments.getBoolean(ARG_RINGTONE_HAS_PERMISSIONS)) {
+                AlertDialog.Builder(requireActivity())
+                        .setPositiveButton(R.string.remove_sound, okListener)
+                        .setNegativeButton(android.R.string.cancel, null /* listener */)
+                        .setMessage(R.string.confirm_remove_custom_ringtone)
+                        .create()
+            } else {
+                AlertDialog.Builder(requireActivity())
+                        .setPositiveButton(R.string.remove_sound, okListener)
+                        .setMessage(R.string.custom_ringtone_lost_permissions)
+                        .create()
+            }
+        }
+
+        companion object {
+            private const val ARG_RINGTONE_URI_TO_REMOVE = "arg_ringtone_uri_to_remove"
+            private const val ARG_RINGTONE_HAS_PERMISSIONS = "arg_ringtone_has_permissions"
+
+            fun show(manager: FragmentManager, toRemove: Uri?, hasPermissions: Boolean) {
+                if (manager.isDestroyed) {
+                    return
+                }
+
+                val args = Bundle()
+                args.putParcelable(ARG_RINGTONE_URI_TO_REMOVE, toRemove)
+                args.putBoolean(ARG_RINGTONE_HAS_PERMISSIONS, hasPermissions)
+
+                val fragment: DialogFragment = ConfirmRemoveCustomRingtoneDialogFragment()
+                fragment.arguments = args
+                fragment.isCancelable = hasPermissions
+                fragment.show(manager, "confirm_ringtone_remove")
+            }
+        }
+    }
+
+    /**
+     * 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 inner class ItemClickWatcher : OnItemClickedListener {
+        override fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int) {
+            when (id) {
+                AddCustomRingtoneViewHolder.CLICK_ADD_NEW -> {
+                    stopPlayingRingtone(selectedRingtoneHolder, false)
+                    startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT)
+                            .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+                            .addCategory(Intent.CATEGORY_OPENABLE)
+                            .setType("audio/*"), 0)
+                }
+                RingtoneViewHolder.CLICK_NORMAL -> {
+                    val oldSelection = selectedRingtoneHolder
+                    val newSelection = viewHolder.itemHolder as RingtoneHolder
+
+                    // 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)
+                    }
+                }
+                RingtoneViewHolder.CLICK_LONG_PRESS -> {
+                    mIndexOfRingtoneToRemove = viewHolder.getAdapterPosition()
+                }
+                RingtoneViewHolder.CLICK_NO_PERMISSIONS -> {
+                    ConfirmRemoveCustomRingtoneDialogFragment.show(supportFragmentManager,
+                            (viewHolder.itemHolder as RingtoneHolder).uri, false)
+                }
+            }
+        }
+    }
+
+    /**
+     * 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 inner class AddCustomRingtoneTask(private val mUri: Uri)
+        : AsyncTask<Void?, Void?, String>() {
+        private val mContext: Context = getApplicationContext()
+
+        override fun doInBackground(vararg voids: Void?): String {
+            val contentResolver = mContext.contentResolver
+
+            // Take the long-term permission to read (playback) the audio at the uri.
+            contentResolver
+                    .takePersistableUriPermission(mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+            try {
+                contentResolver.query(mUri, null, null, null, null).use { cursor ->
+                    if (cursor != null && cursor.moveToFirst()) {
+                        // If the file was a media file, return its title.
+                        val 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.
+                        val displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+                        if (displayNameIndex != -1) {
+                            var title = cursor.getString(displayNameIndex)
+                            val dotIndex = title.lastIndexOf(".")
+                            if (dotIndex > 0) {
+                                title = title.substring(0, dotIndex)
+                            }
+                            return title
+                        }
+                    } else {
+                        LogUtils.e("No ringtone for uri: %s", mUri)
+                    }
+                }
+            } catch (e: Exception) {
+                LogUtils.e("Unable to locate title for custom ringtone: $mUri", e)
+            }
+
+            return mContext.getString(R.string.unknown_ringtone_title)
+        }
+
+        override fun onPostExecute(title: String) {
+            // Add the new custom ringtone to the data model.
+            DataModel.dataModel.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.
+            LoaderManager.getInstance(this@RingtonePickerActivity).restartLoader(0 /* id */,
+                    null /* args */, this@RingtonePickerActivity /* 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 inner class RemoveCustomRingtoneTask(private val mRemoveUri: Uri)
+        : AsyncTask<Void?, Void?, Void?>() {
+        private lateinit var mSystemDefaultRingtoneUri: Uri
+
+        override fun doInBackground(vararg voids: Void?): Void? {
+            mSystemDefaultRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+
+            // Update all alarms that use the custom ringtone to use the system default.
+            val cr: ContentResolver = getContentResolver()
+            val alarms = Alarm.getAlarms(cr, null)
+            for (alarm in alarms) {
+                if (mRemoveUri == alarm.alert) {
+                    alarm.alert = mSystemDefaultRingtoneUri
+                    // Start a second background task to persist the updated alarm.
+                    AlarmUpdateHandler(this@RingtonePickerActivity, null, null)
+                            .asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+                }
+            }
+
+            try {
+                // Release the permission to read (playback) the audio at the uri.
+                cr.releasePersistableUriPermission(mRemoveUri,
+                        Intent.FLAG_GRANT_READ_URI_PERMISSION)
+            } catch (ignore: SecurityException) {
+                // 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 fun onPostExecute(v: Void?) {
+            // Reset the default alarm ringtone if it was just removed.
+            if (mRemoveUri == DataModel.dataModel.defaultAlarmRingtoneUri) {
+                DataModel.dataModel.defaultAlarmRingtoneUri = mSystemDefaultRingtoneUri
+            }
+
+            // Reset the timer ringtone if it was just removed.
+            if (mRemoveUri == DataModel.dataModel.timerRingtoneUri) {
+                val timerRingtoneUri = DataModel.dataModel.defaultTimerRingtoneUri
+                DataModel.dataModel.timerRingtoneUri = timerRingtoneUri
+            }
+
+            // Remove the corresponding custom ringtone.
+            DataModel.dataModel.removeCustomRingtone(mRemoveUri)
+
+            // Find the ringtone to be removed from the adapter.
+            val toRemove = getRingtoneHolder(mRemoveUri) ?: return
+
+            // If the ringtone to remove is also the selected ringtone, adjust the selection.
+            if (toRemove.isSelected) {
+                stopPlayingRingtone(toRemove, false)
+                val defaultRingtone = getRingtoneHolder(mDefaultRingtoneUri)
+                if (defaultRingtone != null) {
+                    defaultRingtone.isSelected = true
+                    mSelectedRingtoneUri = defaultRingtone.uri
+                    defaultRingtone.notifyItemChanged()
+                }
+            }
+
+            // Remove the ringtone from the adapter.
+            mRingtoneAdapter.removeItem(toRemove)
+        }
+    }
+
+    companion object {
+        /** Key to an extra that defines resource id to the title of this activity.  */
+        private const val EXTRA_TITLE = "extra_title"
+
+        /** Key to an extra that identifies the alarm to which the selected ringtone is attached. */
+        private const val EXTRA_ALARM_ID = "extra_alarm_id"
+
+        /** Key to an extra that identifies the selected ringtone.  */
+        private const val EXTRA_RINGTONE_URI = "extra_ringtone_uri"
+
+        /** Key to an extra that defines the uri representing the default ringtone.  */
+        private const val EXTRA_DEFAULT_RINGTONE_URI = "extra_default_ringtone_uri"
+
+        /** Key to an extra that defines the name of the default ringtone.  */
+        private const val EXTRA_DEFAULT_RINGTONE_NAME = "extra_default_ringtone_name"
+
+        /** Key to an instance state value indicating if the
+         * selected ringtone is currently playing. */
+        private const val STATE_KEY_PLAYING = "extra_is_playing"
+
+        /**
+         * @return an intent that launches the ringtone picker to edit the ringtone of the given
+         * `alarm`
+         */
+        @JvmStatic
+        @Keep
+        fun createAlarmRingtonePickerIntent(context: Context, alarm: Alarm): Intent {
+            return Intent(context, RingtonePickerActivity::class.java)
+                    .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(RingtoneManager.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
+         */
+        @JvmStatic
+        @Keep
+        fun createTimerRingtonePickerIntent(context: Context): Intent {
+            val dataModel = DataModel.dataModel
+            return Intent(context, RingtonePickerActivity::class.java)
+                    .putExtra(EXTRA_TITLE, R.string.timer_sound)
+                    .putExtra(EXTRA_RINGTONE_URI, dataModel.timerRingtoneUri)
+                    .putExtra(EXTRA_DEFAULT_RINGTONE_URI, dataModel.defaultTimerRingtoneUri)
+                    .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_timer_ringtone_title)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneViewHolder.java b/src/com/android/deskclock/ringtone/RingtoneViewHolder.java
deleted file mode 100644
index 3e0a0b8..0000000
--- a/src/com/android/deskclock/ringtone/RingtoneViewHolder.java
+++ /dev/null
@@ -1,129 +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.ringtone;
-
-import android.graphics.PorterDuff;
-import androidx.core.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/RingtoneViewHolder.kt b/src/com/android/deskclock/ringtone/RingtoneViewHolder.kt
new file mode 100644
index 0000000..86c3fc7
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneViewHolder.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.ContextMenu
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.View
+import android.view.ViewGroup
+import android.view.ContextMenu.ContextMenuInfo
+import android.view.View.OnCreateContextMenuListener
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+
+internal class RingtoneViewHolder private constructor(itemView: View)
+    : ItemViewHolder<RingtoneHolder>(itemView), View.OnClickListener, OnCreateContextMenuListener {
+    private val mSelectedView: View = itemView.findViewById(R.id.sound_image_selected)
+    private val mNameView: TextView = itemView.findViewById<View>(R.id.ringtone_name) as TextView
+    private val mImageView: ImageView =
+            itemView.findViewById<View>(R.id.ringtone_image) as ImageView
+
+    init {
+        itemView.setOnClickListener(this)
+    }
+
+    override fun onBindItemView(itemHolder: RingtoneHolder) {
+        mNameView.text = itemHolder.name
+        val opaque = itemHolder.isSelected || !itemHolder.hasPermissions()
+        mNameView.alpha = if (opaque) 1f else .63f
+        mImageView.alpha = if (opaque) 1f else .63f
+        mImageView.clearColorFilter()
+
+        val itemViewType: Int = getItemViewType()
+        if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
+            if (!itemHolder.hasPermissions()) {
+                mImageView.setImageResource(R.drawable.ic_ringtone_not_found)
+                val 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.visibility = if (itemHolder.isSelected) View.VISIBLE else View.GONE
+
+        val bgColorId = if (itemHolder.isSelected) R.color.white_08p else R.color.transparent
+        itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), bgColorId))
+
+        if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
+            itemView.setOnCreateContextMenuListener(this)
+        }
+    }
+
+    override fun onClick(view: View) {
+        if (itemHolder!!.hasPermissions()) {
+            notifyItemClicked(CLICK_NORMAL)
+        } else {
+            notifyItemClicked(CLICK_NO_PERMISSIONS)
+        }
+    }
+
+    override fun onCreateContextMenu(
+        contextMenu: ContextMenu,
+        view: View,
+        contextMenuInfo: ContextMenuInfo
+    ) {
+        notifyItemClicked(CLICK_LONG_PRESS)
+        contextMenu.add(Menu.NONE, 0, Menu.NONE, R.string.remove_sound)
+    }
+
+    class Factory internal constructor(private val mInflater: LayoutInflater)
+        : ItemViewHolder.Factory {
+        override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+            val itemView = mInflater.inflate(R.layout.ringtone_item_sound, parent, false)
+            return RingtoneViewHolder(itemView)
+        }
+    }
+
+    companion object {
+        const val VIEW_TYPE_SYSTEM_SOUND = R.layout.ringtone_item_sound
+        const val VIEW_TYPE_CUSTOM_SOUND = -R.layout.ringtone_item_sound
+        const val CLICK_NORMAL = 0
+        const val CLICK_LONG_PRESS = -1
+        const val CLICK_NO_PERMISSIONS = -2
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityListener.java b/src/com/android/deskclock/ringtone/SystemRingtoneHolder.kt
similarity index 63%
copy from src/com/android/deskclock/data/CityListener.java
copy to src/com/android/deskclock/ringtone/SystemRingtoneHolder.kt
index 91f66b3..73be7d7 100644
--- a/src/com/android/deskclock/data/CityListener.java
+++ b/src/com/android/deskclock/ringtone/SystemRingtoneHolder.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.data;
+package com.android.deskclock.ringtone
 
-import java.util.List;
+import android.net.Uri
 
-/**
- * The interface through which interested parties are notified of changes to the world cities list.
- */
-public interface CityListener {
-    void citiesChanged(List<City> oldCities, List<City> newCities);
+internal class SystemRingtoneHolder(uri: Uri, name: String?) : RingtoneHolder(uri, name) {
+    override fun getItemViewType(): Int {
+        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
deleted file mode 100644
index b1eb8ce..0000000
--- a/src/com/android/deskclock/settings/AlarmVolumePreference.java
+++ /dev/null
@@ -1,140 +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.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 androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.SeekBar;
-
-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 {
-
-    private static final long ALARM_PREVIEW_DURATION_MS = 2000;
-
-    private SeekBar mSeekbar;
-    private ImageView mAlarmIcon;
-    private boolean mPreviewPlaying;
-
-    public AlarmVolumePreference(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    public void onBindViewHolder(PreferenceViewHolder holder) {
-        super.onBindViewHolder(holder);
-
-        final Context context = getContext();
-        final AudioManager audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
-
-        // Disable click feedback for this preference.
-        holder.itemView.setClickable(false);
-
-        mSeekbar = (SeekBar) holder.findViewById(R.id.alarm_volume_slider);
-        mSeekbar.setMax(audioManager.getStreamMaxVolume(STREAM_ALARM));
-        mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM));
-        mAlarmIcon = (ImageView) holder.findViewById(R.id.alarm_icon);
-        onSeekbarChanged();
-
-        final ContentObserver volumeObserver = new ContentObserver(mSeekbar.getHandler()) {
-            @Override
-            public void onChange(boolean selfChange) {
-                // Volume was changed elsewhere, update our slider.
-                mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM));
-            }
-        };
-
-        mSeekbar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
-            @Override
-            public void onViewAttachedToWindow(View v) {
-                context.getContentResolver().registerContentObserver(Settings.System.CONTENT_URI,
-                        true, volumeObserver);
-            }
-
-            @Override
-            public void onViewDetachedFromWindow(View v) {
-                context.getContentResolver().unregisterContentObserver(volumeObserver);
-            }
-        });
-
-        mSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
-            @Override
-            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
-                if (fromUser) {
-                    audioManager.setStreamVolume(STREAM_ALARM, progress, 0);
-                }
-                onSeekbarChanged();
-            }
-
-            @Override
-            public void onStartTrackingTouch(SeekBar seekBar) {
-            }
-
-            @Override
-            public void onStopTrackingTouch(SeekBar seekBar) {
-                if (!mPreviewPlaying && seekBar.getProgress() != 0) {
-                    // If we are not currently playing and progress is set to non-zero, start.
-                    RingtonePreviewKlaxon.start(
-                            context, DataModel.getDataModel().getDefaultAlarmRingtoneUri());
-                    mPreviewPlaying = true;
-                    seekBar.postDelayed(new Runnable() {
-                        @Override
-                        public void run() {
-                            RingtonePreviewKlaxon.stop(context);
-                            mPreviewPlaying = false;
-                        }
-                    }, ALARM_PREVIEW_DURATION_MS);
-                }
-            }
-        });
-    }
-
-    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/AlarmVolumePreference.kt b/src/com/android/deskclock/settings/AlarmVolumePreference.kt
new file mode 100644
index 0000000..dd08eea
--- /dev/null
+++ b/src/com/android/deskclock/settings/AlarmVolumePreference.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.TargetApi
+import android.app.NotificationManager
+import android.content.Context
+import android.content.Context.AUDIO_SERVICE
+import android.content.Context.NOTIFICATION_SERVICE
+import android.database.ContentObserver
+import android.media.AudioManager
+import android.media.AudioManager.STREAM_ALARM
+import android.os.Build
+import android.provider.Settings
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.SeekBar
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+
+import com.android.deskclock.R
+import com.android.deskclock.RingtonePreviewKlaxon
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+class AlarmVolumePreference(context: Context?, attrs: AttributeSet?) : Preference(context!!, attrs) {
+    private lateinit var mSeekbar: SeekBar
+
+    private var mPreviewPlaying = false
+
+    override fun onBindViewHolder(holder: PreferenceViewHolder) {
+        super.onBindViewHolder(holder)
+        val context: Context = getContext()
+        val audioManager: AudioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager
+
+        // Disable click feedback for this preference.
+        holder.itemView.setClickable(false)
+        // Minimum volume for alarm is not 0, calculate it.
+        val maxVolume = audioManager.getStreamMaxVolume(STREAM_ALARM) - getMinVolume(audioManager)
+        mSeekbar = holder.findViewById(R.id.alarm_volume_slider) as SeekBar
+        mSeekbar.setMax(maxVolume)
+        mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM) -
+                getMinVolume(audioManager))
+        (holder.findViewById(R.id.alarm_icon) as ImageView)
+                .setImageResource(R.drawable.ic_alarm_small)
+        onSeekbarChanged()
+
+        val volumeObserver: ContentObserver = object : ContentObserver(mSeekbar.getHandler()) {
+            override fun onChange(selfChange: Boolean) {
+                // Volume was changed elsewhere, update our slider.
+                mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM) -
+                        getMinVolume(audioManager))
+            }
+        }
+
+        mSeekbar.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+            override fun onViewAttachedToWindow(v: View) {
+                context.getContentResolver().registerContentObserver(Settings.System.CONTENT_URI,
+                        true, volumeObserver)
+            }
+
+            override fun onViewDetachedFromWindow(v: View) {
+                context.getContentResolver().unregisterContentObserver(volumeObserver)
+            }
+        })
+
+        mSeekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+                if (fromUser) {
+                    val newVolume = progress + getMinVolume(audioManager)
+                    audioManager.setStreamVolume(STREAM_ALARM, newVolume, 0)
+                }
+                onSeekbarChanged()
+            }
+
+            override fun onStartTrackingTouch(seekBar: SeekBar?) {
+            }
+
+            override fun onStopTrackingTouch(seekBar: SeekBar) {
+                if (!mPreviewPlaying) {
+                    // If we are not currently playing, start.
+                    RingtonePreviewKlaxon
+                            .start(context, DataModel.dataModel.defaultAlarmRingtoneUri)
+                    mPreviewPlaying = true
+                    seekBar.postDelayed(Runnable {
+                        RingtonePreviewKlaxon.stop(context)
+                        mPreviewPlaying = false
+                    }, ALARM_PREVIEW_DURATION_MS)
+                }
+            }
+        })
+    }
+
+    private fun onSeekbarChanged() {
+        mSeekbar.setEnabled(doesDoNotDisturbAllowAlarmPlayback())
+    }
+
+    private fun doesDoNotDisturbAllowAlarmPlayback(): Boolean {
+        return !Utils.isNOrLater || doesDoNotDisturbAllowAlarmPlaybackNPlus()
+    }
+
+    @TargetApi(Build.VERSION_CODES.N)
+    private fun doesDoNotDisturbAllowAlarmPlaybackNPlus(): Boolean {
+        val notificationManager =
+                getContext().getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+        return notificationManager.getCurrentInterruptionFilter() !=
+                NotificationManager.INTERRUPTION_FILTER_NONE
+    }
+
+    private fun getMinVolume(audioManager: AudioManager): Int {
+        return if (Utils.isPOrLater) audioManager.getStreamMinVolume(STREAM_ALARM) else 0
+    }
+
+    companion object {
+        private const val ALARM_PREVIEW_DURATION_MS: Long = 2000
+    }
+}
diff --git a/src/com/android/deskclock/settings/ScreensaverSettingsActivity.java b/src/com/android/deskclock/settings/ScreensaverSettingsActivity.java
deleted file mode 100644
index edc0554..0000000
--- a/src/com/android/deskclock/settings/ScreensaverSettingsActivity.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.annotation.TargetApi;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceFragment;
-import androidx.appcompat.app.AppCompatActivity;
-import android.view.MenuItem;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-/**
- * Settings for Clock screen saver
- */
-public final class ScreensaverSettingsActivity extends AppCompatActivity {
-
-    public static final String KEY_CLOCK_STYLE = "screensaver_clock_style";
-    public static final String KEY_NIGHT_MODE = "screensaver_night_mode";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.screensaver_settings);
-    }
-
-    @Override
-    public boolean onOptionsItemSelected (MenuItem item) {
-        switch (item.getItemId()) {
-            case android.R.id.home:
-                finish();
-                return true;
-            default:
-                break;
-        }
-        return super.onOptionsItemSelected(item);
-    }
-
-
-    public static class PrefsFragment extends PreferenceFragment
-            implements Preference.OnPreferenceChangeListener {
-
-        @Override
-        @TargetApi(Build.VERSION_CODES.N)
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-
-            if (Utils.isNOrLater()) {
-                getPreferenceManager().setStorageDeviceProtected();
-            }
-            addPreferencesFromResource(R.xml.screensaver_settings);
-        }
-
-        @Override
-        public void onResume() {
-            super.onResume();
-            refresh();
-        }
-
-        @Override
-        public boolean onPreferenceChange(Preference pref, Object newValue) {
-            if (KEY_CLOCK_STYLE.equals(pref.getKey())) {
-                final ListPreference clockStylePref = (ListPreference) pref;
-                final int index = clockStylePref.findIndexOfValue((String) newValue);
-                clockStylePref.setSummary(clockStylePref.getEntries()[index]);
-            }
-            return true;
-        }
-
-        private void refresh() {
-            final ListPreference clockStylePref = (ListPreference) findPreference(KEY_CLOCK_STYLE);
-            clockStylePref.setSummary(clockStylePref.getEntry());
-            clockStylePref.setOnPreferenceChangeListener(this);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/settings/ScreensaverSettingsActivity.kt b/src/com/android/deskclock/settings/ScreensaverSettingsActivity.kt
new file mode 100644
index 0000000..dfe68dc
--- /dev/null
+++ b/src/com/android/deskclock/settings/ScreensaverSettingsActivity.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.TargetApi
+import android.os.Build
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.appcompat.app.AppCompatActivity
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+/**
+ * Settings for Clock screen saver
+ */
+class ScreensaverSettingsActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.screensaver_settings)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.getItemId()) {
+            android.R.id.home -> {
+                finish()
+                return true
+            }
+            else -> {
+            }
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+    class PrefsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
+
+        @TargetApi(Build.VERSION_CODES.N)
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            if (Utils.isNOrLater) {
+                getPreferenceManager().setStorageDeviceProtected()
+            }
+        }
+
+        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+            addPreferencesFromResource(R.xml.screensaver_settings)
+        }
+
+        override fun onResume() {
+            super.onResume()
+            refresh()
+        }
+
+        override fun onPreferenceChange(pref: Preference, newValue: Any?): Boolean {
+            if (KEY_CLOCK_STYLE == pref.getKey()) {
+                val clockStylePref: ListPreference = pref as ListPreference
+                val index: Int = clockStylePref.findIndexOfValue(newValue as String?)
+                clockStylePref.setSummary(clockStylePref.getEntries().get(index))
+            }
+            return true
+        }
+
+        private fun refresh() {
+            val clockStylePref = findPreference<ListPreference>(KEY_CLOCK_STYLE) as ListPreference
+            clockStylePref.setSummary(clockStylePref.getEntry())
+            clockStylePref.setOnPreferenceChangeListener(this)
+        }
+    }
+
+    companion object {
+        const val KEY_CLOCK_STYLE = "screensaver_clock_style"
+        const val KEY_NIGHT_MODE = "screensaver_night_mode"
+    }
+}
diff --git a/src/com/android/deskclock/settings/SettingsActivity.java b/src/com/android/deskclock/settings/SettingsActivity.java
deleted file mode 100644
index dcb5707..0000000
--- a/src/com/android/deskclock/settings/SettingsActivity.java
+++ /dev/null
@@ -1,328 +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.content.Intent;
-import android.os.Bundle;
-import android.os.Vibrator;
-import android.provider.Settings;
-import androidx.preference.ListPreference;
-import androidx.preference.ListPreferenceDialogFragmentCompat;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceDialogFragmentCompat;
-import androidx.preference.PreferenceFragmentCompat;
-import androidx.preference.TwoStatePreference;
-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.R;
-import com.android.deskclock.Utils;
-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 com.android.deskclock.data.TimeZones;
-import com.android.deskclock.data.Weekdays;
-import com.android.deskclock.ringtone.RingtonePickerActivity;
-
-/**
- * Settings for the Alarm Clock.
- */
-public final class SettingsActivity extends BaseActivity {
-
-    public static final String KEY_ALARM_SNOOZE = "snooze_duration";
-    public static final String KEY_ALARM_CRESCENDO = "alarm_crescendo_duration";
-    public static final String KEY_TIMER_CRESCENDO = "timer_crescendo_duration";
-    public static final String KEY_TIMER_RINGTONE = "timer_ringtone";
-    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";
-    public static final String KEY_VOLUME_BUTTONS = "volume_button_setting";
-    public static final String KEY_WEEK_START = "week_start";
-
-    public static final String DEFAULT_VOLUME_BEHAVIOR = "0";
-    public static final String VOLUME_BEHAVIOR_SNOOZE = "1";
-    public static final String VOLUME_BEHAVIOR_DISMISS = "2";
-
-    public static final String PREFS_FRAGMENT_TAG = "prefs_fragment";
-    public static final String PREFERENCE_DIALOG_FRAGMENT_TAG = "preference_dialog";
-
-    private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
-
-    /**
-     * The controller that shows the drop shadow when content is not scrolled to the top.
-     */
-    private DropShadowController mDropShadowController;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.settings);
-
-        mOptionsMenuManager.addMenuItemController(new NavUpMenuItemController(this))
-                .addMenuItemController(MenuItemControllerFactory.getInstance()
-                        .buildMenuItemControllers(this));
-
-        // Create the prefs fragment in code to ensure it's created before PreferenceDialogFragment
-        if (savedInstanceState == null) {
-            getSupportFragmentManager().beginTransaction()
-                    .replace(R.id.main, new PrefsFragment(), PREFS_FRAGMENT_TAG)
-                    .disallowAddToBackStack()
-                    .commit();
-        }
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-
-        final View dropShadow = findViewById(R.id.drop_shadow);
-        final PrefsFragment fragment =
-                (PrefsFragment) getSupportFragmentManager().findFragmentById(R.id.main);
-        mDropShadowController = new DropShadowController(dropShadow, fragment.getListView());
-    }
-
-    @Override
-    protected void onPause() {
-        mDropShadowController.stop();
-        super.onPause();
-    }
-
-    @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);
-    }
-
-    public static class PrefsFragment extends PreferenceFragmentCompat implements
-            Preference.OnPreferenceChangeListener,
-            Preference.OnPreferenceClickListener {
-
-        @Override
-        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();
-        }
-
-        @Override
-        public void onActivityCreated(Bundle savedInstanceState) {
-            super.onActivityCreated(savedInstanceState);
-
-            // By default, do not recreate the DeskClock activity
-            getActivity().setResult(RESULT_CANCELED);
-        }
-
-        @Override
-        public void onResume() {
-            super.onResume();
-            refresh();
-        }
-
-        @Override
-        public boolean onPreferenceChange(Preference pref, Object newValue) {
-            switch (pref.getKey()) {
-                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:
-                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_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_TIMER_VIBRATE:
-                    final TwoStatePreference timerVibratePref = (TwoStatePreference) pref;
-                    DataModel.getDataModel().setTimerVibrate(timerVibratePref.isChecked());
-                    break;
-                case KEY_TIMER_RINGTONE:
-                    pref.setSummary(DataModel.getDataModel().getTimerRingtoneTitle());
-                    break;
-            }
-            // Set result so DeskClock knows to refresh itself
-            getActivity().setResult(RESULT_OK);
-            return true;
-        }
-
-        @Override
-        public boolean onPreferenceClick(Preference pref) {
-            final Context context = getActivity();
-            if (context == null) {
-                return false;
-            }
-
-            switch (pref.getKey()) {
-                case KEY_DATE_TIME:
-                    final Intent dialogIntent = new Intent(Settings.ACTION_DATE_SETTINGS);
-                    dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                    startActivity(dialogIntent);
-                    return true;
-                case KEY_TIMER_RINGTONE:
-                    startActivity(RingtonePickerActivity.createTimerRingtonePickerIntent(context));
-                    return true;
-            }
-
-            return false;
-        }
-
-        @Override
-        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);
-        }
-
-        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 TimeZones timezones = DataModel.getDataModel().getTimeZones();
-            final ListPreference homeTimezonePref = (ListPreference) findPreference(KEY_HOME_TZ);
-            homeTimezonePref.setEntryValues(timezones.getTimeZoneIds());
-            homeTimezonePref.setEntries(timezones.getTimeZoneNames());
-            homeTimezonePref.setSummary(homeTimezonePref.getEntry());
-            homeTimezonePref.setOnPreferenceChangeListener(this);
-        }
-
-        private void refresh() {
-            final ListPreference autoSilencePref =
-                    (ListPreference) findPreference(KEY_AUTO_SILENCE);
-            String delay = autoSilencePref.getValue();
-            updateAutoSnoozeSummary(autoSilencePref, delay);
-            autoSilencePref.setOnPreferenceChangeListener(this);
-
-            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();
-            autoHomeClockPref.setOnPreferenceChangeListener(this);
-
-            final ListPreference homeTimezonePref = (ListPreference) findPreference(KEY_HOME_TZ);
-            homeTimezonePref.setEnabled(autoHomeClockEnabled);
-            refreshListPreference(homeTimezonePref);
-
-            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 SimpleMenuPreference weekStartPref = (SimpleMenuPreference)
-                    findPreference(KEY_WEEK_START);
-            // Set the default value programmatically
-            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);
-
-            final Preference timerRingtonePref = findPreference(KEY_TIMER_RINGTONE);
-            timerRingtonePref.setOnPreferenceClickListener(this);
-            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) {
-                listPref.setSummary(R.string.auto_silence_never);
-            } else {
-                listPref.setSummary(Utils.getNumberFormattedQuantityString(getActivity(),
-                        R.plurals.auto_silence_summary, i));
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/SettingsActivity.kt b/src/com/android/deskclock/settings/SettingsActivity.kt
new file mode 100644
index 0000000..c5bccb0
--- /dev/null
+++ b/src/com/android/deskclock/settings/SettingsActivity.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.content.Intent
+import android.os.Bundle
+import android.os.Vibrator
+import android.provider.Settings
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.preference.ListPreference
+import androidx.preference.ListPreferenceDialogFragmentCompat
+import androidx.preference.Preference
+import androidx.preference.PreferenceDialogFragmentCompat
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.TwoStatePreference
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.DropShadowController
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+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 com.android.deskclock.ringtone.RingtonePickerActivity
+
+/**
+ * Settings for the Alarm Clock.
+ */
+class SettingsActivity : BaseActivity() {
+    private val mOptionsMenuManager = OptionsMenuManager()
+
+    /**
+     * The controller that shows the drop shadow when content is not scrolled to the top.
+     */
+    private lateinit var mDropShadowController: DropShadowController
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.settings)
+
+        mOptionsMenuManager.addMenuItemController(NavUpMenuItemController(this))
+                .addMenuItemController(*MenuItemControllerFactory.buildMenuItemControllers(this))
+
+        // Create the prefs fragment in code to ensure it's created before PreferenceDialogFragment
+        if (savedInstanceState == null) {
+            getSupportFragmentManager().beginTransaction()
+                    .replace(R.id.main, PrefsFragment(), PREFS_FRAGMENT_TAG)
+                    .disallowAddToBackStack()
+                    .commit()
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        val dropShadow: View = findViewById(R.id.drop_shadow)
+        val fragment = getSupportFragmentManager().findFragmentById(R.id.main) as PrefsFragment
+        mDropShadowController = DropShadowController(dropShadow, fragment.getListView())
+    }
+
+    override fun onPause() {
+        mDropShadowController.stop()
+        super.onPause()
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        mOptionsMenuManager.onCreateOptionsMenu(menu)
+        return true
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+        mOptionsMenuManager.onPrepareOptionsMenu(menu)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return (mOptionsMenuManager.onOptionsItemSelected(item) ||
+                super.onOptionsItemSelected(item))
+    }
+
+    class PrefsFragment :
+            PreferenceFragmentCompat(),
+            Preference.OnPreferenceChangeListener,
+            Preference.OnPreferenceClickListener {
+
+        override fun onCreatePreferences(bundle: Bundle?, rootKey: String?) {
+            getPreferenceManager().setStorageDeviceProtected()
+            addPreferencesFromResource(R.xml.settings)
+            val timerVibrate: Preference? = findPreference(KEY_TIMER_VIBRATE)
+            timerVibrate?.let {
+                val hasVibrator: Boolean = (it.getContext()
+                        .getSystemService(VIBRATOR_SERVICE) as Vibrator).hasVibrator()
+                it.setVisible(hasVibrator)
+            }
+            loadTimeZoneList()
+        }
+
+        override fun onActivityCreated(savedInstanceState: Bundle?) {
+            super.onActivityCreated(savedInstanceState)
+
+            // By default, do not recreate the DeskClock activity
+            getActivity()?.setResult(RESULT_CANCELED)
+        }
+
+        override fun onResume() {
+            super.onResume()
+            refresh()
+        }
+
+        override fun onPreferenceChange(pref: Preference, newValue: Any): Boolean {
+            when (pref.getKey()) {
+                KEY_ALARM_CRESCENDO, KEY_HOME_TZ, KEY_ALARM_SNOOZE, KEY_TIMER_CRESCENDO -> {
+                    val preference: ListPreference = pref as ListPreference
+                    val index: Int = preference.findIndexOfValue(newValue as String)
+                    preference.setSummary(preference.getEntries().get(index))
+                }
+                KEY_CLOCK_STYLE, KEY_WEEK_START, KEY_VOLUME_BUTTONS -> {
+                    val simpleMenuPreference = pref as SimpleMenuPreference
+                    val i: Int = simpleMenuPreference.findIndexOfValue(newValue as String)
+                    pref.setSummary(simpleMenuPreference.getEntries().get(i))
+                }
+                KEY_CLOCK_DISPLAY_SECONDS -> {
+                    DataModel.dataModel.displayClockSeconds = newValue as Boolean
+                }
+                KEY_AUTO_SILENCE -> {
+                    val delay = newValue as String
+                    updateAutoSnoozeSummary(pref as ListPreference, delay)
+                }
+                KEY_AUTO_HOME_CLOCK -> {
+                    val autoHomeClockEnabled: Boolean = (pref as TwoStatePreference).isChecked()
+                    val homeTimeZonePref: Preference? = findPreference(KEY_HOME_TZ)
+                    homeTimeZonePref?.setEnabled(!autoHomeClockEnabled)
+                }
+                KEY_TIMER_VIBRATE -> {
+                    val timerVibratePref: TwoStatePreference = pref as TwoStatePreference
+                    DataModel.dataModel.timerVibrate = timerVibratePref.isChecked()
+                }
+                KEY_TIMER_RINGTONE -> pref.setSummary(DataModel.dataModel.timerRingtoneTitle)
+            }
+
+            // Set result so DeskClock knows to refresh itself
+            getActivity()?.setResult(RESULT_OK)
+            return true
+        }
+
+        override fun onPreferenceClick(pref: Preference): Boolean {
+            val context: Context = getActivity() ?: return false
+
+            when (pref.getKey()) {
+                KEY_DATE_TIME -> {
+                    val dialogIntent = Intent(Settings.ACTION_DATE_SETTINGS)
+                    dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    startActivity(dialogIntent)
+                    return true
+                }
+                KEY_TIMER_RINGTONE -> {
+                    startActivity(RingtonePickerActivity.createTimerRingtonePickerIntent(context))
+                    return true
+                }
+                else -> return false
+            }
+        }
+
+        override fun onDisplayPreferenceDialog(preference: Preference) {
+            // Only single-selection lists are currently supported.
+            val f: PreferenceDialogFragmentCompat
+            f = if (preference is ListPreference) {
+                ListPreferenceDialogFragmentCompat.newInstance(preference.getKey())
+            } else {
+                throw IllegalArgumentException("Unsupported DialogPreference type")
+            }
+            showDialog(f)
+        }
+
+        private fun showDialog(fragment: PreferenceDialogFragmentCompat) {
+            // Don't show dialog if one is already shown.
+            if (parentFragmentManager.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(parentFragmentManager, PREFERENCE_DIALOG_FRAGMENT_TAG)
+        }
+
+        /**
+         * Reconstruct the timezone list.
+         */
+        private fun loadTimeZoneList() {
+            val timezones = DataModel.dataModel.timeZones
+            val homeTimezonePref: ListPreference? = findPreference(KEY_HOME_TZ)
+            homeTimezonePref?.let {
+                it.setEntryValues(timezones.timeZoneIds)
+                it.setEntries(timezones.timeZoneNames)
+                it.setSummary(homeTimezonePref.getEntry())
+                it.setOnPreferenceChangeListener(this)
+            }
+        }
+
+        private fun refresh() {
+            val autoSilencePref: ListPreference? = findPreference(KEY_AUTO_SILENCE)
+            autoSilencePref?.let {
+                val delay: String = it.getValue()
+                updateAutoSnoozeSummary(it, delay)
+                it.setOnPreferenceChangeListener(this)
+            }
+
+            val clockStylePref: SimpleMenuPreference? = findPreference(KEY_CLOCK_STYLE)
+            clockStylePref?.let {
+                it.setSummary(it.getEntry())
+                it.setOnPreferenceChangeListener(this)
+            }
+
+            val volumeButtonsPref: SimpleMenuPreference? = findPreference(KEY_VOLUME_BUTTONS)
+            volumeButtonsPref?.let {
+                it.setSummary(volumeButtonsPref.getEntry())
+                it.setOnPreferenceChangeListener(this)
+            }
+
+            val clockSecondsPref: Preference? = findPreference(KEY_CLOCK_DISPLAY_SECONDS)
+            clockSecondsPref?.setOnPreferenceChangeListener(this)
+
+            val autoHomeClockPref: Preference? = findPreference(KEY_AUTO_HOME_CLOCK)
+            val autoHomeClockEnabled: Boolean =
+                    (autoHomeClockPref as TwoStatePreference).isChecked()
+            autoHomeClockPref.setOnPreferenceChangeListener(this)
+
+            val homeTimezonePref: ListPreference? = findPreference(KEY_HOME_TZ)
+            homeTimezonePref?.setEnabled(autoHomeClockEnabled)
+            refreshListPreference(homeTimezonePref!!)
+
+            refreshListPreference(findPreference(KEY_ALARM_CRESCENDO)!!)
+            refreshListPreference(findPreference(KEY_TIMER_CRESCENDO)!!)
+            refreshListPreference(findPreference(KEY_ALARM_SNOOZE)!!)
+
+            val dateAndTimeSetting: Preference? = findPreference(KEY_DATE_TIME)
+            dateAndTimeSetting?.setOnPreferenceClickListener(this)
+
+            val weekStartPref: SimpleMenuPreference? = findPreference(KEY_WEEK_START)
+            // Set the default value programmatically
+            val weekdayOrder = DataModel.dataModel.weekdayOrder
+            val firstDay = weekdayOrder.calendarDays[0]
+            val value = firstDay.toString()
+            weekStartPref?.let {
+                val idx: Int = it.findIndexOfValue(value)
+                it.setValueIndex(idx)
+                it.setSummary(weekStartPref.getEntries().get(idx))
+                it.setOnPreferenceChangeListener(this)
+            }
+
+            val timerRingtonePref: Preference? = findPreference(KEY_TIMER_RINGTONE)
+            timerRingtonePref?.let {
+                it.setOnPreferenceClickListener(this)
+                it.setSummary(DataModel.dataModel.timerRingtoneTitle)
+            }
+        }
+
+        private fun refreshListPreference(preference: ListPreference) {
+            preference.setSummary(preference.getEntry())
+            preference.setOnPreferenceChangeListener(this)
+        }
+
+        private fun updateAutoSnoozeSummary(listPref: ListPreference, delay: String) {
+            val i = delay.toInt()
+            if (i == -1) {
+                listPref.setSummary(R.string.auto_silence_never)
+            } else {
+                listPref.setSummary(Utils.getNumberFormattedQuantityString(getActivity()!!,
+                        R.plurals.auto_silence_summary, i))
+            }
+        }
+    }
+
+    companion object {
+        const val KEY_ALARM_SNOOZE = "snooze_duration"
+        const val KEY_ALARM_CRESCENDO = "alarm_crescendo_duration"
+        const val KEY_TIMER_CRESCENDO = "timer_crescendo_duration"
+        const val KEY_TIMER_RINGTONE = "timer_ringtone"
+        const val KEY_TIMER_VIBRATE = "timer_vibrate"
+        const val KEY_AUTO_SILENCE = "auto_silence"
+        const val KEY_CLOCK_STYLE = "clock_style"
+        const val KEY_CLOCK_DISPLAY_SECONDS = "display_clock_seconds"
+        const val KEY_HOME_TZ = "home_time_zone"
+        const val KEY_AUTO_HOME_CLOCK = "automatic_home_clock"
+        const val KEY_DATE_TIME = "date_time"
+        const val KEY_VOLUME_BUTTONS = "volume_button_setting"
+        const val KEY_WEEK_START = "week_start"
+        const val DEFAULT_VOLUME_BEHAVIOR = "0"
+        const val VOLUME_BEHAVIOR_SNOOZE = "1"
+        const val VOLUME_BEHAVIOR_DISMISS = "2"
+        const val PREFS_FRAGMENT_TAG = "prefs_fragment"
+        const val PREFERENCE_DIALOG_FRAGMENT_TAG = "preference_dialog"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/SimpleMenuPreference.java b/src/com/android/deskclock/settings/SimpleMenuPreference.java
deleted file mode 100644
index 579ad57..0000000
--- a/src/com/android/deskclock/settings/SimpleMenuPreference.java
+++ /dev/null
@@ -1,143 +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.settings;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.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/SimpleMenuPreference.kt b/src/com/android/deskclock/settings/SimpleMenuPreference.kt
new file mode 100644
index 0000000..7fb8cb8
--- /dev/null
+++ b/src/com/android/deskclock/settings/SimpleMenuPreference.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.core.content.ContextCompat
+import androidx.preference.DropDownPreference
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+/**
+ * Bend [DropDownPreference] to support
+ * [Simple Menus](https://material.google.com/components/menus.html#menus-behavior).
+ */
+class SimpleMenuPreference(
+    context: Context?,
+    attrs: AttributeSet?,
+    defStyleAttr: Int,
+    defStyleRes: Int
+) : DropDownPreference(context!!, attrs, defStyleAttr, defStyleRes) {
+    private lateinit var mAdapter: SimpleMenuAdapter
+
+    constructor(context: Context?) : this(context, null) {
+    }
+
+    constructor(context: Context?, attrs: AttributeSet?) :
+            this(context, attrs, R.attr.dropdownPreferenceStyle) {
+    }
+
+    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) :
+            this(context, attrs, defStyle, 0) {
+    }
+
+    override fun createAdapter(): ArrayAdapter<CharSequence?> {
+        mAdapter = SimpleMenuAdapter(getContext(), R.layout.simple_menu_dropdown_item)
+        return mAdapter
+    }
+
+    override fun setSummary(summary: CharSequence?) {
+        val entries: Array<CharSequence> = getEntries()
+        val index = Utils.indexOf(entries, summary!!)
+        require(index != -1) { "Illegal Summary" }
+        val lastSelectedOriginalPosition = mAdapter.lastSelectedOriginalPosition
+        mAdapter.setSelectedPosition(index)
+        setSelectedPosition(entries, lastSelectedOriginalPosition, index)
+        setSelectedPosition(getEntryValues(), lastSelectedOriginalPosition, index)
+        super.setSummary(summary)
+    }
+
+    private class SimpleMenuAdapter internal constructor(context: Context, resource: Int) :
+            ArrayAdapter<CharSequence?>(context, resource) {
+
+        /** The original position of the last selected element  */
+        var lastSelectedOriginalPosition = 0
+            private set
+
+        private fun restoreOriginalOrder() {
+            val item: CharSequence? = getItem(0)
+            remove(item)
+            insert(item, lastSelectedOriginalPosition)
+        }
+
+        private fun swapSelectedToFront(position: Int) {
+            val item: CharSequence? = getItem(position)
+            remove(item)
+            insert(item, 0)
+            lastSelectedOriginalPosition = position
+        }
+
+        fun setSelectedPosition(position: Int) {
+            setNotifyOnChange(false)
+            val item: CharSequence? = getItem(position)
+            restoreOriginalOrder()
+            val originalPosition: Int = getPosition(item)
+            swapSelectedToFront(originalPosition)
+            notifyDataSetChanged()
+        }
+
+        override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
+            val 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
+        }
+    }
+
+    companion object {
+        private fun restoreOriginalOrder(
+            array: Array<CharSequence>,
+            lastSelectedOriginalPosition: Int
+        ) {
+            val item = array[0]
+            System.arraycopy(array, 1, array, 0, lastSelectedOriginalPosition)
+            array[lastSelectedOriginalPosition] = item
+        }
+
+        private fun swapSelectedToFront(array: Array<CharSequence>, position: Int) {
+            val item = array[position]
+            System.arraycopy(array, 0, array, 1, position)
+            array[0] = item
+        }
+
+        private fun setSelectedPosition(
+            array: Array<CharSequence>,
+            lastSelectedOriginalPosition: Int,
+            position: Int
+        ) {
+            val item = array[position]
+            restoreOriginalOrder(array, lastSelectedOriginalPosition)
+            val originalPosition = Utils.indexOf(array, item)
+            swapSelectedToFront(array, originalPosition)
+        }
+    }
+}
diff --git a/src/com/android/deskclock/stopwatch/LapsAdapter.java b/src/com/android/deskclock/stopwatch/LapsAdapter.java
deleted file mode 100644
index e3afaf2..0000000
--- a/src/com/android/deskclock/stopwatch/LapsAdapter.java
+++ /dev/null
@@ -1,358 +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.stopwatch;
-
-import android.content.Context;
-import androidx.annotation.VisibleForTesting;
-import androidx.recyclerview.widget.RecyclerView;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Lap;
-import com.android.deskclock.data.Stopwatch;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.text.DecimalFormatSymbols;
-import java.util.List;
-
-/**
- * Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
- * lap is at the bottom.
- */
-class LapsAdapter extends RecyclerView.Adapter<LapsAdapter.LapItemHolder> {
-
-    private static final long TEN_MINUTES = 10 * DateUtils.MINUTE_IN_MILLIS;
-    private static final long HOUR = DateUtils.HOUR_IN_MILLIS;
-    private static final long TEN_HOURS = 10 * HOUR;
-    private static final long HUNDRED_HOURS = 100 * HOUR;
-
-    /** A single space preceded by a zero-width LRM; This groups adjacent chars left-to-right. */
-    private static final String LRM_SPACE = "\u200E ";
-
-    /** Reusable StringBuilder that assembles a formatted time; alleviates memory churn. */
-    private static final StringBuilder sTimeBuilder = new StringBuilder(12);
-
-    private final LayoutInflater mInflater;
-    private final Context mContext;
-
-    /** Used to determine when the time format for the lap time column has changed length. */
-    private int mLastFormattedLapTimeLength;
-
-    /** Used to determine when the time format for the total time column has changed length. */
-    private int mLastFormattedAccumulatedTimeLength;
-
-    LapsAdapter(Context context) {
-        mContext = context;
-        mInflater = LayoutInflater.from(context);
-        setHasStableIds(true);
-    }
-
-    /**
-     * After recording the first lap, there is always a "current lap" in progress.
-     *
-     * @return 0 if no laps are yet recorded; lap count + 1 if any laps exist
-     */
-    @Override
-    public int getItemCount() {
-        final int lapCount = getLaps().size();
-        final int currentLapCount = lapCount == 0 ? 0 : 1;
-        return currentLapCount + lapCount;
-    }
-
-    @Override
-    public LapItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        final View v = mInflater.inflate(R.layout.lap_view, parent, false /* attachToRoot */);
-        return new LapItemHolder(v);
-    }
-
-    @Override
-    public void onBindViewHolder(LapItemHolder viewHolder, int position) {
-        final long lapTime;
-        final int lapNumber;
-        final long totalTime;
-
-        // Lap will be null for the current lap.
-        final Lap lap = position == 0 ? null : getLaps().get(position - 1);
-        if (lap != null) {
-            // For a recorded lap, merely extract the values to format.
-            lapTime = lap.getLapTime();
-            lapNumber = lap.getLapNumber();
-            totalTime = lap.getAccumulatedTime();
-        } else {
-            // For the current lap, compute times relative to the stopwatch.
-            totalTime = getStopwatch().getTotalTime();
-            lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
-            lapNumber = getLaps().size() + 1;
-        }
-
-        // Bind data into the child views.
-        viewHolder.lapTime.setText(formatLapTime(lapTime, true));
-        viewHolder.accumulatedTime.setText(formatAccumulatedTime(totalTime, true));
-        viewHolder.lapNumber.setText(formatLapNumber(getLaps().size() + 1, lapNumber));
-    }
-
-    @Override
-    public long getItemId(int position) {
-        final List<Lap> laps = getLaps();
-        if (position == 0) {
-            return laps.size() + 1;
-        }
-
-        return laps.get(position - 1).getLapNumber();
-    }
-
-    /**
-     * @param rv the RecyclerView that contains the {@code childView}
-     * @param totalTime time accumulated for the current lap and all prior laps
-     */
-    void updateCurrentLap(RecyclerView rv, long totalTime) {
-        // If no laps exist there is nothing to do.
-        if (getItemCount() == 0) {
-            return;
-        }
-
-        final View currentLapView = rv.getChildAt(0);
-        if (currentLapView != null) {
-            // Compute the lap time using the total time.
-            final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
-
-            final LapItemHolder holder = (LapItemHolder) rv.getChildViewHolder(currentLapView);
-            holder.lapTime.setText(formatLapTime(lapTime, false));
-            holder.accumulatedTime.setText(formatAccumulatedTime(totalTime, false));
-        }
-    }
-
-    /**
-     * Record a new lap and update this adapter to include it.
-     *
-     * @return a newly cleared lap
-     */
-    Lap addLap() {
-        final Lap lap = DataModel.getDataModel().addLap();
-
-        if (getItemCount() == 10) {
-            // 10 total laps indicates all items switch from 1 to 2 digit lap numbers.
-            notifyDataSetChanged();
-        } else {
-            // New current lap now exists.
-            notifyItemInserted(0);
-
-            // Prior current lap must be refreshed once with the true values in place.
-            notifyItemChanged(1);
-        }
-
-        return lap;
-    }
-
-    /**
-     * Remove all recorded laps and update this adapter.
-     */
-    void clearLaps() {
-        // Clear the computed time lengths related to the old recorded laps.
-        mLastFormattedLapTimeLength = 0;
-        mLastFormattedAccumulatedTimeLength = 0;
-
-        notifyDataSetChanged();
-    }
-
-    /**
-     * @return a formatted textual description of lap times and total time
-     */
-    String getShareText() {
-        final Stopwatch stopwatch = getStopwatch();
-        final long totalTime = stopwatch.getTotalTime();
-        final String stopwatchTime = formatTime(totalTime, totalTime, ":");
-
-        // Choose a size for the builder that is unlikely to be resized.
-        final StringBuilder builder = new StringBuilder(1000);
-
-        // Add the total elapsed time of the stopwatch.
-        builder.append(mContext.getString(R.string.sw_share_main, stopwatchTime));
-        builder.append("\n");
-
-        final List<Lap> laps = getLaps();
-        if (!laps.isEmpty()) {
-            // Add a header for lap times.
-            builder.append(mContext.getString(R.string.sw_share_laps));
-            builder.append("\n");
-
-            // Loop through the laps in the order they were recorded; reverse of display order.
-            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(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();
-    }
-
-    /**
-     * @param lapCount the total number of recorded laps
-     * @param lapNumber the number of the lap being formatted
-     * @return e.g. "# 7" if {@code lapCount} less than 10; "# 07" if {@code lapCount} is 10 or more
-     */
-    @VisibleForTesting
-    String formatLapNumber(int lapCount, int lapNumber) {
-        if (lapCount < 10) {
-            return mContext.getString(R.string.lap_number_single_digit, lapNumber);
-        } else {
-            return mContext.getString(R.string.lap_number_double_digit, lapNumber);
-        }
-    }
-
-    /**
-     * @param maxTime the maximum amount of time; used to choose a time format
-     * @param time the time to format guaranteed not to exceed {@code maxTime}
-     * @param separator displayed between hours and minutes as well as minutes and seconds
-     * @return a formatted version of the time
-     */
-    @VisibleForTesting
-    static String formatTime(long maxTime, long time, String separator) {
-        final int hours, minutes, seconds, hundredths;
-        if (time <= 0) {
-            // A negative time should be impossible, but is tolerated to avoid crashing the app.
-            hours = minutes = seconds = hundredths = 0;
-        } else {
-            hours = (int) (time / DateUtils.HOUR_IN_MILLIS);
-            int remainder = (int) (time % DateUtils.HOUR_IN_MILLIS);
-
-            minutes = (int) (remainder / DateUtils.MINUTE_IN_MILLIS);
-            remainder = (int) (remainder % DateUtils.MINUTE_IN_MILLIS);
-
-            seconds = (int) (remainder / DateUtils.SECOND_IN_MILLIS);
-            remainder = (int) (remainder % DateUtils.SECOND_IN_MILLIS);
-
-            hundredths = remainder / 10;
-        }
-
-        final char decimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
-
-        sTimeBuilder.setLength(0);
-
-        // The display of hours and minutes varies based on maxTime.
-        if (maxTime < TEN_MINUTES) {
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 1));
-        } else if (maxTime < HOUR) {
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
-        } else if (maxTime < TEN_HOURS) {
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 1));
-            sTimeBuilder.append(separator);
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
-        } else if (maxTime < HUNDRED_HOURS) {
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 2));
-            sTimeBuilder.append(separator);
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
-        } else {
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 3));
-            sTimeBuilder.append(separator);
-            sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
-        }
-
-        // The display of seconds and hundredths-of-a-second is constant.
-        sTimeBuilder.append(separator);
-        sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(seconds, 2));
-        sTimeBuilder.append(decimalSeparator);
-        sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hundredths, 2));
-
-        return sTimeBuilder.toString();
-    }
-
-    /**
-     * @param lapTime the lap time to be formatted
-     * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
-     *                  set changes; they are not allowed to occur during bind
-     * @return a formatted version of the lap time
-     */
-    private String formatLapTime(long lapTime, boolean isBinding) {
-        // The longest lap dictates the way the given lapTime must be formatted.
-        final long longestLapTime = Math.max(DataModel.getDataModel().getLongestLapTime(), lapTime);
-        final String formattedTime = formatTime(longestLapTime, lapTime, LRM_SPACE);
-
-        // If the newly formatted lap time has altered the format, refresh all laps.
-        final int newLength = formattedTime.length();
-        if (!isBinding && mLastFormattedLapTimeLength != newLength) {
-            mLastFormattedLapTimeLength = newLength;
-            notifyDataSetChanged();
-        }
-
-        return formattedTime;
-    }
-
-    /**
-     * @param accumulatedTime the accumulated time to be formatted
-     * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
-     *                  set changes; they are not allowed to occur during bind
-     * @return a formatted version of the accumulated time
-     */
-    private String formatAccumulatedTime(long accumulatedTime, boolean isBinding) {
-        final long totalTime = getStopwatch().getTotalTime();
-        final long longestAccumulatedTime = Math.max(totalTime, accumulatedTime);
-        final String formattedTime = formatTime(longestAccumulatedTime, accumulatedTime, LRM_SPACE);
-
-        // If the newly formatted accumulated time has altered the format, refresh all laps.
-        final int newLength = formattedTime.length();
-        if (!isBinding && mLastFormattedAccumulatedTimeLength != newLength) {
-            mLastFormattedAccumulatedTimeLength = newLength;
-            notifyDataSetChanged();
-        }
-
-        return formattedTime;
-    }
-
-    private Stopwatch getStopwatch() {
-        return DataModel.getDataModel().getStopwatch();
-    }
-
-    private List<Lap> getLaps() {
-        return DataModel.getDataModel().getLaps();
-    }
-
-    /**
-     * Cache the child views of each lap item view.
-     */
-    static final class LapItemHolder extends RecyclerView.ViewHolder {
-
-        private final TextView lapNumber;
-        private final TextView lapTime;
-        private final TextView accumulatedTime;
-
-        LapItemHolder(View itemView) {
-            super(itemView);
-
-            lapTime = (TextView) itemView.findViewById(R.id.lap_time);
-            lapNumber = (TextView) itemView.findViewById(R.id.lap_number);
-            accumulatedTime = (TextView) itemView.findViewById(R.id.lap_total);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/LapsAdapter.kt b/src/com/android/deskclock/stopwatch/LapsAdapter.kt
new file mode 100644
index 0000000..b9264d4
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/LapsAdapter.kt
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.content.Context
+import android.text.format.DateUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Lap
+import com.android.deskclock.data.Stopwatch
+import com.android.deskclock.stopwatch.LapsAdapter.LapItemHolder
+import com.android.deskclock.uidata.UiDataModel
+
+import java.text.DecimalFormatSymbols
+
+import kotlin.math.max
+
+/**
+ * Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
+ * lap is at the bottom.
+ */
+internal class LapsAdapter(context: Context) : RecyclerView.Adapter<LapItemHolder?>() {
+    private val mInflater: LayoutInflater
+    private val mContext: Context
+
+    /** Used to determine when the time format for the lap time column has changed length.  */
+    private var mLastFormattedLapTimeLength = 0
+
+    /** Used to determine when the time format for the total time column has changed length.  */
+    private var mLastFormattedAccumulatedTimeLength = 0
+
+    init {
+        mContext = context
+        mInflater = LayoutInflater.from(context)
+        setHasStableIds(true)
+    }
+
+    /**
+     * After recording the first lap, there is always a "current lap" in progress.
+     *
+     * @return 0 if no laps are yet recorded; lap count + 1 if any laps exist
+     */
+    override fun getItemCount(): Int {
+        val lapCount = laps.size
+        val currentLapCount = if (lapCount == 0) 0 else 1
+        return currentLapCount + lapCount
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LapItemHolder {
+        val v: View = mInflater.inflate(R.layout.lap_view, parent, false /* attachToRoot */)
+        return LapItemHolder(v)
+    }
+
+    override fun onBindViewHolder(viewHolder: LapItemHolder, position: Int) {
+        val lapTime: Long
+        val lapNumber: Int
+        val totalTime: Long
+
+        // Lap will be null for the current lap.
+        val lap = if (position == 0) null else laps[position - 1]
+        if (lap != null) {
+            // For a recorded lap, merely extract the values to format.
+            lapTime = lap.lapTime
+            lapNumber = lap.lapNumber
+            totalTime = lap.accumulatedTime
+        } else {
+            // For the current lap, compute times relative to the stopwatch.
+            totalTime = stopwatch.totalTime
+            lapTime = DataModel.dataModel.getCurrentLapTime(totalTime)
+            lapNumber = laps.size + 1
+        }
+
+        // Bind data into the child views.
+        viewHolder.lapTime.setText(formatLapTime(lapTime, true))
+        viewHolder.accumulatedTime.setText(formatAccumulatedTime(totalTime, true))
+        viewHolder.lapNumber.setText(formatLapNumber(laps.size + 1, lapNumber))
+    }
+
+    override fun getItemId(position: Int): Long {
+        val laps = laps
+        return if (position == 0) {
+            (laps.size + 1).toLong()
+        } else {
+            laps[position - 1].lapNumber.toLong()
+        }
+    }
+
+    /**
+     * @param rv the RecyclerView that contains the `childView`
+     * @param totalTime time accumulated for the current lap and all prior laps
+     */
+    fun updateCurrentLap(rv: RecyclerView, totalTime: Long) {
+        // If no laps exist there is nothing to do.
+        if (itemCount == 0) {
+            return
+        }
+
+        val currentLapView: View? = rv.getChildAt(0)
+        if (currentLapView != null) {
+            // Compute the lap time using the total time.
+            val lapTime = DataModel.dataModel.getCurrentLapTime(totalTime)
+            val holder = rv.getChildViewHolder(currentLapView) as LapItemHolder
+            holder.lapTime.setText(formatLapTime(lapTime, false))
+            holder.accumulatedTime.setText(formatAccumulatedTime(totalTime, false))
+        }
+    }
+
+    /**
+     * Record a new lap and update this adapter to include it.
+     *
+     * @return a newly cleared lap
+     */
+    fun addLap(): Lap? {
+        val lap = DataModel.dataModel.addLap()
+
+        if (itemCount == 10) {
+            // 10 total laps indicates all items switch from 1 to 2 digit lap numbers.
+            notifyDataSetChanged()
+        } else {
+            // New current lap now exists.
+            notifyItemInserted(0)
+
+            // Prior current lap must be refreshed once with the true values in place.
+            notifyItemChanged(1)
+        }
+
+        return lap
+    }
+
+    /**
+     * Remove all recorded laps and update this adapter.
+     */
+    fun clearLaps() {
+        // Clear the computed time lengths related to the old recorded laps.
+        mLastFormattedLapTimeLength = 0
+        mLastFormattedAccumulatedTimeLength = 0
+
+        notifyDataSetChanged()
+    }
+
+    /**
+     * @return a formatted textual description of lap times and total time
+     */
+    val shareText: String
+        get() {
+            val stopwatch = stopwatch
+            val totalTime = stopwatch.totalTime
+            val stopwatchTime = formatTime(totalTime, totalTime, ":")
+
+            // Choose a size for the builder that is unlikely to be resized.
+            val builder = StringBuilder(1000)
+
+            // Add the total elapsed time of the stopwatch.
+            builder.append(mContext.getString(R.string.sw_share_main, stopwatchTime))
+            builder.append("\n")
+
+            val laps = laps
+            if (laps.isNotEmpty()) {
+                // Add a header for lap times.
+                builder.append(mContext.getString(R.string.sw_share_laps))
+                builder.append("\n")
+
+                // Loop through the laps in the order they were recorded; reverse of display order.
+                val separator = DecimalFormatSymbols.getInstance().decimalSeparator.toString() + " "
+                for (i in laps.indices.reversed()) {
+                    val lap = laps[i]
+                    builder.append(lap.lapNumber)
+                    builder.append(separator)
+                    val lapTime = lap.lapTime
+                    builder.append(formatTime(lapTime, lapTime, " "))
+                    builder.append("\n")
+                }
+
+                // Append the final lap
+                builder.append(laps.size + 1)
+                builder.append(separator)
+                val lapTime = DataModel.dataModel.getCurrentLapTime(totalTime)
+                builder.append(formatTime(lapTime, lapTime, " "))
+                builder.append("\n")
+            }
+            return builder.toString()
+        }
+
+    /**
+     * @param lapCount the total number of recorded laps
+     * @param lapNumber the number of the lap being formatted
+     * @return e.g. "# 7" if `lapCount` less than 10; "# 07" if `lapCount` is 10 or more
+     */
+    @VisibleForTesting
+    fun formatLapNumber(lapCount: Int, lapNumber: Int): String {
+        return if (lapCount < 10) {
+            mContext.getString(R.string.lap_number_single_digit, lapNumber)
+        } else {
+            mContext.getString(R.string.lap_number_double_digit, lapNumber)
+        }
+    }
+
+    /**
+     * @param lapTime the lap time to be formatted
+     * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
+     * set changes; they are not allowed to occur during bind
+     * @return a formatted version of the lap time
+     */
+    private fun formatLapTime(lapTime: Long, isBinding: Boolean): String {
+        // The longest lap dictates the way the given lapTime must be formatted.
+        val longestLapTime = max(DataModel.dataModel.longestLapTime, lapTime)
+        val formattedTime = formatTime(longestLapTime, lapTime, LRM_SPACE)
+
+        // If the newly formatted lap time has altered the format, refresh all laps.
+        val newLength = formattedTime.length
+        if (!isBinding && mLastFormattedLapTimeLength != newLength) {
+            mLastFormattedLapTimeLength = newLength
+            notifyDataSetChanged()
+        }
+
+        return formattedTime
+    }
+
+    /**
+     * @param accumulatedTime the accumulated time to be formatted
+     * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
+     * set changes; they are not allowed to occur during bind
+     * @return a formatted version of the accumulated time
+     */
+    private fun formatAccumulatedTime(accumulatedTime: Long, isBinding: Boolean): String {
+        val totalTime = stopwatch.totalTime
+        val longestAccumulatedTime = max(totalTime, accumulatedTime)
+        val formattedTime = formatTime(longestAccumulatedTime, accumulatedTime, LRM_SPACE)
+
+        // If the newly formatted accumulated time has altered the format, refresh all laps.
+        val newLength = formattedTime.length
+        if (!isBinding && mLastFormattedAccumulatedTimeLength != newLength) {
+            mLastFormattedAccumulatedTimeLength = newLength
+            notifyDataSetChanged()
+        }
+
+        return formattedTime
+    }
+
+    private val stopwatch: Stopwatch
+        get() = DataModel.dataModel.stopwatch
+
+    private val laps: List<Lap>
+        get() = DataModel.dataModel.laps
+
+    /**
+     * Cache the child views of each lap item view.
+     */
+    internal class LapItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val lapNumber: TextView
+        val lapTime: TextView
+        val accumulatedTime: TextView
+
+        init {
+            lapTime = itemView.findViewById(R.id.lap_time) as TextView
+            lapNumber = itemView.findViewById(R.id.lap_number) as TextView
+            accumulatedTime = itemView.findViewById(R.id.lap_total) as TextView
+        }
+    }
+
+    companion object {
+        private val TEN_MINUTES: Long = 10 * DateUtils.MINUTE_IN_MILLIS
+        private val HOUR: Long = DateUtils.HOUR_IN_MILLIS
+        private val TEN_HOURS = 10 * HOUR
+        private val HUNDRED_HOURS = 100 * HOUR
+
+        /** A single space preceded by a zero-width LRM; This groups adjacent chars left-to-right.  */
+        private const val LRM_SPACE = "\u200E "
+
+        /** Reusable StringBuilder that assembles a formatted time; alleviates memory churn.  */
+        private val sTimeBuilder = StringBuilder(12)
+
+        /**
+         * @param maxTime the maximum amount of time; used to choose a time format
+         * @param time the time to format guaranteed not to exceed `maxTime`
+         * @param separator displayed between hours and minutes as well as minutes and seconds
+         * @return a formatted version of the time
+         */
+        @VisibleForTesting
+        fun formatTime(maxTime: Long, time: Long, separator: String?): String {
+            val hours: Int
+            val minutes: Int
+            val seconds: Int
+            val hundredths: Int
+            if (time <= 0) {
+                // A negative time should be impossible, but is tolerated to avoid crashing the app.
+                hundredths = 0
+                seconds = hundredths
+                minutes = seconds
+                hours = minutes
+            } else {
+                hours = (time / DateUtils.HOUR_IN_MILLIS).toInt()
+                var remainder = (time % DateUtils.HOUR_IN_MILLIS).toInt()
+                minutes = (remainder / DateUtils.MINUTE_IN_MILLIS).toInt()
+                remainder = (remainder % DateUtils.MINUTE_IN_MILLIS).toInt()
+                seconds = (remainder / DateUtils.SECOND_IN_MILLIS).toInt()
+                remainder = (remainder % DateUtils.SECOND_IN_MILLIS).toInt()
+                hundredths = remainder / 10
+            }
+
+            val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator
+
+            sTimeBuilder.setLength(0)
+
+            // The display of hours and minutes varies based on maxTime.
+            when {
+                maxTime < TEN_MINUTES -> {
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 1))
+                }
+                maxTime < HOUR -> {
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+                }
+                maxTime < TEN_HOURS -> {
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hours, 1))
+                    sTimeBuilder.append(separator)
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+                }
+                maxTime < HUNDRED_HOURS -> {
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hours, 2))
+                    sTimeBuilder.append(separator)
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+                }
+                else -> {
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hours, 3))
+                    sTimeBuilder.append(separator)
+                    sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+                }
+            }
+
+            // The display of seconds and hundredths-of-a-second is constant.
+            sTimeBuilder.append(separator)
+            sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(seconds, 2))
+            sTimeBuilder.append(decimalSeparator)
+            sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hundredths, 2))
+
+            return sTimeBuilder.toString()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchCircleView.java b/src/com/android/deskclock/stopwatch/StopwatchCircleView.java
deleted file mode 100644
index 31ae20d..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchCircleView.java
+++ /dev/null
@@ -1,177 +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.stopwatch;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.view.View;
-
-import com.android.deskclock.R;
-import com.android.deskclock.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 java.util.List;
-
-/**
- * Custom view that draws a reference lap as a circle when one exists.
- */
-public final class StopwatchCircleView extends View {
-
-    /** The size of the dot indicating the user's position within the reference lap. */
-    private final float mDotRadius;
-
-    /** An amount to subtract from the true radius to account for drawing thicknesses. */
-    private final float mRadiusOffset;
-
-    /** Used to scale the width of the marker to make it similarly visible on all screens. */
-    private final float mScreenDensity;
-
-    /** The color indicating the remaining portion of the current lap. */
-    private final int mRemainderColor;
-
-    /** The color indicating the completed portion of the lap. */
-    private final int mCompletedColor;
-
-    /** The size of the stroke that paints the lap circle. */
-    private final float mStrokeSize;
-
-    /** The size of the stroke that paints the marker for the end of the prior lap. */
-    private final float mMarkerStrokeSize;
-
-    private final Paint mPaint = new Paint();
-    private final Paint mFill = new Paint();
-    private final RectF mArcRect = new RectF();
-
-    @SuppressWarnings("unused")
-    public StopwatchCircleView(Context context) {
-        this(context, null);
-    }
-
-    public StopwatchCircleView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        final Resources resources = context.getResources();
-        final float dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size);
-
-        mDotRadius = dotDiameter / 2f;
-        mScreenDensity = resources.getDisplayMetrics().density;
-        mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
-        mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
-        mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize);
-
-        mRemainderColor = Color.WHITE;
-        mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent);
-
-        mPaint.setAntiAlias(true);
-        mPaint.setStyle(Paint.Style.STROKE);
-
-        mFill.setAntiAlias(true);
-        mFill.setColor(mCompletedColor);
-        mFill.setStyle(Paint.Style.FILL);
-    }
-
-    /**
-     * Start the animation if it is not currently running.
-     */
-    void update() {
-        postInvalidateOnAnimation();
-    }
-
-    @Override
-    public void onDraw(Canvas canvas) {
-        // Compute the size and location of the circle to be drawn.
-        final int xCenter = getWidth() / 2;
-        final int yCenter = getHeight() / 2;
-        final float radius = Math.min(xCenter, yCenter) - mRadiusOffset;
-
-        // Reset old painting state.
-        mPaint.setColor(mRemainderColor);
-        mPaint.setStrokeWidth(mStrokeSize);
-
-        final List<Lap> laps = getLaps();
-
-        // If a reference lap does not exist or should not be drawn, draw a simple white circle.
-        if (laps.isEmpty() || !DataModel.getDataModel().canAddMoreLaps()) {
-            // Draw a complete white circle; no red arc required.
-            canvas.drawCircle(xCenter, yCenter, radius, mPaint);
-
-            // No need to continue animating the plain white circle.
-            return;
-        }
-
-        // The first lap is the reference lap to which all future laps are compared.
-        final Stopwatch stopwatch = getStopwatch();
-        final int lapCount = laps.size();
-        final Lap firstLap = laps.get(lapCount - 1);
-        final Lap priorLap = laps.get(0);
-        final long firstLapTime = firstLap.getLapTime();
-        final long currentLapTime = stopwatch.getTotalTime() - priorLap.getAccumulatedTime();
-
-        // Draw a combination of red and white arcs to create a circle.
-        mArcRect.top = yCenter - radius;
-        mArcRect.bottom = yCenter + radius;
-        mArcRect.left =  xCenter - radius;
-        mArcRect.right = xCenter + radius;
-        final float redPercent = (float) currentLapTime / (float) firstLapTime;
-        final float whitePercent = 1 - (redPercent > 1 ? 1 : redPercent);
-
-        // Draw a white arc to indicate the amount of reference lap that remains.
-        canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, whitePercent * 360, false, mPaint);
-
-        // Draw a red arc to indicate the amount of reference lap completed.
-        mPaint.setColor(mCompletedColor);
-        canvas.drawArc(mArcRect, 270, redPercent * 360 , false, mPaint);
-
-        // Starting on lap 2, a marker can be drawn indicating where the prior lap ended.
-        if (lapCount > 1) {
-            mPaint.setColor(mRemainderColor);
-            mPaint.setStrokeWidth(mMarkerStrokeSize);
-            final float markerAngle = (float) priorLap.getLapTime() / (float) firstLapTime * 360;
-            final float startAngle = 270 + markerAngle;
-            final float sweepAngle = mScreenDensity * (float) (360 / (radius * Math.PI));
-            canvas.drawArc(mArcRect, startAngle, sweepAngle, false, mPaint);
-        }
-
-        // Draw a red dot to indicate current position relative to reference lap.
-        final float dotAngleDegrees = 270 + redPercent * 360;
-        final double dotAngleRadians = Math.toRadians(dotAngleDegrees);
-        final float dotX = xCenter + (float) (radius * Math.cos(dotAngleRadians));
-        final float dotY = yCenter + (float) (radius * Math.sin(dotAngleRadians));
-        canvas.drawCircle(dotX, dotY, mDotRadius, mFill);
-
-        // If the stopwatch is not running it does not require continuous updates.
-        if (stopwatch.isRunning()) {
-            postInvalidateOnAnimation();
-        }
-    }
-
-    private Stopwatch getStopwatch() {
-        return DataModel.getDataModel().getStopwatch();
-    }
-
-    private List<Lap> getLaps() {
-        return DataModel.getDataModel().getLaps();
-    }
-}
diff --git a/src/com/android/deskclock/stopwatch/StopwatchCircleView.kt b/src/com/android/deskclock/stopwatch/StopwatchCircleView.kt
new file mode 100644
index 0000000..a7d8699
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchCircleView.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.view.View
+
+import com.android.deskclock.R
+import com.android.deskclock.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 kotlin.math.cos
+import kotlin.math.min
+import kotlin.math.sin
+
+/**
+ * Custom view that draws a reference lap as a circle when one exists.
+ */
+class StopwatchCircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
+
+    /** The size of the dot indicating the user's position within the reference lap.  */
+    private val mDotRadius: Float
+
+    /** An amount to subtract from the true radius to account for drawing thicknesses.  */
+    private val mRadiusOffset: Float
+
+    /** Used to scale the width of the marker to make it similarly visible on all screens.  */
+    private val mScreenDensity: Float
+
+    /** The color indicating the remaining portion of the current lap.  */
+    private val mRemainderColor: Int
+
+    /** The color indicating the completed portion of the lap.  */
+    private val mCompletedColor: Int
+
+    /** The size of the stroke that paints the lap circle.  */
+    private val mStrokeSize: Float
+
+    /** The size of the stroke that paints the marker for the end of the prior lap.  */
+    private val mMarkerStrokeSize: Float
+
+    private val mPaint: Paint = Paint()
+    private val mFill: Paint = Paint()
+    private val mArcRect: RectF = RectF()
+
+    constructor(context: Context) : this(context, null) {
+    }
+
+    init {
+        val resources: Resources = context.getResources()
+        val dotDiameter: Float = resources.getDimension(R.dimen.circletimer_dot_size)
+
+        mDotRadius = dotDiameter / 2f
+        mScreenDensity = resources.getDisplayMetrics().density
+        mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size)
+        mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size)
+        mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize)
+
+        mRemainderColor = Color.WHITE
+        mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent)
+
+        mPaint.setAntiAlias(true)
+        mPaint.setStyle(Paint.Style.STROKE)
+
+        mFill.setAntiAlias(true)
+        mFill.setColor(mCompletedColor)
+        mFill.setStyle(Paint.Style.FILL)
+    }
+
+    /**
+     * Start the animation if it is not currently running.
+     */
+    fun update() {
+        postInvalidateOnAnimation()
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        // Compute the size and location of the circle to be drawn.
+        val xCenter: Int = getWidth() / 2
+        val yCenter: Int = getHeight() / 2
+        val radius = min(xCenter, yCenter) - mRadiusOffset
+
+        // Reset old painting state.
+        mPaint.setColor(mRemainderColor)
+        mPaint.setStrokeWidth(mStrokeSize)
+        val laps = laps
+
+        // If a reference lap does not exist or should not be drawn, draw a simple white circle.
+        if (laps.isEmpty() || !DataModel.dataModel.canAddMoreLaps()) {
+            // Draw a complete white circle; no red arc required.
+            canvas.drawCircle(xCenter.toFloat(), yCenter.toFloat(), radius, mPaint)
+
+            // No need to continue animating the plain white circle.
+            return
+        }
+
+        // The first lap is the reference lap to which all future laps are compared.
+        val stopwatch = stopwatch
+        val lapCount = laps.size
+        val firstLap = laps[lapCount - 1]
+        val priorLap = laps[0]
+        val firstLapTime = firstLap.lapTime
+        val currentLapTime = stopwatch.totalTime - priorLap.accumulatedTime
+
+        // Draw a combination of red and white arcs to create a circle.
+        mArcRect.top = yCenter - radius
+        mArcRect.bottom = yCenter + radius
+        mArcRect.left = xCenter - radius
+        mArcRect.right = xCenter + radius
+        val redPercent = currentLapTime.toFloat() / firstLapTime.toFloat()
+        val whitePercent: Float = 1f - if (redPercent > 1) 1f else redPercent
+
+        // Draw a white arc to indicate the amount of reference lap that remains.
+        canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, whitePercent * 360, false, mPaint)
+
+        // Draw a red arc to indicate the amount of reference lap completed.
+        mPaint.setColor(mCompletedColor)
+        canvas.drawArc(mArcRect, 270f, redPercent * 360, false, mPaint)
+
+        // Starting on lap 2, a marker can be drawn indicating where the prior lap ended.
+        if (lapCount > 1) {
+            mPaint.setColor(mRemainderColor)
+            mPaint.setStrokeWidth(mMarkerStrokeSize)
+            val markerAngle = priorLap.lapTime.toFloat() / firstLapTime.toFloat() * 360
+            val startAngle = 270 + markerAngle
+            val sweepAngle = mScreenDensity * (360 / (radius * Math.PI)).toFloat()
+            canvas.drawArc(mArcRect, startAngle, sweepAngle, false, mPaint)
+        }
+
+        // Draw a red dot to indicate current position relative to reference lap.
+        val dotAngleDegrees = 270 + redPercent * 360
+        val dotAngleRadians = Math.toRadians(dotAngleDegrees.toDouble())
+        val dotX = xCenter + (radius * cos(dotAngleRadians)).toFloat()
+        val dotY = yCenter + (radius * sin(dotAngleRadians)).toFloat()
+        canvas.drawCircle(dotX, dotY, mDotRadius, mFill)
+
+        // If the stopwatch is not running it does not require continuous updates.
+        if (stopwatch.isRunning) {
+            postInvalidateOnAnimation()
+        }
+    }
+
+    private val stopwatch: Stopwatch
+        get() = DataModel.dataModel.stopwatch
+
+    private val laps: List<Lap>
+        get() = DataModel.dataModel.laps
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchFragment.java b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
deleted file mode 100644
index fe91b37..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchFragment.java
+++ /dev/null
@@ -1,740 +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.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.GradientDrawable;
-import android.os.Bundle;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.core.graphics.ColorUtils;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.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.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.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.uidata.UiDataModel.Tab.STOPWATCH;
-
-/**
- * Fragment that shows the stopwatch and recorded laps.
- */
-public final class StopwatchFragment extends DeskClockFragment {
-
-    /** 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();
-
-    /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */
-    private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
-
-    /** Updates the user interface in response to stopwatch changes. */
-    private final StopwatchListener mStopwatchWatcher = new StopwatchWatcher();
-
-    /** Draws a gradient over the bottom of the {@link #mLapsList} to reduce clash with the fab. */
-    private GradientItemDecoration mGradientItemDecoration;
-
-    /** The data source for {@link #mLapsList}. */
-    private LapsAdapter mLapsAdapter;
-
-    /** The layout manager for the {@link #mLapsAdapter}. */
-    private LinearLayoutManager mLapsLayoutManager;
-
-    /** 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 (seconds and above only). */
-    private TextView mMainTimeText;
-
-    /** 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() {
-        super(STOPWATCH);
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
-        mLapsAdapter = new LapsAdapter(getActivity());
-        mLapsLayoutManager = new LinearLayoutManager(getActivity());
-        mGradientItemDecoration = new GradientItemDecoration(getActivity());
-
-        final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false);
-        mTime = (StopwatchCircleView) v.findViewById(R.id.stopwatch_circle);
-        mLapsList = (RecyclerView) v.findViewById(R.id.laps_list);
-        ((SimpleItemAnimator) mLapsList.getItemAnimator()).setSupportsChangeAnimations(false);
-        mLapsList.setLayoutManager(mLapsLayoutManager);
-        mLapsList.addItemDecoration(mGradientItemDecoration);
-
-        // In landscape layouts, the laps list can reach the top of the screen and thus can cause
-        // a drop shadow to appear. The same is not true for portrait landscapes.
-        if (Utils.isLandscape(getActivity())) {
-            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.
-        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;
-    }
-
-    @Override
-    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.
-        mLapsAdapter.notifyDataSetChanged();
-
-        // Synchronize the user interface with the data model.
-        updateUI(FAB_AND_BUTTONS_IMMEDIATE);
-
-        // Start watching for page changes away from this fragment.
-        UiDataModel.getUiDataModel().addTabListener(mTabWatcher);
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-
-        // Stop all updates while the fragment is not visible.
-        stopUpdatingTime();
-
-        // Stop watching for page changes away from this fragment.
-        UiDataModel.getUiDataModel().removeTabListener(mTabWatcher);
-
-        // Release the wake lock if it is currently held.
-        releaseWakeLock();
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-
-        DataModel.getDataModel().removeStopwatchListener(mStopwatchWatcher);
-    }
-
-    @Override
-    public void onFabClick(@NonNull ImageView fab) {
-        toggleStopwatchState();
-    }
-
-    @Override
-    public void onLeftButtonClick(@NonNull Button left) {
-        doReset();
-    }
-
-    @Override
-    public void onRightButtonClick(@NonNull Button right) {
-        switch (getStopwatch().getState()) {
-            case RUNNING:
-                doAddLap();
-                break;
-            case PAUSED:
-                doShare();
-                break;
-        }
-    }
-
-    private void updateFab(@NonNull ImageView fab, boolean animate) {
-        if (getStopwatch().isRunning()) {
-            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 {
-            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);
-    }
-
-    public void onUpdateFab(@NonNull ImageView fab) {
-        updateFab(fab, false);
-    }
-
-    @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) {
-        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.setVisibility(INVISIBLE);
-                right.setClickable(true);
-                right.setVisibility(INVISIBLE);
-                break;
-            case RUNNING:
-                left.setVisibility(VISIBLE);
-                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;
-        }
-    }
-
-    /**
-     * @param color the newly installed app window color
-     */
-    protected void onAppColorChanged(@ColorInt int color) {
-        if (mGradientItemDecoration != null) {
-            mGradientItemDecoration.updateGradientColors(color);
-        }
-        if (mLapsList != null) {
-            mLapsList.invalidateItemDecorations();
-        }
-    }
-
-    /**
-     * Start the stopwatch.
-     */
-    private void doStart() {
-        Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock);
-        DataModel.getDataModel().startStopwatch();
-    }
-
-    /**
-     * Pause the stopwatch.
-     */
-    private void doPause() {
-        Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock);
-        DataModel.getDataModel().pauseStopwatch();
-    }
-
-    /**
-     * 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();
-
-        @SuppressLint("InlinedApi")
-        @SuppressWarnings("deprecation")
-        final Intent shareIntent = new Intent(Intent.ACTION_SEND)
-                .addFlags(Utils.isLOrLater() ? Intent.FLAG_ACTIVITY_NEW_DOCUMENT
-                        : Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
-                .putExtra(Intent.EXTRA_SUBJECT, subject)
-                .putExtra(Intent.EXTRA_TEXT, text)
-                .setType("text/plain");
-
-        final Context context = getActivity();
-        final String title = context.getString(R.string.sw_share_button);
-        final Intent shareChooserIntent = Intent.createChooser(shareIntent, title);
-        try {
-            context.startActivity(shareChooserIntent);
-        } catch (ActivityNotFoundException anfe) {
-            LogUtils.e("Cannot share lap data because no suitable receiving Activity exists");
-            updateFab(BUTTONS_IMMEDIATE);
-        }
-    }
-
-    /**
-     * Record and add a new lap ending now.
-     */
-    private void doAddLap() {
-        Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock);
-
-        // Record a new lap.
-        final Lap lap = mLapsAdapter.addLap();
-        if (lap == null) {
-            return;
-        }
-
-        // Update button states.
-        updateFab(BUTTONS_IMMEDIATE);
-
-        if (lap.getLapNumber() == 1) {
-            // Child views from prior lap sets hang around and blit to the screen when adding the
-            // first lap of the subsequent lap set. Remove those superfluous children here manually
-            // to ensure they aren't seen as the first lap is drawn.
-            mLapsList.removeAllViewsInLayout();
-
-            if (mTime != null) {
-                // Start animating the reference lap.
-                mTime.update();
-            }
-
-            // Recording the first lap transitions the UI to display the laps list.
-            showOrHideLaps(false);
-        }
-
-        // Ensure the newly added lap is visible on screen.
-        mLapsList.scrollToPosition(0);
-    }
-
-    /**
-     * Show or hide the list of laps.
-     */
-    private void showOrHideLaps(boolean clearLaps) {
-        final ViewGroup sceneRoot = (ViewGroup) getView();
-        if (sceneRoot == null) {
-            return;
-        }
-
-        TransitionManager.beginDelayedTransition(sceneRoot);
-
-        if (clearLaps) {
-            mLapsAdapter.clearLaps();
-        }
-
-        final boolean lapsVisible = mLapsAdapter.getItemCount() > 0;
-        mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE);
-
-        if (Utils.isPortrait(getActivity())) {
-            // When the lap list is visible, it includes the bottom padding. When it is absent the
-            // appropriate bottom padding must be applied to the container.
-            final Resources res = getResources();
-            final int bottom = lapsVisible ? 0 : res.getDimensionPixelSize(R.dimen.fab_height);
-            final int top = sceneRoot.getPaddingTop();
-            final int left = sceneRoot.getPaddingLeft();
-            final int right = sceneRoot.getPaddingRight();
-            sceneRoot.setPadding(left, top, right, bottom);
-        }
-    }
-
-    private void adjustWakeLock() {
-        final boolean appInForeground = DataModel.getDataModel().isApplicationInForeground();
-        if (getStopwatch().isRunning() && isTabSelected() && appInForeground) {
-            getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-        } else {
-            releaseWakeLock();
-        }
-    }
-
-    private void releaseWakeLock() {
-        getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-    }
-
-    /**
-     * Either pause or start the stopwatch based on its current state.
-     */
-    private void toggleStopwatchState() {
-        if (getStopwatch().isRunning()) {
-            doPause();
-        } else {
-            doStart();
-        }
-    }
-
-    private Stopwatch getStopwatch() {
-        return DataModel.getDataModel().getStopwatch();
-    }
-
-    private boolean canRecordMoreLaps() {
-        return DataModel.getDataModel().canAddMoreLaps();
-    }
-
-    /**
-     * Post the first runnable to update times within the UI. It will reschedule itself as needed.
-     */
-    private void startUpdatingTime() {
-        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
-        stopUpdatingTime();
-        mMainTimeText.post(mTimeUpdateRunnable);
-    }
-
-    /**
-     * Remove the runnable that updates times within the UI.
-     */
-    private void stopUpdatingTime() {
-        mMainTimeText.removeCallbacks(mTimeUpdateRunnable);
-    }
-
-    /**
-     * Update all time displays based on a single snapshot of the stopwatch progress. This includes
-     * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in
-     * the list of laps.
-     */
-    private void updateTime() {
-        // Compute the total time of the stopwatch.
-        final Stopwatch stopwatch = getStopwatch();
-        final long totalTime = stopwatch.getTotalTime();
-        mStopwatchTextController.setTimeString(totalTime);
-
-        // Update the current lap.
-        final boolean currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0;
-        if (!stopwatch.isReset() && currentLapIsVisible) {
-            mLapsAdapter.updateCurrentLap(mLapsList, totalTime);
-        }
-    }
-
-    /**
-     * Synchronize the UI state with the model data.
-     */
-    private void updateUI(@UpdateFabFlag int updateTypes) {
-        adjustWakeLock();
-
-        // Draw the latest stopwatch and current lap times.
-        updateTime();
-
-        if (mTime != null) {
-            mTime.update();
-        }
-
-        final Stopwatch stopwatch = getStopwatch();
-        if (!stopwatch.isReset()) {
-            startUpdatingTime();
-        }
-
-        // Adjust the visibility of the list of laps.
-        showOrHideLaps(stopwatch.isReset());
-
-        // Update button states.
-        updateFab(updateTypes);
-    }
-
-    /**
-     * This runnable periodically updates times throughout the UI. It stops these updates when the
-     * stopwatch is no longer running.
-     */
-    private final class TimeUpdateRunnable implements Runnable {
-        @Override
-        public void run() {
-            final long startTime = Utils.now();
-
-            updateTime();
-
-            // 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();
-
-            if (blink) {
-                mMainTimeText.setAlpha(0f);
-                mHundredthsTimeText.setAlpha(0f);
-            } else {
-                mMainTimeText.setAlpha(1f);
-                mHundredthsTimeText.setAlpha(1f);
-            }
-
-            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);
-            }
-        }
-    }
-
-    /**
-     * Acquire or release the wake lock based on the tab state.
-     */
-    private final class TabWatcher implements TabListener {
-        @Override
-        public void selectedTabChanged(Tab oldSelectedTab, Tab newSelectedTab) {
-            adjustWakeLock();
-        }
-    }
-
-    /**
-     * Update the user interface in response to a stopwatch change.
-     */
-    private class StopwatchWatcher implements StopwatchListener {
-        @Override
-        public void stopwatchUpdated(Stopwatch before, Stopwatch after) {
-            if (after.isReset()) {
-                // 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_MORPH | BUTTONS_IMMEDIATE);
-            }
-        }
-
-        @Override
-        public void lapAdded(Lap lap) {
-        }
-    }
-
-    /**
-     * 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.
-     */
-    private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
-            implements View.OnLayoutChangeListener {
-        @Override
-        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-            setTabScrolledToTop(Utils.isScrolledToTop(mLapsList));
-        }
-
-        @Override
-        public void onLayoutChange(View v, int left, int top, int right, int bottom,
-                int oldLeft, int oldTop, int oldRight, int oldBottom) {
-            setTabScrolledToTop(Utils.isScrolledToTop(mLapsList));
-        }
-    }
-
-    /**
-     * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the
-     * contrast between floating buttons and the laps list content.
-     */
-    private static final class GradientItemDecoration extends RecyclerView.ItemDecoration {
-
-        //  0% -  25% of gradient length -> opacity changes from 0% to 50%
-        // 25% -  90% of gradient length -> opacity changes from 50% to 100%
-        // 90% - 100% of gradient length -> opacity remains at 100%
-        private static final int[] ALPHAS = {
-                0x00, // 0%
-                0x1A, // 10%
-                0x33, // 20%
-                0x4D, // 30%
-                0x66, // 40%
-                0x80, // 50%
-                0x89, // 53.8%
-                0x93, // 57.6%
-                0x9D, // 61.5%
-                0xA7, // 65.3%
-                0xB1, // 69.2%
-                0xBA, // 73.0%
-                0xC4, // 76.9%
-                0xCE, // 80.7%
-                0xD8, // 84.6%
-                0xE2, // 88.4%
-                0xEB, // 92.3%
-                0xF5, // 96.1%
-                0xFF, // 100%
-                0xFF, // 100%
-                0xFF, // 100%
-        };
-
-        /**
-         * A reusable array of control point colors that define the gradient. It is based on the
-         * background color of the window and thus recomputed each time that color is changed.
-         */
-        private final int[] mGradientColors = new int[ALPHAS.length];
-
-        /** The drawable that produces the tinting gradient effect of this decoration. */
-        private final GradientDrawable mGradient = new GradientDrawable();
-
-        /** The height of the gradient; sized relative to the fab height. */
-        private final int mGradientHeight;
-
-        GradientItemDecoration(Context context) {
-            mGradient.setOrientation(TOP_BOTTOM);
-            updateGradientColors(ThemeUtils.resolveColor(context, android.R.attr.windowBackground));
-
-            final Resources resources = context.getResources();
-            final float fabHeight = resources.getDimensionPixelSize(R.dimen.fab_height);
-            mGradientHeight = Math.round(fabHeight * 1.2f);
-        }
-
-        @Override
-        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
-            super.onDrawOver(c, parent, state);
-
-            final int w = parent.getWidth();
-            final int h = parent.getHeight();
-
-            mGradient.setBounds(0, h - mGradientHeight, w, h);
-            mGradient.draw(c);
-        }
-
-        /**
-         * Given a {@code baseColor}, compute a gradient of tinted colors that define the fade
-         * effect to apply to the bottom of the lap list.
-         *
-         * @param baseColor a base color to which the gradient tint should be applied
-         */
-        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]);
-            }
-
-            // Set the gradient colors into the drawable.
-            mGradient.setColors(mGradientColors);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/stopwatch/StopwatchFragment.kt b/src/com/android/deskclock/stopwatch/StopwatchFragment.kt
new file mode 100644
index 0000000..02a7259
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchFragment.kt
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.R.attr.state_activated
+import android.R.attr.state_pressed
+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.GradientDrawable
+import android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM
+import android.os.Bundle
+import android.transition.TransitionManager
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.core.graphics.ColorUtils
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.SimpleItemAnimator
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.DeskClockFragment
+import com.android.deskclock.FabContainer
+import com.android.deskclock.FabContainer.UpdateFabFlag
+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.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.uidata.TabListener
+import com.android.deskclock.uidata.UiDataModel
+
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+import kotlin.math.roundToInt
+
+/**
+ * Fragment that shows the stopwatch and recorded laps.
+ */
+class StopwatchFragment : DeskClockFragment(UiDataModel.Tab.STOPWATCH) {
+
+    /** Keep the screen on when this tab is selected.  */
+    private val mTabWatcher: TabListener = TabWatcher()
+
+    /** Scheduled to update the stopwatch time and current lap time while stopwatch is running.  */
+    private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
+
+    /** Updates the user interface in response to stopwatch changes.  */
+    private val mStopwatchWatcher: StopwatchListener = StopwatchWatcher()
+
+    /** Draws a gradient over the bottom of the [.mLapsList] to reduce clash with the fab.  */
+    private var mGradientItemDecoration: GradientItemDecoration? = null
+
+    /** The data source for [.mLapsList].  */
+    private lateinit var mLapsAdapter: LapsAdapter
+
+    /** The layout manager for the [.mLapsAdapter].  */
+    private lateinit var mLapsLayoutManager: LinearLayoutManager
+
+    /** Draws the reference lap while the stopwatch is running.  */
+    private var mTime: StopwatchCircleView? = null
+
+    /** The View containing both TextViews of the stopwatch.  */
+    private lateinit var mStopwatchWrapper: View
+
+    /** Displays the recorded lap times.  */
+    private lateinit var mLapsList: RecyclerView
+
+    /** Displays the current stopwatch time (seconds and above only).  */
+    private lateinit var mMainTimeText: TextView
+
+    /** Displays the current stopwatch time (hundredths only).  */
+    private lateinit var mHundredthsTimeText: TextView
+
+    /** Formats and displays the text in the stopwatch.  */
+    private lateinit var mStopwatchTextController: StopwatchTextController
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        state: Bundle?
+    ): View {
+        mLapsAdapter = LapsAdapter(requireActivity())
+        mLapsLayoutManager = LinearLayoutManager(requireActivity())
+        mGradientItemDecoration = GradientItemDecoration(requireActivity())
+
+        val v: View = inflater.inflate(R.layout.stopwatch_fragment, container, false)
+        mTime = v.findViewById(R.id.stopwatch_circle)
+        mLapsList = v.findViewById(R.id.laps_list) as RecyclerView
+        (mLapsList.getItemAnimator() as SimpleItemAnimator).setSupportsChangeAnimations(false)
+        mLapsList.setLayoutManager(mLapsLayoutManager)
+        mLapsList.addItemDecoration(mGradientItemDecoration!!)
+
+        // In landscape layouts, the laps list can reach the top of the screen and thus can cause
+        // a drop shadow to appear. The same is not true for portrait landscapes.
+        if (Utils.isLandscape(requireActivity())) {
+            val scrollPositionWatcher = ScrollPositionWatcher()
+            mLapsList.addOnLayoutChangeListener(scrollPositionWatcher)
+            mLapsList.addOnScrollListener(scrollPositionWatcher)
+        } else {
+            setTabScrolledToTop(true)
+        }
+        mLapsList.setAdapter(mLapsAdapter)
+
+        // Timer text serves as a virtual start/stop button.
+        mMainTimeText = v.findViewById(R.id.stopwatch_time_text) as TextView
+        mHundredthsTimeText = v.findViewById(R.id.stopwatch_hundredths_text) as TextView
+        mStopwatchTextController = StopwatchTextController(mMainTimeText, mHundredthsTimeText)
+        mStopwatchWrapper = v.findViewById(R.id.stopwatch_time_wrapper)
+
+        DataModel.dataModel.addStopwatchListener(mStopwatchWatcher)
+
+        mStopwatchWrapper.setOnClickListener(TimeClickListener())
+        if (mTime != null) {
+            mStopwatchWrapper.setOnTouchListener(CircleTouchListener())
+        }
+
+        val c: Context = mMainTimeText.getContext()
+        val colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent)
+        val textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary)
+        val timeTextColor =
+                ColorStateList(
+                        arrayOf(intArrayOf(-state_activated, -state_pressed), intArrayOf()),
+                        intArrayOf(textColorPrimary, colorAccent)
+                )
+        mMainTimeText.setTextColor(timeTextColor)
+        mHundredthsTimeText.setTextColor(timeTextColor)
+
+        return v
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        val activity: Activity = requireActivity()
+        val intent: Intent? = activity.getIntent()
+        if (intent != null) {
+            val action: String? = intent.getAction()
+            if (StopwatchService.Companion.ACTION_START_STOPWATCH == action) {
+                DataModel.dataModel.startStopwatch()
+                // Consume the intent
+                activity.setIntent(null)
+            } else if (StopwatchService.Companion.ACTION_PAUSE_STOPWATCH == action) {
+                DataModel.dataModel.pauseStopwatch()
+                // Consume the intent
+                activity.setIntent(null)
+            }
+        }
+
+        // Conservatively assume the data in the adapter has changed while the fragment was paused.
+        mLapsAdapter.notifyDataSetChanged()
+
+        // Synchronize the user interface with the data model.
+        updateUI(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+        // Start watching for page changes away from this fragment.
+        UiDataModel.uiDataModel.addTabListener(mTabWatcher)
+    }
+
+    override fun onStop() {
+        super.onStop()
+
+        // Stop all updates while the fragment is not visible.
+        stopUpdatingTime()
+
+        // Stop watching for page changes away from this fragment.
+        UiDataModel.uiDataModel.removeTabListener(mTabWatcher)
+
+        // Release the wake lock if it is currently held.
+        releaseWakeLock()
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+
+        DataModel.dataModel.removeStopwatchListener(mStopwatchWatcher)
+    }
+
+    override fun onFabClick(fab: ImageView) {
+        toggleStopwatchState()
+    }
+
+    override fun onLeftButtonClick(left: Button) {
+        doReset()
+    }
+
+    override fun onRightButtonClick(right: Button) {
+        when (stopwatch.state) {
+            Stopwatch.State.RUNNING -> doAddLap()
+            Stopwatch.State.PAUSED -> doShare()
+            Stopwatch.State.RESET -> {
+            }
+            null -> {
+            }
+        }
+    }
+
+    private fun updateFab(fab: ImageView, animate: Boolean) {
+        if (stopwatch.isRunning) {
+            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 {
+            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 fun onUpdateFab(fab: ImageView) {
+        updateFab(fab, false)
+    }
+
+    override fun onMorphFab(fab: ImageView) {
+        // Update the fab's drawable to match the current timer state.
+        updateFab(fab, Utils.isNOrLater)
+        // Animate the drawable.
+        AnimatorUtils.startDrawableAnimation(fab)
+    }
+
+    override fun onUpdateFabButtons(left: Button, right: Button) {
+        val resources: Resources = getResources()
+        left.setClickable(true)
+        left.setText(R.string.sw_reset_button)
+        left.setContentDescription(resources.getString(R.string.sw_reset_button))
+
+        when (stopwatch.state) {
+            Stopwatch.State.RESET -> {
+                left.setVisibility(INVISIBLE)
+                right.setClickable(true)
+                right.setVisibility(INVISIBLE)
+            }
+            Stopwatch.State.RUNNING -> {
+                left.setVisibility(VISIBLE)
+                val canRecordLaps = canRecordMoreLaps()
+                right.setText(R.string.sw_lap_button)
+                right.setContentDescription(resources.getString(R.string.sw_lap_button))
+                right.setClickable(canRecordLaps)
+                right.setVisibility(if (canRecordLaps) VISIBLE else INVISIBLE)
+            }
+            Stopwatch.State.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))
+            }
+            null -> {
+            }
+        }
+    }
+
+    /**
+     * @param color the newly installed app window color
+     */
+    override fun onAppColorChanged(@ColorInt color: Int) {
+        mGradientItemDecoration?.updateGradientColors(color)
+        mLapsList.invalidateItemDecorations()
+    }
+
+    /**
+     * Start the stopwatch.
+     */
+    private fun doStart() {
+        Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock)
+        DataModel.dataModel.startStopwatch()
+    }
+
+    /**
+     * Pause the stopwatch.
+     */
+    private fun doPause() {
+        Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock)
+        DataModel.dataModel.pauseStopwatch()
+    }
+
+    /**
+     * Reset the stopwatch.
+     */
+    private fun doReset() {
+        val priorState = stopwatch.state
+        Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock)
+        DataModel.dataModel.resetStopwatch()
+        mMainTimeText.setAlpha(1f)
+        mHundredthsTimeText.setAlpha(1f)
+        if (priorState == Stopwatch.State.RUNNING) {
+            updateFab(FabContainer.FAB_MORPH)
+        }
+    }
+
+    /**
+     * Send stopwatch time and lap times to an external sharing application.
+     */
+    private fun doShare() {
+        // Disable the fab buttons to avoid double-taps on the share button.
+        updateFab(FabContainer.BUTTONS_DISABLE)
+
+        val subjects: Array<String> = getResources().getStringArray(R.array.sw_share_strings)
+        val subject = subjects[(Math.random() * subjects.size).toInt()]
+        val text = mLapsAdapter.shareText
+
+        @SuppressLint("InlinedApi")
+        val shareIntent: Intent = Intent(Intent.ACTION_SEND)
+                .addFlags(if (Utils.isLOrLater) {
+                    Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+                } else {
+                    Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
+                })
+                .putExtra(Intent.EXTRA_SUBJECT, subject)
+                .putExtra(Intent.EXTRA_TEXT, text)
+                .setType("text/plain")
+
+        val context: Context = requireActivity()
+        val title: String = context.getString(R.string.sw_share_button)
+        val shareChooserIntent: Intent = Intent.createChooser(shareIntent, title)
+        try {
+            context.startActivity(shareChooserIntent)
+        } catch (anfe: ActivityNotFoundException) {
+            LogUtils.e("Cannot share lap data because no suitable receiving Activity exists")
+            updateFab(FabContainer.BUTTONS_IMMEDIATE)
+        }
+    }
+
+    /**
+     * Record and add a new lap ending now.
+     */
+    private fun doAddLap() {
+        Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock)
+
+        // Record a new lap.
+        val lap = mLapsAdapter.addLap() ?: return
+
+        // Update button states.
+        updateFab(FabContainer.BUTTONS_IMMEDIATE)
+        if (lap.lapNumber == 1) {
+            // Child views from prior lap sets hang around and blit to the screen when adding the
+            // first lap of the subsequent lap set. Remove those superfluous children here manually
+            // to ensure they aren't seen as the first lap is drawn.
+            mLapsList.removeAllViewsInLayout()
+            if (mTime != null) {
+                // Start animating the reference lap.
+                mTime!!.update()
+            }
+
+            // Recording the first lap transitions the UI to display the laps list.
+            showOrHideLaps(false)
+        }
+
+        // Ensure the newly added lap is visible on screen.
+        mLapsList.scrollToPosition(0)
+    }
+
+    /**
+     * Show or hide the list of laps.
+     */
+    private fun showOrHideLaps(clearLaps: Boolean) {
+        val sceneRoot: ViewGroup = getView() as ViewGroup? ?: return
+
+        TransitionManager.beginDelayedTransition(sceneRoot)
+
+        if (clearLaps) {
+            mLapsAdapter.clearLaps()
+        }
+
+        val lapsVisible = mLapsAdapter.getItemCount() > 0
+        mLapsList.setVisibility(if (lapsVisible) VISIBLE else GONE)
+
+        if (Utils.isPortrait(requireActivity())) {
+            // When the lap list is visible, it includes the bottom padding. When it is absent the
+            // appropriate bottom padding must be applied to the container.
+            val res: Resources = getResources()
+            val bottom = if (lapsVisible) 0 else res.getDimensionPixelSize(R.dimen.fab_height)
+            val top: Int = sceneRoot.getPaddingTop()
+            val left: Int = sceneRoot.getPaddingLeft()
+            val right: Int = sceneRoot.getPaddingRight()
+            sceneRoot.setPadding(left, top, right, bottom)
+        }
+    }
+
+    private fun adjustWakeLock() {
+        val appInForeground = DataModel.dataModel.isApplicationInForeground
+        if (stopwatch.isRunning && isTabSelected && appInForeground) {
+            requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        } else {
+            releaseWakeLock()
+        }
+    }
+
+    private fun releaseWakeLock() {
+        requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+    }
+
+    /**
+     * Either pause or start the stopwatch based on its current state.
+     */
+    private fun toggleStopwatchState() {
+        if (stopwatch.isRunning) {
+            doPause()
+        } else {
+            doStart()
+        }
+    }
+
+    private val stopwatch: Stopwatch
+        get() = DataModel.dataModel.stopwatch
+
+    private fun canRecordMoreLaps(): Boolean = DataModel.dataModel.canAddMoreLaps()
+
+    /**
+     * Post the first runnable to update times within the UI. It will reschedule itself as needed.
+     */
+    private fun startUpdatingTime() {
+        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
+        stopUpdatingTime()
+        mMainTimeText.post(mTimeUpdateRunnable)
+    }
+
+    /**
+     * Remove the runnable that updates times within the UI.
+     */
+    private fun stopUpdatingTime() {
+        mMainTimeText.removeCallbacks(mTimeUpdateRunnable)
+    }
+
+    /**
+     * Update all time displays based on a single snapshot of the stopwatch progress. This includes
+     * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in
+     * the list of laps.
+     */
+    private fun updateTime() {
+        // Compute the total time of the stopwatch.
+        val stopwatch = stopwatch
+        val totalTime = stopwatch.totalTime
+        mStopwatchTextController.setTimeString(totalTime)
+
+        // Update the current lap.
+        val currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0
+        if (!stopwatch.isReset && currentLapIsVisible) {
+            mLapsAdapter.updateCurrentLap(mLapsList, totalTime)
+        }
+    }
+
+    /**
+     * Synchronize the UI state with the model data.
+     */
+    private fun updateUI(@UpdateFabFlag updateTypes: Int) {
+        adjustWakeLock()
+
+        // Draw the latest stopwatch and current lap times.
+        updateTime()
+        if (mTime != null) {
+            mTime!!.update()
+        }
+        val stopwatch = stopwatch
+        if (!stopwatch.isReset) {
+            startUpdatingTime()
+        }
+
+        // Adjust the visibility of the list of laps.
+        showOrHideLaps(stopwatch.isReset)
+
+        // Update button states.
+        updateFab(updateTypes)
+    }
+
+    /**
+     * This runnable periodically updates times throughout the UI. It stops these updates when the
+     * stopwatch is no longer running.
+     */
+    private inner class TimeUpdateRunnable : Runnable {
+        override fun run() {
+            val startTime = Utils.now()
+            updateTime()
+
+            // Blink text iff the stopwatch is paused and not pressed.
+            val touchTarget: View = if (mTime != null) mTime!! else mStopwatchWrapper
+            val stopwatch = stopwatch
+            val blink = (stopwatch.isPaused && startTime % 1000 < 500 && !touchTarget.isPressed())
+
+            if (blink) {
+                mMainTimeText.setAlpha(0f)
+                mHundredthsTimeText.setAlpha(0f)
+            } else {
+                mMainTimeText.setAlpha(1f)
+                mHundredthsTimeText.setAlpha(1f)
+            }
+
+            if (!stopwatch.isReset) {
+                val period = (if (stopwatch.isPaused) {
+                    REDRAW_PERIOD_PAUSED
+                } else {
+                    REDRAW_PERIOD_RUNNING
+                }).toLong()
+                val endTime = Utils.now()
+                val delay: Long = max(0, startTime + period - endTime).toLong()
+                mMainTimeText.postDelayed(this, delay)
+            }
+        }
+    }
+
+    /**
+     * Acquire or release the wake lock based on the tab state.
+     */
+    private inner class TabWatcher : TabListener {
+        override fun selectedTabChanged(
+            oldSelectedTab: UiDataModel.Tab,
+            newSelectedTab: UiDataModel.Tab
+        ) {
+            adjustWakeLock()
+        }
+    }
+
+    /**
+     * Update the user interface in response to a stopwatch change.
+     */
+    private inner class StopwatchWatcher : StopwatchListener {
+        override fun stopwatchUpdated(before: Stopwatch, after: Stopwatch) {
+            if (after.isReset) {
+                // Ensure the drop shadow is hidden when the stopwatch is reset.
+                setTabScrolledToTop(true)
+                if (DataModel.dataModel.isApplicationInForeground) {
+                    updateUI(FabContainer.BUTTONS_IMMEDIATE)
+                }
+                return
+            }
+            if (DataModel.dataModel.isApplicationInForeground) {
+                updateUI(FabContainer.FAB_MORPH or FabContainer.BUTTONS_IMMEDIATE)
+            }
+        }
+
+        override fun lapAdded(lap: Lap) {
+        }
+    }
+
+    /**
+     * Toggles stopwatch state when user taps stopwatch.
+     */
+    private inner class TimeClickListener : View.OnClickListener {
+
+        override fun onClick(view: View?) {
+            if (stopwatch.isRunning) {
+                DataModel.dataModel.pauseStopwatch()
+            } else {
+                DataModel.dataModel.startStopwatch()
+            }
+        }
+    }
+
+    /**
+     * Checks if the user is pressing inside of the stopwatch circle.
+     */
+    private inner class CircleTouchListener : View.OnTouchListener {
+
+        override fun onTouch(view: View, event: MotionEvent): Boolean {
+            val actionMasked: Int = event.getActionMasked()
+            if (actionMasked != MotionEvent.ACTION_DOWN) {
+                return false
+            }
+            val rX: Float = view.getWidth() / 2f
+            val rY: Float = (view.getHeight() - view.getPaddingBottom()) / 2f
+            val r = min(rX, rY)
+
+            val x: Float = event.getX() - rX
+            val y: Float = event.getY() - rY
+
+            val inCircle = (x / r.toDouble()).pow(2.0) + (y / r.toDouble()).pow(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 [UiDataModel] as the user scrolls
+     * the recyclerview or when the size/position of elements within the recyclerview changes.
+     */
+    private inner class ScrollPositionWatcher :
+            RecyclerView.OnScrollListener(), View.OnLayoutChangeListener {
+
+        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mLapsList))
+        }
+
+        override fun onLayoutChange(
+            v: View?,
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            oldLeft: Int,
+            oldTop: Int,
+            oldRight: Int,
+            oldBottom: Int
+        ) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mLapsList))
+        }
+    }
+
+    /**
+     * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the
+     * contrast between floating buttons and the laps list content.
+     */
+    private class GradientItemDecoration internal constructor(context: Context)
+        : RecyclerView.ItemDecoration() {
+
+        /**
+         * A reusable array of control point colors that define the gradient. It is based on the
+         * background color of the window and thus recomputed each time that color is changed.
+         */
+        private val mGradientColors = IntArray(ALPHAS.size)
+
+        /** The drawable that produces the tinting gradient effect of this decoration.  */
+        private val mGradient: GradientDrawable = GradientDrawable()
+
+        /** The height of the gradient; sized relative to the fab height.  */
+        private val mGradientHeight: Int
+
+        init {
+            mGradient.setOrientation(TOP_BOTTOM)
+            updateGradientColors(ThemeUtils.resolveColor(context, android.R.attr.windowBackground))
+
+            val resources: Resources = context.getResources()
+            val fabHeight: Int = resources.getDimensionPixelSize(R.dimen.fab_height)
+            mGradientHeight = (fabHeight * 1.2f).roundToInt()
+        }
+
+        override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+            super.onDrawOver(c, parent, state)
+
+            val w: Int = parent.getWidth()
+            val h: Int = parent.getHeight()
+
+            mGradient.setBounds(0, h - mGradientHeight, w, h)
+            mGradient.draw(c)
+        }
+
+        /**
+         * Given a `baseColor`, compute a gradient of tinted colors that define the fade
+         * effect to apply to the bottom of the lap list.
+         *
+         * @param baseColor a base color to which the gradient tint should be applied
+         */
+        fun updateGradientColors(@ColorInt baseColor: Int) {
+            // Compute the tinted colors that form the gradient.
+            mGradientColors.indices.forEach { i ->
+                mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i])
+            }
+
+            // Set the gradient colors into the drawable.
+            mGradient.setColors(mGradientColors)
+        }
+
+        companion object {
+            //  0% -  25% of gradient length -> opacity changes from 0% to 50%
+            // 25% -  90% of gradient length -> opacity changes from 50% to 100%
+            // 90% - 100% of gradient length -> opacity remains at 100%
+            private val ALPHAS = intArrayOf(
+                    0x00, // 0%
+                    0x1A, // 10%
+                    0x33, // 20%
+                    0x4D, // 30%
+                    0x66, // 40%
+                    0x80, // 50%
+                    0x89, // 53.8%
+                    0x93, // 57.6%
+                    0x9D, // 61.5%
+                    0xA7, // 65.3%
+                    0xB1, // 69.2%
+                    0xBA, // 73.0%
+                    0xC4, // 76.9%
+                    0xCE, // 80.7%
+                    0xD8, // 84.6%
+                    0xE2, // 88.4%
+                    0xEB, // 92.3%
+                    0xF5, // 96.1%
+                    0xFF, // 100%
+                    0xFF, // 100%
+                    0xFF)
+        }
+    }
+
+    companion object {
+        /** Milliseconds between redraws while running.  */
+        private const val REDRAW_PERIOD_RUNNING = 25
+
+        /** Milliseconds between redraws while paused.  */
+        private const val REDRAW_PERIOD_PAUSED = 500
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java
deleted file mode 100644
index 44113b0..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java
+++ /dev/null
@@ -1,167 +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.stopwatch;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.R;
-
-import static android.view.View.MeasureSpec.AT_MOST;
-import static android.view.View.MeasureSpec.EXACTLY;
-import static android.view.View.MeasureSpec.UNSPECIFIED;
-
-/**
- * Dynamically apportions size the stopwatch circle depending on the preferred width of the laps
- * list and the container size. Layouts fall into two different buckets:
- *
- * When the width of the laps list is less than half the container width, the laps list and
- * stopwatch display are each centered within half the container.
- * <pre>
- *     ---------------------------------------------------------------------------
- *     |                                    |               Lap 5                |
- *     |                                    |               Lap 4                |
- *     |             21:45.67               |               Lap 3                |
- *     |                                    |               Lap 2                |
- *     |                                    |               Lap 1                |
- *     ---------------------------------------------------------------------------
- * </pre>
- *
- * When the width of the laps list is greater than half the container width, the laps list is
- * granted all of the space it requires and the stopwatch display is centered within the remaining
- * container width.
- * <pre>
- *     ---------------------------------------------------------------------------
- *     |               |                          Lap 5                          |
- *     |               |                          Lap 4                          |
- *     |   21:45.67    |                          Lap 3                          |
- *     |               |                          Lap 2                          |
- *     |               |                          Lap 1                          |
- *     ---------------------------------------------------------------------------
- * </pre>
- */
-public class StopwatchLandscapeLayout extends ViewGroup {
-
-    private View mLapsListView;
-    private View mStopwatchView;
-
-    public StopwatchLandscapeLayout(Context context) {
-        super(context);
-    }
-
-    public StopwatchLandscapeLayout(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public StopwatchLandscapeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        mLapsListView = findViewById(R.id.laps_list);
-        mStopwatchView = findViewById(R.id.stopwatch_time_wrapper);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        final int height = MeasureSpec.getSize(heightMeasureSpec);
-        final int width = MeasureSpec.getSize(widthMeasureSpec);
-        final int halfWidth = width / 2;
-
-        final int minWidthSpec = MeasureSpec.makeMeasureSpec(width, UNSPECIFIED);
-        final int maxHeightSpec = MeasureSpec.makeMeasureSpec(height, AT_MOST);
-
-        // First determine the width of the laps list.
-        final int lapsListWidth;
-        if (mLapsListView != null && mLapsListView.getVisibility() != GONE) {
-            // Measure the intrinsic size of the laps list.
-            mLapsListView.measure(minWidthSpec, maxHeightSpec);
-
-            // Actual laps list width is the larger of half the container and its intrinsic width.
-            lapsListWidth = Math.max(mLapsListView.getMeasuredWidth(), halfWidth);
-            final int lapsListWidthSpec = MeasureSpec.makeMeasureSpec(lapsListWidth, EXACTLY);
-            mLapsListView.measure(lapsListWidthSpec, maxHeightSpec);
-        } else {
-            lapsListWidth = 0;
-        }
-
-        // Stopwatch timer consumes the remaining width of container not granted to laps list.
-        final int stopwatchWidth = width - lapsListWidth;
-        final int stopwatchWidthSpec = MeasureSpec.makeMeasureSpec(stopwatchWidth, EXACTLY);
-        mStopwatchView.measure(stopwatchWidthSpec, maxHeightSpec);
-
-        // Record the measured size of this container.
-        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        // Compute the space available for layout.
-        final int left = getPaddingLeft();
-        final int top = getPaddingTop();
-        final int right = getWidth() - getPaddingRight();
-        final int bottom = getHeight() - getPaddingBottom();
-        final int width = right - left;
-        final int height = bottom - top;
-        final int halfHeight = height / 2;
-        final boolean isLTR = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
-
-        final int lapsListWidth;
-        if (mLapsListView != null && mLapsListView.getVisibility() != GONE) {
-            // Layout the laps list, centering it vertically.
-            lapsListWidth = mLapsListView.getMeasuredWidth();
-            final int lapsListHeight = mLapsListView.getMeasuredHeight();
-            final int lapsListTop = top + halfHeight - (lapsListHeight / 2);
-            final int lapsListBottom = lapsListTop + lapsListHeight;
-            final int lapsListLeft;
-            final int lapsListRight;
-            if (isLTR) {
-                lapsListLeft = right - lapsListWidth;
-                lapsListRight = right;
-            } else {
-                lapsListLeft = left;
-                lapsListRight = left + lapsListWidth;
-            }
-
-            mLapsListView.layout(lapsListLeft, lapsListTop, lapsListRight, lapsListBottom);
-        } else {
-            lapsListWidth = 0;
-        }
-
-        // Layout the stopwatch, centering it horizontally and vertically.
-        final int stopwatchWidth = mStopwatchView.getMeasuredWidth();
-        final int stopwatchHeight = mStopwatchView.getMeasuredHeight();
-        final int stopwatchTop = top + halfHeight - (stopwatchHeight / 2);
-        final int stopwatchBottom = stopwatchTop + stopwatchHeight;
-        final int stopwatchLeft;
-        final int stopwatchRight;
-        if (isLTR) {
-            stopwatchLeft = left + ((width - lapsListWidth - stopwatchWidth) / 2);
-            stopwatchRight = stopwatchLeft + stopwatchWidth;
-        } else {
-            stopwatchRight = right - ((width - lapsListWidth - stopwatchWidth) / 2);
-            stopwatchLeft = stopwatchRight - stopwatchWidth;
-        }
-
-        mStopwatchView.layout(stopwatchLeft, stopwatchTop, stopwatchRight, stopwatchBottom);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.kt b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.kt
new file mode 100644
index 0000000..1bc2f7e
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.View.MeasureSpec.AT_MOST
+import android.view.View.MeasureSpec.EXACTLY
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.ViewGroup
+
+import com.android.deskclock.R
+
+import kotlin.math.max
+
+/**
+ * Dynamically apportions size the stopwatch circle depending on the preferred width of the laps
+ * list and the container size. Layouts fall into two different buckets:
+ *
+ * When the width of the laps list is less than half the container width, the laps list and
+ * stopwatch display are each centered within half the container.
+ * <pre>
+ * ---------------------------------------------------------------------------
+ * |                                    |               Lap 5                |
+ * |                                    |               Lap 4                |
+ * |             21:45.67               |               Lap 3                |
+ * |                                    |               Lap 2                |
+ * |                                    |               Lap 1                |
+ * ---------------------------------------------------------------------------
+</pre> *
+ *
+ * When the width of the laps list is greater than half the container width, the laps list is
+ * granted all of the space it requires and the stopwatch display is centered within the remaining
+ * container width.
+ * <pre>
+ * ---------------------------------------------------------------------------
+ * |               |                          Lap 5                          |
+ * |               |                          Lap 4                          |
+ * |   21:45.67    |                          Lap 3                          |
+ * |               |                          Lap 2                          |
+ * |               |                          Lap 1                          |
+ * ---------------------------------------------------------------------------
+</pre> *
+ */
+class StopwatchLandscapeLayout : ViewGroup {
+    private var mLapsListView: View? = null
+    private lateinit var mStopwatchView: View
+
+    constructor(context: Context?) : super(context) {
+    }
+
+    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
+    }
+
+    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
+            : super(context, attrs, defStyleAttr) {
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+
+        mLapsListView = findViewById(R.id.laps_list)
+        mStopwatchView = findViewById(R.id.stopwatch_time_wrapper)
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        val height: Int = MeasureSpec.getSize(heightMeasureSpec)
+        val width: Int = MeasureSpec.getSize(widthMeasureSpec)
+        val halfWidth = width / 2
+
+        val minWidthSpec: Int = MeasureSpec.makeMeasureSpec(width, UNSPECIFIED)
+        val maxHeightSpec: Int = MeasureSpec.makeMeasureSpec(height, AT_MOST)
+
+        // First determine the width of the laps list.
+        val lapsListWidth: Int
+        val lapsListView = mLapsListView
+        if (lapsListView != null && lapsListView.getVisibility() != GONE) {
+            // Measure the intrinsic size of the laps list.
+            lapsListView.measure(minWidthSpec, maxHeightSpec)
+
+            // Actual laps list width is the larger of half the container and its intrinsic width.
+            lapsListWidth = max(lapsListView.getMeasuredWidth(), halfWidth)
+            val lapsListWidthSpec: Int = MeasureSpec.makeMeasureSpec(lapsListWidth, EXACTLY)
+            lapsListView.measure(lapsListWidthSpec, maxHeightSpec)
+        } else {
+            lapsListWidth = 0
+        }
+
+        // Stopwatch timer consumes the remaining width of container not granted to laps list.
+        val stopwatchWidth = width - lapsListWidth
+        val stopwatchWidthSpec: Int = MeasureSpec.makeMeasureSpec(stopwatchWidth, EXACTLY)
+        mStopwatchView.measure(stopwatchWidthSpec, maxHeightSpec)
+
+        // Record the measured size of this container.
+        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
+    }
+
+    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+        // Compute the space available for layout.
+        val left: Int = getPaddingLeft()
+        val top: Int = getPaddingTop()
+        val right: Int = getWidth() - getPaddingRight()
+        val bottom: Int = getHeight() - getPaddingBottom()
+        val width = right - left
+        val height = bottom - top
+        val halfHeight = height / 2
+        val isLTR = getLayoutDirection() == LAYOUT_DIRECTION_LTR
+
+        val lapsListWidth: Int
+        val lapsListView = mLapsListView
+        if (lapsListView != null && lapsListView.getVisibility() != GONE) {
+            // Layout the laps list, centering it vertically.
+            lapsListWidth = lapsListView.getMeasuredWidth()
+            val lapsListHeight: Int = lapsListView.getMeasuredHeight()
+            val lapsListTop = top + halfHeight - lapsListHeight / 2
+            val lapsListBottom = lapsListTop + lapsListHeight
+            val lapsListLeft: Int
+            val lapsListRight: Int
+            if (isLTR) {
+                lapsListLeft = right - lapsListWidth
+                lapsListRight = right
+            } else {
+                lapsListLeft = left
+                lapsListRight = left + lapsListWidth
+            }
+            lapsListView.layout(lapsListLeft, lapsListTop, lapsListRight, lapsListBottom)
+        } else {
+            lapsListWidth = 0
+        }
+
+        // Layout the stopwatch, centering it horizontally and vertically.
+        val stopwatchWidth: Int = mStopwatchView.getMeasuredWidth()
+        val stopwatchHeight: Int = mStopwatchView.getMeasuredHeight()
+        val stopwatchTop = top + halfHeight - stopwatchHeight / 2
+        val stopwatchBottom = stopwatchTop + stopwatchHeight
+        val stopwatchLeft: Int
+        val stopwatchRight: Int
+        if (isLTR) {
+            stopwatchLeft = left + (width - lapsListWidth - stopwatchWidth) / 2
+            stopwatchRight = stopwatchLeft + stopwatchWidth
+        } else {
+            stopwatchRight = right - (width - lapsListWidth - stopwatchWidth) / 2
+            stopwatchLeft = stopwatchRight - stopwatchWidth
+        }
+
+        mStopwatchView.layout(stopwatchLeft, stopwatchTop, stopwatchRight, stopwatchBottom)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchService.java b/src/com/android/deskclock/stopwatch/StopwatchService.java
deleted file mode 100644
index f7f9425..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchService.java
+++ /dev/null
@@ -1,97 +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.stopwatch;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
-
-/**
- * This service exists solely to allow the stopwatch notification to alter the state of the
- * stopwatch without disturbing the notification shade. If an activity were used instead (even one
- * that is not displayed) the notification manager implicitly closes the notification shade which
- * clashes with the use case of starting/pausing/lapping/resetting the stopwatch without
- * disturbing the notification shade.
- */
-public final class StopwatchService extends Service {
-
-    private static final String ACTION_PREFIX = "com.android.deskclock.action.";
-
-    // shows the tab with the stopwatch
-    public static final String ACTION_SHOW_STOPWATCH = ACTION_PREFIX + "SHOW_STOPWATCH";
-    // starts the current stopwatch
-    public static final String ACTION_START_STOPWATCH = ACTION_PREFIX + "START_STOPWATCH";
-    // pauses the current stopwatch that's currently running
-    public static final String ACTION_PAUSE_STOPWATCH = ACTION_PREFIX + "PAUSE_STOPWATCH";
-    // laps the stopwatch that's currently running
-    public static final String ACTION_LAP_STOPWATCH = ACTION_PREFIX + "LAP_STOPWATCH";
-    // resets the stopwatch if it's stopped
-    public static final String ACTION_RESET_STOPWATCH = ACTION_PREFIX + "RESET_STOPWATCH";
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        final String action = intent.getAction();
-        final int label = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent);
-        switch (action) {
-            case ACTION_SHOW_STOPWATCH: {
-                Events.sendStopwatchEvent(R.string.action_show, label);
-
-                // Open DeskClock positioned on the stopwatch tab.
-                UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
-                final Intent showStopwatch = new Intent(this, DeskClock.class)
-                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                startActivity(showStopwatch);
-                break;
-            }
-            case ACTION_START_STOPWATCH: {
-                Events.sendStopwatchEvent(R.string.action_start, label);
-                DataModel.getDataModel().startStopwatch();
-                break;
-            }
-            case ACTION_PAUSE_STOPWATCH: {
-                Events.sendStopwatchEvent(R.string.action_pause, label);
-                DataModel.getDataModel().pauseStopwatch();
-                break;
-            }
-            case ACTION_RESET_STOPWATCH: {
-                Events.sendStopwatchEvent(R.string.action_reset, label);
-                DataModel.getDataModel().resetStopwatch();
-                break;
-            }
-            case ACTION_LAP_STOPWATCH: {
-                Events.sendStopwatchEvent(R.string.action_lap, label);
-                DataModel.getDataModel().addLap();
-                break;
-            }
-        }
-
-        return START_NOT_STICKY;
-    }
-}
diff --git a/src/com/android/deskclock/stopwatch/StopwatchService.kt b/src/com/android/deskclock/stopwatch/StopwatchService.kt
new file mode 100644
index 0000000..19ac00a
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchService.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * This service exists solely to allow the stopwatch notification to alter the state of the
+ * stopwatch without disturbing the notification shade. If an activity were used instead (even one
+ * that is not displayed) the notification manager implicitly closes the notification shade which
+ * clashes with the use case of starting/pausing/lapping/resetting the stopwatch without
+ * disturbing the notification shade.
+ */
+class StopwatchService : Service() {
+
+    override fun onBind(intent: Intent?): IBinder? = null
+
+    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+        val action: String? = intent.getAction()
+        val label: Int = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent)
+        when (action) {
+            ACTION_SHOW_STOPWATCH -> {
+                Events.sendStopwatchEvent(R.string.action_show, label)
+
+                // Open DeskClock positioned on the stopwatch tab.
+                UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.STOPWATCH
+                val showStopwatch: Intent = Intent(this, DeskClock::class.java)
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                startActivity(showStopwatch)
+            }
+            ACTION_START_STOPWATCH -> {
+                Events.sendStopwatchEvent(R.string.action_start, label)
+                DataModel.dataModel.startStopwatch()
+            }
+            ACTION_PAUSE_STOPWATCH -> {
+                Events.sendStopwatchEvent(R.string.action_pause, label)
+                DataModel.dataModel.pauseStopwatch()
+            }
+            ACTION_RESET_STOPWATCH -> {
+                Events.sendStopwatchEvent(R.string.action_reset, label)
+                DataModel.dataModel.resetStopwatch()
+            }
+            ACTION_LAP_STOPWATCH -> {
+                Events.sendStopwatchEvent(R.string.action_lap, label)
+                DataModel.dataModel.addLap()
+            }
+        }
+
+        return START_NOT_STICKY
+    }
+
+    companion object {
+        private const val ACTION_PREFIX = "com.android.deskclock.action."
+
+        // shows the tab with the stopwatch
+        const val ACTION_SHOW_STOPWATCH = ACTION_PREFIX + "SHOW_STOPWATCH"
+
+        // starts the current stopwatch
+        const val ACTION_START_STOPWATCH = ACTION_PREFIX + "START_STOPWATCH"
+
+        // pauses the current stopwatch that's currently running
+        const val ACTION_PAUSE_STOPWATCH = ACTION_PREFIX + "PAUSE_STOPWATCH"
+
+        // laps the stopwatch that's currently running
+        const val ACTION_LAP_STOPWATCH = ACTION_PREFIX + "LAP_STOPWATCH"
+
+        // resets the stopwatch if it's stopped
+        const val ACTION_RESET_STOPWATCH = ACTION_PREFIX + "RESET_STOPWATCH"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/ExpiredTimersActivity.java b/src/com/android/deskclock/timer/ExpiredTimersActivity.java
deleted file mode 100644
index 30ed121..0000000
--- a/src/com/android/deskclock/timer/ExpiredTimersActivity.java
+++ /dev/null
@@ -1,309 +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.timer;
-
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.os.Bundle;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-import android.transition.AutoTransition;
-import android.transition.TransitionManager;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import com.android.deskclock.BaseActivity;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.data.TimerListener;
-
-import java.util.List;
-
-/**
- * This activity is designed to be shown over the lock screen. As such, it displays the expired
- * timers and a single button to reset them all. Each expired timer can also be reset to one minute
- * with a button in the user interface. All other timer operations are disabled in this activity.
- */
-public class ExpiredTimersActivity extends BaseActivity {
-
-    /** Scheduled to update the timers while at least one is expired. */
-    private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
-
-    /** Updates the timers displayed in this activity as the backing data changes. */
-    private final TimerListener mTimerChangeWatcher = new TimerChangeWatcher();
-
-    /** The scene root for transitions when expired timers are added/removed from this container. */
-    private ViewGroup mExpiredTimersScrollView;
-
-    /** Displays the expired timers. */
-    private ViewGroup mExpiredTimersView;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        final List<Timer> expiredTimers = getExpiredTimers();
-
-        // If no expired timers, finish
-        if (expiredTimers.size() == 0) {
-            LogUtils.i("No expired timers, skipping display.");
-            finish();
-            return;
-        }
-
-        setContentView(R.layout.expired_timers_activity);
-
-        mExpiredTimersView = (ViewGroup) findViewById(R.id.expired_timers_list);
-        mExpiredTimersScrollView = (ViewGroup) findViewById(R.id.expired_timers_scroll);
-
-        findViewById(R.id.fab).setOnClickListener(new FabClickListener());
-
-        final View view = findViewById(R.id.expired_timers_activity);
-        view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
-
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
-                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
-                | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
-                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
-                | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);
-
-        // Close dialogs and window shade, so this is fully visible
-        sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-
-        // Honor rotation on tablets; fix the orientation on phones.
-        if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
-            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
-        }
-
-        // Create views for each of the expired timers.
-        for (Timer timer : expiredTimers) {
-            addTimer(timer);
-        }
-
-        // Update views in response to timer data changes.
-        DataModel.getDataModel().addTimerListener(mTimerChangeWatcher);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        startUpdatingTime();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-        stopUpdatingTime();
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        DataModel.getDataModel().removeTimerListener(mTimerChangeWatcher);
-    }
-
-    @Override
-    public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
-        if (event.getAction() == KeyEvent.ACTION_UP) {
-            switch (event.getKeyCode()) {
-                case KeyEvent.KEYCODE_VOLUME_UP:
-                case KeyEvent.KEYCODE_VOLUME_DOWN:
-                case KeyEvent.KEYCODE_VOLUME_MUTE:
-                case KeyEvent.KEYCODE_CAMERA:
-                case KeyEvent.KEYCODE_FOCUS:
-                    DataModel.getDataModel().resetOrDeleteExpiredTimers(
-                            R.string.label_hardware_button);
-                    return true;
-            }
-        }
-        return super.dispatchKeyEvent(event);
-    }
-
-    /**
-     * Post the first runnable to update times within the UI. It will reschedule itself as needed.
-     */
-    private void startUpdatingTime() {
-        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
-        stopUpdatingTime();
-        mExpiredTimersView.post(mTimeUpdateRunnable);
-    }
-
-    /**
-     * Remove the runnable that updates times within the UI.
-     */
-    private void stopUpdatingTime() {
-        mExpiredTimersView.removeCallbacks(mTimeUpdateRunnable);
-    }
-
-    /**
-     * Create and add a new view that corresponds with the given {@code timer}.
-     */
-    private void addTimer(Timer timer) {
-        TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition());
-
-        final int timerId = timer.getId();
-        final TimerItem timerItem = (TimerItem)
-                getLayoutInflater().inflate(R.layout.timer_item, mExpiredTimersView, false);
-        // Store the timer id as a tag on the view so it can be located on delete.
-        timerItem.setId(timerId);
-        mExpiredTimersView.addView(timerItem);
-
-        // 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);
-        addMinuteButton.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                final Timer timer = DataModel.getDataModel().getTimer(timerId);
-                DataModel.getDataModel().addTimerMinute(timer);
-            }
-        });
-
-        // If the first timer was just added, center it.
-        final List<Timer> expiredTimers = getExpiredTimers();
-        if (expiredTimers.size() == 1) {
-            centerFirstTimer();
-        } else if (expiredTimers.size() == 2) {
-            uncenterFirstTimer();
-        }
-    }
-
-    /**
-     * Remove an existing view that corresponds with the given {@code timer}.
-     */
-    private void removeTimer(Timer timer) {
-        TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition());
-
-        final int timerId = timer.getId();
-        final int count = mExpiredTimersView.getChildCount();
-        for (int i = 0; i < count; ++i) {
-            final View timerView = mExpiredTimersView.getChildAt(i);
-            if (timerView.getId() == timerId) {
-                mExpiredTimersView.removeView(timerView);
-                break;
-            }
-        }
-
-        // If the second last timer was just removed, center the last timer.
-        final List<Timer> expiredTimers = getExpiredTimers();
-        if (expiredTimers.isEmpty()) {
-            finish();
-        } else if (expiredTimers.size() == 1) {
-            centerFirstTimer();
-        }
-    }
-
-    /**
-     * Center the single timer.
-     */
-    private void centerFirstTimer() {
-        final FrameLayout.LayoutParams lp =
-                (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams();
-        lp.gravity = Gravity.CENTER;
-        mExpiredTimersView.requestLayout();
-    }
-
-    /**
-     * Display the multiple timers as a scrollable list.
-     */
-    private void uncenterFirstTimer() {
-        final FrameLayout.LayoutParams lp =
-                (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams();
-        lp.gravity = Gravity.NO_GRAVITY;
-        mExpiredTimersView.requestLayout();
-    }
-
-    private List<Timer> getExpiredTimers() {
-        return DataModel.getDataModel().getExpiredTimers();
-    }
-
-    /**
-     * Periodically refreshes the state of each timer.
-     */
-    private class TimeUpdateRunnable implements Runnable {
-        @Override
-        public void run() {
-            final long startTime = SystemClock.elapsedRealtime();
-
-            final int count = mExpiredTimersView.getChildCount();
-            for (int i = 0; i < count; ++i) {
-                final TimerItem timerItem = (TimerItem) mExpiredTimersView.getChildAt(i);
-                final Timer timer = DataModel.getDataModel().getTimer(timerItem.getId());
-                if (timer != null) {
-                    timerItem.update(timer);
-                }
-            }
-
-            final long endTime = SystemClock.elapsedRealtime();
-
-            // Try to maintain a consistent period of time between redraws.
-            final long delay = Math.max(0L, startTime + 20L - endTime);
-            mExpiredTimersView.postDelayed(this, delay);
-        }
-    }
-
-    /**
-     * Clicking the fab resets all expired timers.
-     */
-    private class FabClickListener implements View.OnClickListener {
-        @Override
-        public void onClick(View v) {
-            stopUpdatingTime();
-            DataModel.getDataModel().removeTimerListener(mTimerChangeWatcher);
-            DataModel.getDataModel().resetOrDeleteExpiredTimers(R.string.label_deskclock);
-            finish();
-        }
-    }
-
-    /**
-     * Adds and removes expired timers from this activity based on their state changes.
-     */
-    private class TimerChangeWatcher implements TimerListener {
-        @Override
-        public void timerAdded(Timer timer) {
-            if (timer.isExpired()) {
-                addTimer(timer);
-            }
-        }
-
-        @Override
-        public void timerUpdated(Timer before, Timer after) {
-            if (!before.isExpired() && after.isExpired()) {
-                addTimer(after);
-            } else if (before.isExpired() && !after.isExpired()) {
-                removeTimer(before);
-            }
-        }
-
-        @Override
-        public void timerRemoved(Timer timer) {
-            if (timer.isExpired()) {
-                removeTimer(timer);
-            }
-        }
-    }
-}
diff --git a/src/com/android/deskclock/timer/ExpiredTimersActivity.kt b/src/com/android/deskclock/timer/ExpiredTimersActivity.kt
new file mode 100644
index 0000000..09c4ee1
--- /dev/null
+++ b/src/com/android/deskclock/timer/ExpiredTimersActivity.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT 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.Intent
+import android.content.pm.ActivityInfo
+import android.os.Bundle
+import android.os.SystemClock
+import android.text.TextUtils
+import android.transition.AutoTransition
+import android.transition.TransitionManager
+import android.widget.FrameLayout
+import android.widget.TextView
+import android.view.Gravity
+import android.view.KeyEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.data.TimerListener
+
+/**
+ * This activity is designed to be shown over the lock screen. As such, it displays the expired
+ * timers and a single button to reset them all. Each expired timer can also be reset to one minute
+ * with a button in the user interface. All other timer operations are disabled in this activity.
+ */
+class ExpiredTimersActivity : BaseActivity() {
+    /** Scheduled to update the timers while at least one is expired.  */
+    private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
+
+    /** Updates the timers displayed in this activity as the backing data changes.  */
+    private val mTimerChangeWatcher: TimerListener = TimerChangeWatcher()
+
+    /** The scene root for transitions when expired timers are added/removed from this container. */
+    private lateinit var mExpiredTimersScrollView: ViewGroup
+
+    /** Displays the expired timers.  */
+    private lateinit var mExpiredTimersView: ViewGroup
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val expiredTimers = expiredTimers
+
+        // If no expired timers, finish
+        if (expiredTimers.size == 0) {
+            LogUtils.i("No expired timers, skipping display.")
+            finish()
+            return
+        }
+
+        setContentView(R.layout.expired_timers_activity)
+
+        mExpiredTimersView = findViewById(R.id.expired_timers_list) as ViewGroup
+        mExpiredTimersScrollView = findViewById(R.id.expired_timers_scroll) as ViewGroup
+
+        (findViewById(R.id.fab) as View).setOnClickListener(FabClickListener())
+
+        val view: View = findViewById(R.id.expired_timers_activity)
+        view.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
+
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
+
+        setTurnScreenOn(true)
+        setShowWhenLocked(true)
+
+        // Close dialogs and window shade, so this is fully visible
+        sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+
+        // Honor rotation on tablets; fix the orientation on phones.
+        if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
+            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR)
+        }
+
+        // Create views for each of the expired timers.
+        for (timer in expiredTimers) {
+            addTimer(timer)
+        }
+
+        // Update views in response to timer data changes.
+        DataModel.dataModel.addTimerListener(mTimerChangeWatcher)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        startUpdatingTime()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        stopUpdatingTime()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        DataModel.dataModel.removeTimerListener(mTimerChangeWatcher)
+    }
+
+    override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+        if (event.action == KeyEvent.ACTION_UP) {
+            when (event.keyCode) {
+                KeyEvent.KEYCODE_VOLUME_UP,
+                KeyEvent.KEYCODE_VOLUME_DOWN,
+                KeyEvent.KEYCODE_VOLUME_MUTE,
+                KeyEvent.KEYCODE_CAMERA,
+                KeyEvent.KEYCODE_FOCUS -> {
+                    DataModel.dataModel.resetOrDeleteExpiredTimers(R.string.label_hardware_button)
+                    return true
+                }
+            }
+        }
+        return super.dispatchKeyEvent(event)
+    }
+
+    /**
+     * Post the first runnable to update times within the UI. It will reschedule itself as needed.
+     */
+    private fun startUpdatingTime() {
+        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
+        stopUpdatingTime()
+        mExpiredTimersView.post(mTimeUpdateRunnable)
+    }
+
+    /**
+     * Remove the runnable that updates times within the UI.
+     */
+    private fun stopUpdatingTime() {
+        mExpiredTimersView.removeCallbacks(mTimeUpdateRunnable)
+    }
+
+    /**
+     * Create and add a new view that corresponds with the given `timer`.
+     */
+    private fun addTimer(timer: Timer) {
+        TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, AutoTransition())
+
+        val timerId: Int = timer.id
+        val timerItem = getLayoutInflater()
+                .inflate(R.layout.timer_item, mExpiredTimersView, false) as TimerItem
+        // Store the timer id as a tag on the view so it can be located on delete.
+        timerItem.id = timerId
+        mExpiredTimersView.addView(timerItem)
+
+        // Hide the label hint for expired timers.
+        val labelView = timerItem.findViewById<View>(R.id.timer_label) as TextView
+        labelView.hint = null
+        labelView.visibility = if (TextUtils.isEmpty(timer.label)) View.GONE else View.VISIBLE
+
+        // Add logic to the "Add 1 Minute" button.
+        val addMinuteButton = timerItem.findViewById<View>(R.id.reset_add)
+        addMinuteButton.setOnClickListener {
+            val timer: Timer = DataModel.dataModel.getTimer(timerId)!!
+            DataModel.dataModel.addTimerMinute(timer)
+        }
+
+        // If the first timer was just added, center it.
+        val expiredTimers = expiredTimers
+        if (expiredTimers.size == 1) {
+            centerFirstTimer()
+        } else if (expiredTimers.size == 2) {
+            uncenterFirstTimer()
+        }
+    }
+
+    /**
+     * Remove an existing view that corresponds with the given `timer`.
+     */
+    private fun removeTimer(timer: Timer) {
+        TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, AutoTransition())
+
+        val timerId: Int = timer.id
+        val count = mExpiredTimersView.childCount
+        for (i in 0 until count) {
+            val timerView = mExpiredTimersView.getChildAt(i)
+            if (timerView.id == timerId) {
+                mExpiredTimersView.removeView(timerView)
+                break
+            }
+        }
+
+        // If the second last timer was just removed, center the last timer.
+        val expiredTimers = expiredTimers
+        if (expiredTimers.isEmpty()) {
+            finish()
+        } else if (expiredTimers.size == 1) {
+            centerFirstTimer()
+        }
+    }
+
+    /**
+     * Center the single timer.
+     */
+    private fun centerFirstTimer() {
+        val lp = mExpiredTimersView.layoutParams as FrameLayout.LayoutParams
+        lp.gravity = Gravity.CENTER
+        mExpiredTimersView.requestLayout()
+    }
+
+    /**
+     * Display the multiple timers as a scrollable list.
+     */
+    private fun uncenterFirstTimer() {
+        val lp = mExpiredTimersView.layoutParams as FrameLayout.LayoutParams
+        lp.gravity = Gravity.NO_GRAVITY
+        mExpiredTimersView.requestLayout()
+    }
+
+    private val expiredTimers: List<Timer>
+        get() = DataModel.dataModel.expiredTimers
+
+    /**
+     * Periodically refreshes the state of each timer.
+     */
+    private inner class TimeUpdateRunnable : Runnable {
+        override fun run() {
+            val startTime = SystemClock.elapsedRealtime()
+
+            val count = mExpiredTimersView.childCount
+            for (i in 0 until count) {
+                val timerItem = mExpiredTimersView.getChildAt(i) as TimerItem
+                val timer: Timer? = DataModel.dataModel.getTimer(timerItem.id)
+                if (timer != null) {
+                    timerItem.update(timer)
+                }
+            }
+
+            val endTime = SystemClock.elapsedRealtime()
+
+            // Try to maintain a consistent period of time between redraws.
+            val delay = Math.max(0L, startTime + 20L - endTime)
+            mExpiredTimersView.postDelayed(this, delay)
+        }
+    }
+
+    /**
+     * Clicking the fab resets all expired timers.
+     */
+    private inner class FabClickListener : View.OnClickListener {
+        override fun onClick(v: View) {
+            stopUpdatingTime()
+            DataModel.dataModel.removeTimerListener(mTimerChangeWatcher)
+            DataModel.dataModel.resetOrDeleteExpiredTimers(R.string.label_deskclock)
+            finish()
+        }
+    }
+
+    /**
+     * Adds and removes expired timers from this activity based on their state changes.
+     */
+    private inner class TimerChangeWatcher : TimerListener {
+        override fun timerAdded(timer: Timer) {
+            if (timer.isExpired) {
+                addTimer(timer)
+            }
+        }
+
+        override fun timerUpdated(before: Timer, after: Timer) {
+            if (!before.isExpired && after.isExpired) {
+                addTimer(after)
+            } else if (before.isExpired && !after.isExpired) {
+                removeTimer(before)
+            }
+        }
+
+        override fun timerRemoved(timer: Timer) {
+            if (timer.isExpired) {
+                removeTimer(timer)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerCircleView.java b/src/com/android/deskclock/timer/TimerCircleView.java
deleted file mode 100644
index f605f91..0000000
--- a/src/com/android/deskclock/timer/TimerCircleView.java
+++ /dev/null
@@ -1,151 +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.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.RectF;
-import android.util.AttributeSet;
-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;
-
-/**
- * Custom view that draws timer progress as a circle.
- */
-public final class TimerCircleView extends View {
-
-    /** The size of the dot indicating the progress through the timer. */
-    private final float mDotRadius;
-
-    /** An amount to subtract from the true radius to account for drawing thicknesses. */
-    private final float mRadiusOffset;
-
-    /** The color indicating the remaining portion of the timer. */
-    private final int mRemainderColor;
-
-    /** The color indicating the completed portion of the timer. */
-    private final int mCompletedColor;
-
-    /** The size of the stroke that paints the timer circle. */
-    private final float mStrokeSize;
-
-    private final Paint mPaint = new Paint();
-    private final Paint mFill = new Paint();
-    private final RectF mArcRect = new RectF();
-
-    private Timer mTimer;
-
-    @SuppressWarnings("unused")
-    public TimerCircleView(Context context) {
-        this(context, null);
-    }
-
-    public TimerCircleView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        final Resources resources = context.getResources();
-        final float dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size);
-
-        mDotRadius = dotDiameter / 2f;
-        mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
-        mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, 0);
-
-        mRemainderColor = Color.WHITE;
-        mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent);
-
-        mPaint.setAntiAlias(true);
-        mPaint.setStyle(Paint.Style.STROKE);
-
-        mFill.setAntiAlias(true);
-        mFill.setColor(mCompletedColor);
-        mFill.setStyle(Paint.Style.FILL);
-    }
-
-    void update(Timer timer) {
-        if (mTimer != timer) {
-            mTimer = timer;
-            postInvalidateOnAnimation();
-        }
-    }
-
-    @Override
-    public void onDraw(Canvas canvas) {
-        if (mTimer == null) {
-            return;
-        }
-
-        // Compute the size and location of the circle to be drawn.
-        final int xCenter = getWidth() / 2;
-        final int yCenter = getHeight() / 2;
-        final float radius = Math.min(xCenter, yCenter) - mRadiusOffset;
-
-        // Reset old painting state.
-        mPaint.setColor(mRemainderColor);
-        mPaint.setStrokeWidth(mStrokeSize);
-
-        // If the timer is reset, draw a simple white circle.
-        final float redPercent;
-        if (mTimer.isReset()) {
-            // Draw a complete white circle; no red arc required.
-            canvas.drawCircle(xCenter, yCenter, radius, mPaint);
-
-            // Red percent is 0 since no timer progress has been made.
-            redPercent = 0;
-        } else if (mTimer.isExpired()) {
-            mPaint.setColor(mCompletedColor);
-
-            // Draw a complete white circle; no red arc required.
-            canvas.drawCircle(xCenter, yCenter, radius, mPaint);
-
-            // Red percent is 1 since the timer has expired.
-            redPercent = 1;
-        } else {
-            // Draw a combination of red and white arcs to create a circle.
-            mArcRect.top = yCenter - radius;
-            mArcRect.bottom = yCenter + radius;
-            mArcRect.left = xCenter - radius;
-            mArcRect.right = xCenter + radius;
-            redPercent = Math.min(1, (float) mTimer.getElapsedTime() / (float) mTimer.getTotalLength());
-            final float whitePercent = 1 - redPercent;
-
-            // Draw a white arc to indicate the amount of timer that remains.
-            canvas.drawArc(mArcRect, 270, whitePercent * 360, false, mPaint);
-
-            // Draw a red arc to indicate the amount of timer completed.
-            mPaint.setColor(mCompletedColor);
-            canvas.drawArc(mArcRect, 270, -redPercent * 360 , false, mPaint);
-        }
-
-        // Draw a red dot to indicate current progress through the timer.
-        final float dotAngleDegrees = 270 - redPercent * 360;
-        final double dotAngleRadians = Math.toRadians(dotAngleDegrees);
-        final float dotX = xCenter + (float) (radius * Math.cos(dotAngleRadians));
-        final float dotY = yCenter + (float) (radius * Math.sin(dotAngleRadians));
-        canvas.drawCircle(dotX, dotY, mDotRadius, mFill);
-
-        if (mTimer.isRunning()) {
-            postInvalidateOnAnimation();
-        }
-    }
-}
diff --git a/src/com/android/deskclock/timer/TimerCircleView.kt b/src/com/android/deskclock/timer/TimerCircleView.kt
new file mode 100644
index 0000000..af45501
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerCircleView.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.view.View
+
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.data.Timer
+
+import kotlin.math.cos
+import kotlin.math.min
+import kotlin.math.sin
+
+/**
+ * Custom view that draws timer progress as a circle.
+ */
+class TimerCircleView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : View(context, attrs) {
+    /** The size of the dot indicating the progress through the timer.  */
+    private val mDotRadius: Float
+
+    /** An amount to subtract from the true radius to account for drawing thicknesses.  */
+    private val mRadiusOffset: Float
+
+    /** The color indicating the remaining portion of the timer.  */
+    private val mRemainderColor: Int
+
+    /** The color indicating the completed portion of the timer.  */
+    private val mCompletedColor: Int
+
+    /** The size of the stroke that paints the timer circle.  */
+    private val mStrokeSize: Float
+
+    private val mPaint = Paint()
+    private val mFill = Paint()
+    private val mArcRect = RectF()
+
+    private var mTimer: Timer? = null
+
+    init {
+        val resources = context.resources
+        val dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size)
+
+        mDotRadius = dotDiameter / 2f
+        mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size)
+        mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, 0f)
+
+        mRemainderColor = Color.WHITE
+        mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent)
+
+        mPaint.isAntiAlias = true
+        mPaint.style = Paint.Style.STROKE
+
+        mFill.isAntiAlias = true
+        mFill.color = mCompletedColor
+        mFill.style = Paint.Style.FILL
+    }
+
+    fun update(timer: Timer) {
+        if (mTimer !== timer) {
+            mTimer = timer
+            postInvalidateOnAnimation()
+        }
+    }
+
+    public override fun onDraw(canvas: Canvas) {
+        if (mTimer == null) {
+            return
+        }
+
+        // Compute the size and location of the circle to be drawn.
+        val xCenter = width / 2
+        val yCenter = height / 2
+        val radius = min(xCenter, yCenter) - mRadiusOffset
+
+        // Reset old painting state.
+        mPaint.color = mRemainderColor
+        mPaint.strokeWidth = mStrokeSize
+
+        // If the timer is reset, draw a simple white circle.
+        val redPercent: Float
+        when {
+            mTimer!!.isReset -> {
+                // Draw a complete white circle; no red arc required.
+                canvas.drawCircle(xCenter.toFloat(), yCenter.toFloat(), radius, mPaint)
+
+                // Red percent is 0 since no timer progress has been made.
+                redPercent = 0f
+            }
+            mTimer!!.isExpired -> {
+                mPaint.color = mCompletedColor
+
+                // Draw a complete white circle; no red arc required.
+                canvas.drawCircle(xCenter.toFloat(), yCenter.toFloat(), radius, mPaint)
+
+                // Red percent is 1 since the timer has expired.
+                redPercent = 1f
+            }
+            else -> {
+                // Draw a combination of red and white arcs to create a circle.
+                mArcRect.top = yCenter - radius
+                mArcRect.bottom = yCenter + radius
+                mArcRect.left = xCenter - radius
+                mArcRect.right = xCenter + radius
+                redPercent = min(1f,
+                        mTimer!!.elapsedTime.toFloat() / mTimer!!.totalLength.toFloat())
+                val whitePercent = 1 - redPercent
+
+                // Draw a white arc to indicate the amount of timer that remains.
+                canvas.drawArc(mArcRect, 270f, whitePercent * 360, false, mPaint)
+
+                // Draw a red arc to indicate the amount of timer completed.
+                mPaint.color = mCompletedColor
+                canvas.drawArc(mArcRect, 270f, -redPercent * 360, false, mPaint)
+            }
+        }
+
+        // Draw a red dot to indicate current progress through the timer.
+        val dotAngleDegrees = 270 - redPercent * 360
+        val dotAngleRadians = Math.toRadians(dotAngleDegrees.toDouble())
+        val dotX = xCenter + (radius * cos(dotAngleRadians)).toFloat()
+        val dotY = yCenter + (radius * sin(dotAngleRadians)).toFloat()
+        canvas.drawCircle(dotX, dotY, mDotRadius, mFill)
+
+        if (mTimer!!.isRunning) {
+            postInvalidateOnAnimation()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerFragment.java b/src/com/android/deskclock/timer/TimerFragment.java
deleted file mode 100644
index 8b30105..0000000
--- a/src/com/android/deskclock/timer/TimerFragment.java
+++ /dev/null
@@ -1,789 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.deskclock.timer;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.viewpager.widget.ViewPager;
-import android.view.KeyEvent;
-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.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;
-
-import java.io.Serializable;
-import java.util.Arrays;
-
-import static android.view.View.ALPHA;
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.TRANSLATION_Y;
-import static android.view.View.VISIBLE;
-import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
-
-/**
- * Displays a vertical list of timers in all states.
- */
-public final class TimerFragment extends DeskClockFragment {
-
-    private static final String EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP";
-
-    private static final String KEY_TIMER_SETUP_STATE = "timer_setup_input";
-
-    /** Notified when the user swipes vertically to change the visible timer. */
-    private final TimerPageChangeListener mTimerPageChangeListener = new TimerPageChangeListener();
-
-    /** Scheduled to update the timers while at least one is running. */
-    private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
-
-    /** Updates the {@link #mPageIndicators} in response to timers being added or removed. */
-    private final TimerListener mTimerWatcher = new TimerWatcher();
-
-    private TimerSetupView mCreateTimerView;
-    private ViewPager mViewPager;
-    private TimerPagerAdapter mAdapter;
-    private View mTimersView;
-    private View mCurrentView;
-    private ImageView[] mPageIndicators;
-
-    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.
-     */
-    public static Intent createTimerSetupIntent(Context context) {
-        return new Intent(context, DeskClock.class).putExtra(EXTRA_TIMER_SETUP, true);
-    }
-
-    /** The public no-arg constructor required by all fragments. */
-    public TimerFragment() {
-        super(TIMERS);
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        final View view = inflater.inflate(R.layout.timer_fragment, container, false);
-
-        mAdapter = new TimerPagerAdapter(getFragmentManager());
-        mViewPager = (ViewPager) view.findViewById(R.id.vertical_view_pager);
-        mViewPager.setAdapter(mAdapter);
-        mViewPager.addOnPageChangeListener(mTimerPageChangeListener);
-
-        mTimersView = view.findViewById(R.id.timer_view);
-        mCreateTimerView = (TimerSetupView) view.findViewById(R.id.timer_setup);
-        mCreateTimerView.setFabContainer(this);
-        mPageIndicators = new ImageView[] {
-                (ImageView) view.findViewById(R.id.page_indicator0),
-                (ImageView) view.findViewById(R.id.page_indicator1),
-                (ImageView) view.findViewById(R.id.page_indicator2),
-                (ImageView) view.findViewById(R.id.page_indicator3)
-        };
-
-        DataModel.getDataModel().addTimerListener(mAdapter);
-        DataModel.getDataModel().addTimerListener(mTimerWatcher);
-
-        // If timer setup state is present, retrieve it to be later honored.
-        if (savedInstanceState != null) {
-            mTimerSetupState = savedInstanceState.getSerializable(KEY_TIMER_SETUP_STATE);
-        }
-
-        return view;
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-
-        // Initialize the page indicators.
-        updatePageIndicators();
-
-        boolean createTimer = false;
-        int showTimerId = -1;
-
-        // Examine the intent of the parent activity to determine which view to display.
-        final Intent intent = getActivity().getIntent();
-        if (intent != null) {
-            // These extras are single-use; remove them after honoring them.
-            createTimer = intent.getBooleanExtra(EXTRA_TIMER_SETUP, false);
-            intent.removeExtra(EXTRA_TIMER_SETUP);
-
-            showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
-            intent.removeExtra(TimerService.EXTRA_TIMER_ID);
-        }
-
-        // Choose the view to display in this fragment.
-        if (showTimerId != -1) {
-            // A specific timer must be shown; show the list of timers.
-            showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
-        } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
-            // No timers exist, a timer is being created, or the last view was timer setup;
-            // show the timer setup view.
-            showCreateTimerView(FAB_AND_BUTTONS_IMMEDIATE);
-
-            if (mTimerSetupState != null) {
-                mCreateTimerView.setState(mTimerSetupState);
-                mTimerSetupState = null;
-            }
-        } else {
-            // Otherwise, default to showing the list of timers.
-            showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
-        }
-
-        // If the intent did not specify a timer to show, show the last timer that expired.
-        if (showTimerId == -1) {
-            final Timer timer = DataModel.getDataModel().getMostRecentExpiredTimer();
-            showTimerId = timer == null ? -1 : timer.getId();
-        }
-
-        // If a specific timer should be displayed, display the corresponding timer tab.
-        if (showTimerId != -1) {
-            final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
-            if (timer != null) {
-                final int index = DataModel.getDataModel().getTimers().indexOf(timer);
-                mViewPager.setCurrentItem(index);
-            }
-        }
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        // We may have received a new intent while paused.
-        final Intent intent = getActivity().getIntent();
-        if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
-            // This extra is single-use; remove after honoring it.
-            final int showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
-            intent.removeExtra(TimerService.EXTRA_TIMER_ID);
-
-            final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
-            if (timer != null) {
-                // A specific timer must be shown; show the list of timers.
-                final int index = DataModel.getDataModel().getTimers().indexOf(timer);
-                mViewPager.setCurrentItem(index);
-
-                animateToView(mTimersView, null, false);
-            }
-        }
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-
-        // Stop updating the timers when this fragment is no longer visible.
-        stopUpdatingTime();
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-
-        DataModel.getDataModel().removeTimerListener(mAdapter);
-        DataModel.getDataModel().removeTimerListener(mTimerWatcher);
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-
-        // If the timer creation view is visible, store the input for later restoration.
-        if (mCurrentView == mCreateTimerView) {
-            mTimerSetupState = mCreateTimerView.getState();
-            outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState);
-        }
-    }
-
-    private void updateFab(@NonNull ImageView fab, boolean animate) {
-        if (mCurrentView == mTimersView) {
-            final Timer timer = getTimer();
-            if (timer == null) {
-                fab.setVisibility(INVISIBLE);
-                return;
-            }
-
-            fab.setVisibility(VISIBLE);
-            switch (timer.getState()) {
-                case RUNNING:
-                    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:
-                    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:
-                case EXPIRED:
-                    fab.setImageResource(R.drawable.ic_stop_white_24dp);
-                    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 onUpdateFab(@NonNull ImageView fab) {
-        updateFab(fab, false);
-    }
-
-    @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(VISIBLE);
-
-        } else if (mCurrentView == mCreateTimerView) {
-            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(INVISIBLE);
-        }
-    }
-
-    @Override
-    public void onFabClick(@NonNull ImageView fab) {
-        if (mCurrentView == mTimersView) {
-            final Timer timer = getTimer();
-
-            // If no timer is currently showing a fab action is meaningless.
-            if (timer == null) {
-                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:
-                    DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_deskclock);
-                    break;
-            }
-
-        } else if (mCurrentView == mCreateTimerView) {
-            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);
-
-                // Display the freshly created timer view.
-                mViewPager.setCurrentItem(0);
-            } finally {
-                mCreatingTimer = false;
-            }
-
-            // Return to the list of timers.
-            animateToView(mTimersView, null, true);
-        }
-    }
-
-    @Override
-    public void onLeftButtonClick(@NonNull Button left) {
-        if (mCurrentView == mTimersView) {
-            // Clicking the "delete" button.
-            final Timer timer = getTimer();
-            if (timer == null) {
-                return;
-            }
-
-            if (mAdapter.getCount() > 1) {
-                animateTimerRemove(timer);
-            } else {
-                animateToView(mCreateTimerView, timer, false);
-            }
-
-            left.announceForAccessibility(getActivity().getString(R.string.timer_deleted));
-        } else if (mCurrentView == mCreateTimerView) {
-            // Clicking the "cancel" button on the timer creation page returns to the timers list.
-            mCreateTimerView.reset();
-
-            animateToView(mTimersView, null, false);
-
-            left.announceForAccessibility(getActivity().getString(R.string.timer_canceled));
-        }
-    }
-
-    @Override
-    public void onRightButtonClick(@NonNull Button right) {
-        if (mCurrentView != mCreateTimerView) {
-            animateToView(mCreateTimerView, null, true);
-        }
-    }
-
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (mCurrentView == mCreateTimerView) {
-            return mCreateTimerView.onKeyDown(keyCode, event);
-        }
-        return super.onKeyDown(keyCode, event);
-    }
-
-    /**
-     * Updates the state of the page indicators so they reflect the selected page in the context of
-     * all pages.
-     */
-    private void updatePageIndicators() {
-        final int page = mViewPager.getCurrentItem();
-        final int pageIndicatorCount = mPageIndicators.length;
-        final int pageCount = mAdapter.getCount();
-
-        final int[] states = computePageIndicatorStates(page, pageIndicatorCount, pageCount);
-        for (int i = 0; i < states.length; i++) {
-            final int state = states[i];
-            final ImageView pageIndicator = mPageIndicators[i];
-            if (state == 0) {
-                pageIndicator.setVisibility(GONE);
-            } else {
-                pageIndicator.setVisibility(VISIBLE);
-                pageIndicator.setImageResource(state);
-            }
-        }
-    }
-
-    /**
-     * @param page the selected page; value between 0 and {@code pageCount}
-     * @param pageIndicatorCount the number of indicators displaying the {@code page} location
-     * @param pageCount the number of pages that exist
-     * @return an array of length {@code pageIndicatorCount} specifying which image to display for
-     *      each page indicator or 0 if the page indicator should be hidden
-     */
-    @VisibleForTesting
-    static int[] computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount) {
-        // Compute the number of page indicators that will be visible.
-        final int rangeSize = Math.min(pageIndicatorCount, pageCount);
-
-        // Compute the inclusive range of pages to indicate centered around the selected page.
-        int rangeStart = page - (rangeSize / 2);
-        int rangeEnd = rangeStart + rangeSize - 1;
-
-        // Clamp the range of pages if they extend beyond the last page.
-        if (rangeEnd >= pageCount) {
-            rangeEnd = pageCount - 1;
-            rangeStart = rangeEnd - rangeSize + 1;
-        }
-
-        // Clamp the range of pages if they extend beyond the first page.
-        if (rangeStart < 0) {
-            rangeStart = 0;
-            rangeEnd = rangeSize - 1;
-        }
-
-        // Build the result with all page indicators initially hidden.
-        final int[] states = new int[pageIndicatorCount];
-        Arrays.fill(states, 0);
-
-        // If 0 or 1 total pages exist, all page indicators must remain hidden.
-        if (rangeSize < 2) {
-            return states;
-        }
-
-        // Initialize the visible page indicators to be dark.
-        Arrays.fill(states, 0, rangeSize, R.drawable.ic_swipe_circle_dark);
-
-        // If more pages exist before the first page indicator, make it a fade-in gradient.
-        if (rangeStart > 0) {
-            states[0] = R.drawable.ic_swipe_circle_top;
-        }
-
-        // If more pages exist after the last page indicator, make it a fade-out gradient.
-        if (rangeEnd < pageCount - 1) {
-            states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom;
-        }
-
-        // Set the indicator of the selected page to be light.
-        states[page - rangeStart] = R.drawable.ic_swipe_circle_light;
-
-        return states;
-    }
-
-    /**
-     * Display the view that creates a new timer.
-     */
-    private void showCreateTimerView(int updateTypes) {
-        // Stop animating the timers.
-        stopUpdatingTime();
-
-        // Show the creation view; hide the timer view.
-        mTimersView.setVisibility(GONE);
-        mCreateTimerView.setVisibility(VISIBLE);
-
-        // Record the fact that the create view is visible.
-        mCurrentView = mCreateTimerView;
-
-        // Update the fab and buttons.
-        updateFab(updateTypes);
-    }
-
-    /**
-     * Display the view that lists all existing timers.
-     */
-    private void showTimersView(int updateTypes) {
-        // Clear any defunct timer creation state; the next timer creation starts fresh.
-        mTimerSetupState = null;
-
-        // Show the timer view; hide the creation view.
-        mTimersView.setVisibility(VISIBLE);
-        mCreateTimerView.setVisibility(GONE);
-
-        // Record the fact that the create view is visible.
-        mCurrentView = mTimersView;
-
-        // Update the fab and buttons.
-        updateFab(updateTypes);
-
-        // Start animating the timers.
-        startUpdatingTime();
-    }
-
-    /**
-     * @param timerToRemove the timer to be removed during the animation
-     */
-    private void animateTimerRemove(final Timer timerToRemove) {
-        final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
-
-        final Animator fadeOut = ObjectAnimator.ofFloat(mViewPager, ALPHA, 1, 0);
-        fadeOut.setDuration(duration);
-        fadeOut.setInterpolator(new DecelerateInterpolator());
-        fadeOut.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                DataModel.getDataModel().removeTimer(timerToRemove);
-                Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
-            }
-        });
-
-        final Animator fadeIn = ObjectAnimator.ofFloat(mViewPager, ALPHA, 0, 1);
-        fadeIn.setDuration(duration);
-        fadeIn.setInterpolator(new AccelerateInterpolator());
-
-        final AnimatorSet animatorSet = new AnimatorSet();
-        animatorSet.play(fadeOut).before(fadeIn);
-        animatorSet.start();
-    }
-
-    /**
-     * @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(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(BUTTONS_DISABLE);
-
-        final long animationDuration = UiDataModel.getUiDataModel().getLongAnimationDuration();
-
-        final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver();
-        viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-            @Override
-            public boolean onPreDraw() {
-                if (viewTreeObserver.isAlive()) {
-                    viewTreeObserver.removeOnPreDrawListener(this);
-                }
-
-                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;
-            }
-        });
-    }
-
-    private boolean hasTimers() {
-        return mAdapter.getCount() > 0;
-    }
-
-    private Timer getTimer() {
-        if (mViewPager == null) {
-            return null;
-        }
-
-        return mAdapter.getCount() == 0 ? null : mAdapter.getTimer(mViewPager.getCurrentItem());
-    }
-
-    private void startUpdatingTime() {
-        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
-        stopUpdatingTime();
-        mViewPager.post(mTimeUpdateRunnable);
-    }
-
-    private void stopUpdatingTime() {
-        mViewPager.removeCallbacks(mTimeUpdateRunnable);
-    }
-
-    /**
-     * Periodically refreshes the state of each timer.
-     */
-    private class TimeUpdateRunnable implements Runnable {
-        @Override
-        public void run() {
-            final long startTime = SystemClock.elapsedRealtime();
-            // If no timers require continuous updates, avoid scheduling the next update.
-            if (!mAdapter.updateTime()) {
-                return;
-            }
-            final long endTime = SystemClock.elapsedRealtime();
-
-            // Try to maintain a consistent period of time between redraws.
-            final long delay = Math.max(0, startTime + 20 - endTime);
-            mTimersView.postDelayed(this, delay);
-        }
-    }
-
-    /**
-     * Update the page indicators and fab in response to a new timer becoming visible.
-     */
-    private class TimerPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
-        @Override
-        public void onPageSelected(int position) {
-            updatePageIndicators();
-            updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-
-            // Showing a new timer page may introduce a timer requiring continuous updates.
-            startUpdatingTime();
-        }
-
-        @Override
-        public void onPageScrollStateChanged(int state) {
-            // Teasing a neighboring timer may introduce a timer requiring continuous updates.
-            if (state == ViewPager.SCROLL_STATE_DRAGGING) {
-                startUpdatingTime();
-            }
-        }
-    }
-
-    /**
-     * Update the page indicators in response to timers being added or removed.
-     * Update the fab in response to the visible timer changing.
-     */
-    private class TimerWatcher implements TimerListener {
-        @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
-        public void timerUpdated(Timer before, Timer after) {
-            // If the timer started, animate the timers.
-            if (before.isReset() && !after.isReset()) {
-                startUpdatingTime();
-            }
-
-            // Fetch the index of the change.
-            final int index = DataModel.getDataModel().getTimers().indexOf(after);
-
-            // If the timer just expired but is not displayed, display it now.
-            if (!before.isExpired() && after.isExpired() && index != mViewPager.getCurrentItem()) {
-                mViewPager.setCurrentItem(index, true);
-
-            } else if (mCurrentView == mTimersView && index == mViewPager.getCurrentItem()) {
-                // 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 && mAdapter.getCount() == 0) {
-                animateToView(mCreateTimerView, null, false);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerFragment.kt b/src/com/android/deskclock/timer/TimerFragment.kt
new file mode 100644
index 0000000..883c901
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerFragment.kt
@@ -0,0 +1,757 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import android.widget.Button
+import android.widget.ImageView
+import androidx.annotation.VisibleForTesting
+import androidx.viewpager.widget.ViewPager
+
+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
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.DeskClock
+import com.android.deskclock.DeskClockFragment
+import com.android.deskclock.FabContainer
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.io.Serializable
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Displays a vertical list of timers in all states.
+ */
+class TimerFragment : DeskClockFragment(UiDataModel.Tab.TIMERS) {
+    /** Notified when the user swipes vertically to change the visible timer.  */
+    private val mTimerPageChangeListener = TimerPageChangeListener()
+
+    /** Scheduled to update the timers while at least one is running.  */
+    private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
+
+    /** Updates the [.mPageIndicators] in response to timers being added or removed.  */
+    private val mTimerWatcher: TimerListener = TimerWatcher()
+
+    private lateinit var mCreateTimerView: TimerSetupView
+    private lateinit var mViewPager: ViewPager
+    private lateinit var mAdapter: TimerPagerAdapter
+    private var mTimersView: View? = null
+    private var mCurrentView: View? = null
+    private lateinit var mPageIndicators: Array<ImageView>
+
+    private var mTimerSetupState: Serializable? = null
+
+    /** `true` while this fragment is creating a new timer; `false` otherwise.  */
+    private var mCreatingTimer = false
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val view = inflater.inflate(R.layout.timer_fragment, container, false)
+
+        mAdapter = TimerPagerAdapter(parentFragmentManager)
+        mViewPager = view.findViewById<View>(R.id.vertical_view_pager) as ViewPager
+        mViewPager.setAdapter(mAdapter)
+        mViewPager.addOnPageChangeListener(mTimerPageChangeListener)
+
+        mTimersView = view.findViewById(R.id.timer_view)
+        mCreateTimerView = view.findViewById<View>(R.id.timer_setup) as TimerSetupView
+        mCreateTimerView.setFabContainer(this)
+        mPageIndicators = arrayOf(
+                view.findViewById<View>(R.id.page_indicator0) as ImageView,
+                view.findViewById<View>(R.id.page_indicator1) as ImageView,
+                view.findViewById<View>(R.id.page_indicator2) as ImageView,
+                view.findViewById<View>(R.id.page_indicator3) as ImageView
+        )
+
+        DataModel.dataModel.addTimerListener(mAdapter)
+        DataModel.dataModel.addTimerListener(mTimerWatcher)
+
+        // If timer setup state is present, retrieve it to be later honored.
+        savedInstanceState?.let {
+            mTimerSetupState = it.getSerializable(KEY_TIMER_SETUP_STATE)
+        }
+
+        return view
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        // Initialize the page indicators.
+        updatePageIndicators()
+        var createTimer = false
+        var showTimerId = -1
+
+        // Examine the intent of the parent activity to determine which view to display.
+        val intent = requireActivity().intent
+        intent?.let {
+            // These extras are single-use; remove them after honoring them.
+            createTimer = it.getBooleanExtra(EXTRA_TIMER_SETUP, false)
+            it.removeExtra(EXTRA_TIMER_SETUP)
+
+            showTimerId = it.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
+            it.removeExtra(TimerService.EXTRA_TIMER_ID)
+        }
+
+        // Choose the view to display in this fragment.
+        if (showTimerId != -1) {
+            // A specific timer must be shown; show the list of timers.
+            showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+        } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
+            // No timers exist, a timer is being created, or the last view was timer setup;
+            // show the timer setup view.
+            showCreateTimerView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+            if (mTimerSetupState != null) {
+                mCreateTimerView.state = mTimerSetupState
+                mTimerSetupState = null
+            }
+        } else {
+            // Otherwise, default to showing the list of timers.
+            showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+        }
+
+        // If the intent did not specify a timer to show, show the last timer that expired.
+        if (showTimerId == -1) {
+            val timer: Timer? = DataModel.dataModel.mostRecentExpiredTimer
+            showTimerId = timer?.id ?: -1
+        }
+
+        // If a specific timer should be displayed, display the corresponding timer tab.
+        if (showTimerId != -1) {
+            val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
+            timer?.let {
+                val index: Int = DataModel.dataModel.timers.indexOf(it)
+                mViewPager.setCurrentItem(index)
+            }
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        // We may have received a new intent while paused.
+        val intent = requireActivity().intent
+        if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
+            // This extra is single-use; remove after honoring it.
+            val showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
+            intent.removeExtra(TimerService.EXTRA_TIMER_ID)
+
+            val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
+            timer?.let {
+                // A specific timer must be shown; show the list of timers.
+                val index: Int = DataModel.dataModel.timers.indexOf(it)
+                mViewPager.setCurrentItem(index)
+
+                animateToView(mTimersView, null, false)
+            }
+        }
+    }
+
+    override fun onStop() {
+        super.onStop()
+
+        // Stop updating the timers when this fragment is no longer visible.
+        stopUpdatingTime()
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+
+        DataModel.dataModel.removeTimerListener(mAdapter)
+        DataModel.dataModel.removeTimerListener(mTimerWatcher)
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+
+        // If the timer creation view is visible, store the input for later restoration.
+        if (mCurrentView === mCreateTimerView) {
+            mTimerSetupState = mCreateTimerView.state
+            outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState)
+        }
+    }
+
+    private fun updateFab(fab: ImageView, animate: Boolean) {
+        if (mCurrentView === mTimersView) {
+            val timer = timer
+            if (timer == null) {
+                fab.visibility = View.INVISIBLE
+                return
+            }
+
+            fab.visibility = View.VISIBLE
+            when (timer.state) {
+                Timer.State.RUNNING -> {
+                    if (animate) {
+                        fab.setImageResource(R.drawable.ic_play_pause_animation)
+                    } else {
+                        fab.setImageResource(R.drawable.ic_play_pause)
+                    }
+                    fab.contentDescription = fab.resources.getString(R.string.timer_stop)
+                }
+                Timer.State.RESET -> {
+                    if (animate) {
+                        fab.setImageResource(R.drawable.ic_stop_play_animation)
+                    } else {
+                        fab.setImageResource(R.drawable.ic_pause_play)
+                    }
+                    fab.contentDescription = fab.resources.getString(R.string.timer_start)
+                }
+                Timer.State.PAUSED -> {
+                    if (animate) {
+                        fab.setImageResource(R.drawable.ic_pause_play_animation)
+                    } else {
+                        fab.setImageResource(R.drawable.ic_pause_play)
+                    }
+                    fab.contentDescription = fab.resources.getString(R.string.timer_start)
+                }
+                Timer.State.MISSED, Timer.State.EXPIRED -> {
+                    fab.setImageResource(R.drawable.ic_stop_white_24dp)
+                    fab.contentDescription = fab.resources.getString(R.string.timer_stop)
+                }
+            }
+        } else if (mCurrentView === mCreateTimerView) {
+            if (mCreateTimerView.hasValidInput()) {
+                fab.setImageResource(R.drawable.ic_start_white_24dp)
+                fab.contentDescription = fab.resources.getString(R.string.timer_start)
+                fab.visibility = View.VISIBLE
+            } else {
+                fab.contentDescription = null
+                fab.visibility = View.INVISIBLE
+            }
+        }
+    }
+
+    override fun onUpdateFab(fab: ImageView) {
+        updateFab(fab, false)
+    }
+
+    override fun onMorphFab(fab: ImageView) {
+        // Update the fab's drawable to match the current timer state.
+        updateFab(fab, Utils.isNOrLater)
+        // Animate the drawable.
+        AnimatorUtils.startDrawableAnimation(fab)
+    }
+
+    override fun onUpdateFabButtons(left: Button, right: Button) {
+        if (mCurrentView === mTimersView) {
+            left.isClickable = true
+            left.setText(R.string.timer_delete)
+            left.contentDescription = left.resources.getString(R.string.timer_delete)
+            left.visibility = View.VISIBLE
+
+            right.isClickable = true
+            right.setText(R.string.timer_add_timer)
+            right.contentDescription = right.resources.getString(R.string.timer_add_timer)
+            right.visibility = View.VISIBLE
+        } else if (mCurrentView === mCreateTimerView) {
+            left.isClickable = true
+            left.setText(R.string.timer_cancel)
+            left.contentDescription = left.resources.getString(R.string.timer_cancel)
+            // If no timers yet exist, the user is forced to create the first one.
+            left.visibility = if (hasTimers()) View.VISIBLE else View.INVISIBLE
+
+            right.visibility = View.INVISIBLE
+        }
+    }
+
+    override fun onFabClick(fab: ImageView) {
+        if (mCurrentView === mTimersView) {
+            // If no timer is currently showing a fab action is meaningless.
+            val timer = timer ?: return
+
+            val context = fab.context
+            val currentTime: Long = timer.remainingTime
+
+            when (timer.state) {
+                Timer.State.RUNNING -> {
+                    DataModel.dataModel.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))
+                    }
+                }
+                Timer.State.PAUSED, Timer.State.RESET -> {
+                    DataModel.dataModel.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))
+                    }
+                }
+                Timer.State.MISSED, Timer.State.EXPIRED -> {
+                    DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock)
+                }
+            }
+        } else if (mCurrentView === mCreateTimerView) {
+            mCreatingTimer = true
+            try {
+                // Create the new timer.
+                val timerLength: Long = mCreateTimerView.timeInMillis
+                val timer: Timer = DataModel.dataModel.addTimer(timerLength, "", false)
+                Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock)
+
+                // Start the new timer.
+                DataModel.dataModel.startTimer(timer)
+                Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock)
+
+                // Display the freshly created timer view.
+                mViewPager.setCurrentItem(0)
+            } finally {
+                mCreatingTimer = false
+            }
+
+            // Return to the list of timers.
+            animateToView(mTimersView, null, true)
+        }
+    }
+
+    override fun onLeftButtonClick(left: Button) {
+        if (mCurrentView === mTimersView) {
+            // Clicking the "delete" button.
+            val timer = timer ?: return
+
+            if (mAdapter.getCount() > 1) {
+                animateTimerRemove(timer)
+            } else {
+                animateToView(mCreateTimerView, timer, false)
+            }
+
+            left.announceForAccessibility(requireActivity().getString(R.string.timer_deleted))
+        } else if (mCurrentView === mCreateTimerView) {
+            // Clicking the "cancel" button on the timer creation page returns to the timers list.
+            mCreateTimerView.reset()
+
+            animateToView(mTimersView, null, false)
+
+            left.announceForAccessibility(requireActivity().getString(R.string.timer_canceled))
+        }
+    }
+
+    override fun onRightButtonClick(right: Button) {
+        if (mCurrentView !== mCreateTimerView) {
+            animateToView(mCreateTimerView, null, true)
+        }
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+        return if (mCurrentView === mCreateTimerView) {
+            mCreateTimerView.onKeyDown(keyCode, event)
+        } else super.onKeyDown(keyCode, event)
+    }
+
+    /**
+     * Updates the state of the page indicators so they reflect the selected page in the context of
+     * all pages.
+     */
+    private fun updatePageIndicators() {
+        val page: Int = mViewPager.getCurrentItem()
+        val pageIndicatorCount = mPageIndicators.size
+        val pageCount = mAdapter.getCount()
+
+        val states = computePageIndicatorStates(page, pageIndicatorCount, pageCount)
+        for (i in states.indices) {
+            val state = states[i]
+            val pageIndicator = mPageIndicators[i]
+            if (state == 0) {
+                pageIndicator.visibility = View.GONE
+            } else {
+                pageIndicator.visibility = View.VISIBLE
+                pageIndicator.setImageResource(state)
+            }
+        }
+    }
+
+    /**
+     * Display the view that creates a new timer.
+     */
+    private fun showCreateTimerView(updateTypes: Int) {
+        // Stop animating the timers.
+        stopUpdatingTime()
+
+        // Show the creation view; hide the timer view.
+        mTimersView?.visibility = View.GONE
+        mCreateTimerView.visibility = View.VISIBLE
+
+        // Record the fact that the create view is visible.
+        mCurrentView = mCreateTimerView
+
+        // Update the fab and buttons.
+        updateFab(updateTypes)
+    }
+
+    /**
+     * Display the view that lists all existing timers.
+     */
+    private fun showTimersView(updateTypes: Int) {
+        // Clear any defunct timer creation state; the next timer creation starts fresh.
+        mTimerSetupState = null
+
+        // Show the timer view; hide the creation view.
+        mTimersView?.visibility = View.VISIBLE
+        mCreateTimerView.visibility = View.GONE
+
+        // Record the fact that the create view is visible.
+        mCurrentView = mTimersView
+
+        // Update the fab and buttons.
+        updateFab(updateTypes)
+
+        // Start animating the timers.
+        startUpdatingTime()
+    }
+
+    /**
+     * @param timerToRemove the timer to be removed during the animation
+     */
+    private fun animateTimerRemove(timerToRemove: Timer) {
+        val duration = UiDataModel.uiDataModel.shortAnimationDuration
+
+        val fadeOut: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 1f, 0f)
+        fadeOut.duration = duration
+        fadeOut.interpolator = DecelerateInterpolator()
+        fadeOut.addListener(object : AnimatorListenerAdapter() {
+            override fun onAnimationEnd(animation: Animator) {
+                DataModel.dataModel.removeTimer(timerToRemove)
+                Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock)
+            }
+        })
+
+        val fadeIn: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 0f, 1f)
+        fadeIn.duration = duration
+        fadeIn.interpolator = AccelerateInterpolator()
+
+        val animatorSet = AnimatorSet()
+        animatorSet.play(fadeOut).before(fadeIn)
+        animatorSet.start()
+    }
+
+    /**
+     * @param toView one of [.mTimersView] or [.mCreateTimerView]
+     * @param timerToRemove the timer to be removed during the animation; `null` if no timer
+     * should be removed
+     * @param animateDown `true` if the views should animate upwards, otherwise downwards
+     */
+    private fun animateToView(
+        toView: View?,
+        timerToRemove: Timer?,
+        animateDown: Boolean
+    ) {
+        if (mCurrentView === toView) {
+            return
+        }
+
+        val toTimers = toView === mTimersView
+        if (toTimers) {
+            mTimersView?.visibility = View.VISIBLE
+        } else {
+            mCreateTimerView.visibility = View.VISIBLE
+        }
+        // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
+        updateFab(FabContainer.BUTTONS_DISABLE)
+
+        val animationDuration = UiDataModel.uiDataModel.longAnimationDuration
+
+        val viewTreeObserver = toView!!.viewTreeObserver
+        viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
+            override fun onPreDraw(): Boolean {
+                if (viewTreeObserver.isAlive) {
+                    viewTreeObserver.removeOnPreDrawListener(this)
+                }
+
+                val view = mTimersView?.findViewById<View>(R.id.timer_time)
+                val distanceY: Float = if (view != null) view.height + view.y else 0f
+                val translationDistance = if (animateDown) distanceY else -distanceY
+
+                toView.translationY = -translationDistance
+                mCurrentView?.translationY = 0f
+                toView.alpha = 0f
+                mCurrentView?.alpha = 1f
+
+                val translateCurrent: Animator = ObjectAnimator.ofFloat(mCurrentView,
+                        View.TRANSLATION_Y, translationDistance)
+                val translateNew: Animator = ObjectAnimator.ofFloat(toView, View.TRANSLATION_Y, 0f)
+                val translationAnimatorSet = AnimatorSet()
+                translationAnimatorSet.playTogether(translateCurrent, translateNew)
+                translationAnimatorSet.duration = animationDuration
+                translationAnimatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+                val fadeOutAnimator: Animator = ObjectAnimator.ofFloat(mCurrentView, View.ALPHA, 0f)
+                fadeOutAnimator.duration = animationDuration / 2
+                fadeOutAnimator.addListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationStart(animation: Animator) {
+                        super.onAnimationStart(animation)
+
+                        // The fade-out animation and fab-shrinking animation should run together.
+                        updateFab(FabContainer.FAB_AND_BUTTONS_SHRINK)
+                    }
+
+                    override fun onAnimationEnd(animation: Animator) {
+                        super.onAnimationEnd(animation)
+                        if (toTimers) {
+                            showTimersView(FabContainer.FAB_AND_BUTTONS_EXPAND)
+
+                            // Reset the state of the create view.
+                            mCreateTimerView.reset()
+                        } else {
+                            showCreateTimerView(FabContainer.FAB_AND_BUTTONS_EXPAND)
+                        }
+                        if (timerToRemove != null) {
+                            DataModel.dataModel.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(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+                    }
+                })
+
+                val fadeInAnimator: Animator = ObjectAnimator.ofFloat(toView, View.ALPHA, 1f)
+                fadeInAnimator.duration = animationDuration / 2
+                fadeInAnimator.startDelay = animationDuration / 2
+
+                val animatorSet = AnimatorSet()
+                animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet)
+                animatorSet.addListener(object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        super.onAnimationEnd(animation)
+                        mTimersView?.translationY = 0f
+                        mCreateTimerView.translationY = 0f
+                        mTimersView?.alpha = 1f
+                        mCreateTimerView.alpha = 1f
+                    }
+                })
+                animatorSet.start()
+
+                return true
+            }
+        })
+    }
+
+    private fun hasTimers(): Boolean {
+        return mAdapter.getCount() > 0
+    }
+
+    private val timer: Timer?
+        get() {
+            if (!::mViewPager.isInitialized) {
+                return null
+            }
+
+            return if (mAdapter.getCount() == 0) {
+                null
+            } else {
+                mAdapter.getTimer(mViewPager.getCurrentItem())
+            }
+        }
+
+    private fun startUpdatingTime() {
+        // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
+        stopUpdatingTime()
+        mViewPager.post(mTimeUpdateRunnable)
+    }
+
+    private fun stopUpdatingTime() {
+        mViewPager.removeCallbacks(mTimeUpdateRunnable)
+    }
+
+    /**
+     * Periodically refreshes the state of each timer.
+     */
+    private inner class TimeUpdateRunnable : Runnable {
+        override fun run() {
+            val startTime = SystemClock.elapsedRealtime()
+            // If no timers require continuous updates, avoid scheduling the next update.
+            if (!mAdapter.updateTime()) {
+                return
+            }
+            val endTime = SystemClock.elapsedRealtime()
+
+            // Try to maintain a consistent period of time between redraws.
+            val delay = max(0, startTime + 20 - endTime)
+            mTimersView?.postDelayed(this, delay)
+        }
+    }
+
+    /**
+     * Update the page indicators and fab in response to a new timer becoming visible.
+     */
+    private inner class TimerPageChangeListener : ViewPager.SimpleOnPageChangeListener() {
+        override fun onPageSelected(position: Int) {
+            updatePageIndicators()
+            updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+            // Showing a new timer page may introduce a timer requiring continuous updates.
+            startUpdatingTime()
+        }
+
+        override fun onPageScrollStateChanged(state: Int) {
+            // Teasing a neighboring timer may introduce a timer requiring continuous updates.
+            if (state == ViewPager.SCROLL_STATE_DRAGGING) {
+                startUpdatingTime()
+            }
+        }
+    }
+
+    /**
+     * Update the page indicators in response to timers being added or removed.
+     * Update the fab in response to the visible timer changing.
+     */
+    private inner class TimerWatcher : TimerListener {
+        override fun 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(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+            }
+        }
+
+        override fun timerUpdated(before: Timer, after: Timer) {
+            // If the timer started, animate the timers.
+            if (before.isReset && !after.isReset) {
+                startUpdatingTime()
+            }
+
+            // Fetch the index of the change.
+            val index: Int = DataModel.dataModel.timers.indexOf(after)
+
+            // If the timer just expired but is not displayed, display it now.
+            if (!before.isExpired && after.isExpired && index != mViewPager.getCurrentItem()) {
+                mViewPager.setCurrentItem(index, true)
+            } else if (mCurrentView === mTimersView && index == mViewPager.getCurrentItem()) {
+                // Morph the fab from its old state to new state if necessary.
+                if (before.state != after.state &&
+                        !(before.isPaused && after.isReset)) {
+                    updateFab(FabContainer.FAB_MORPH)
+                }
+            }
+        }
+
+        override fun timerRemoved(timer: Timer) {
+            updatePageIndicators()
+            updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+            if (mCurrentView === mTimersView && mAdapter.getCount() == 0) {
+                animateToView(mCreateTimerView, null, false)
+            }
+        }
+    }
+
+    companion object {
+        private const val EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP"
+
+        private const val KEY_TIMER_SETUP_STATE = "timer_setup_input"
+
+        /**
+         * @return an Intent that selects the timers tab with the
+         * setup screen for a new timer in place.
+         */
+        @VisibleForTesting
+        @JvmStatic
+        fun createTimerSetupIntent(context: Context): Intent {
+            return Intent(context, DeskClock::class.java).putExtra(EXTRA_TIMER_SETUP, true)
+        }
+
+        /**
+         * @param page the selected page; value between 0 and `pageCount`
+         * @param pageIndicatorCount the number of indicators displaying the `page` location
+         * @param pageCount the number of pages that exist
+         * @return an array of length `pageIndicatorCount` specifying which image to display for
+         * each page indicator or 0 if the page indicator should be hidden
+         */
+        @VisibleForTesting
+        @JvmStatic
+        fun computePageIndicatorStates(
+            page: Int,
+            pageIndicatorCount: Int,
+            pageCount: Int
+        ): IntArray {
+            // Compute the number of page indicators that will be visible.
+            val rangeSize = min(pageIndicatorCount, pageCount)
+
+            // Compute the inclusive range of pages to indicate centered around the selected page.
+            var rangeStart = page - rangeSize / 2
+            var rangeEnd = rangeStart + rangeSize - 1
+
+            // Clamp the range of pages if they extend beyond the last page.
+            if (rangeEnd >= pageCount) {
+                rangeEnd = pageCount - 1
+                rangeStart = rangeEnd - rangeSize + 1
+            }
+
+            // Clamp the range of pages if they extend beyond the first page.
+            if (rangeStart < 0) {
+                rangeStart = 0
+                rangeEnd = rangeSize - 1
+            }
+
+            // Build the result with all page indicators initially hidden.
+            val states = IntArray(pageIndicatorCount)
+            states.fill(0)
+
+            // If 0 or 1 total pages exist, all page indicators must remain hidden.
+            if (rangeSize < 2) {
+                return states
+            }
+
+            // Initialize the visible page indicators to be dark.
+            states.fill(R.drawable.ic_swipe_circle_dark, 0, rangeSize)
+
+            // If more pages exist before the first page indicator, make it a fade-in gradient.
+            if (rangeStart > 0) {
+                states[0] = R.drawable.ic_swipe_circle_top
+            }
+
+            // If more pages exist after the last page indicator, make it a fade-out gradient.
+            if (rangeEnd < pageCount - 1) {
+                states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom
+            }
+
+            // Set the indicator of the selected page to be light.
+            states[page - rangeStart] = R.drawable.ic_swipe_circle_light
+            return states
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerItem.java b/src/com/android/deskclock/timer/TimerItem.java
deleted file mode 100644
index d122d77..0000000
--- a/src/com/android/deskclock/timer/TimerItem.java
+++ /dev/null
@@ -1,156 +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.timer;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.os.SystemClock;
-import androidx.core.view.ViewCompat;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-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 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 Button mResetAddButton;
-
-    /** Displays the label associated with the timer. Tapping it presents an edit dialog. */
-    private TextView mLabelView;
-
-    /** The last state of the timer that was rendered; used to avoid expensive operations. */
-    private Timer.State mLastState;
-
-    public TimerItem(Context context) {
-        this(context, null);
-    }
-
-    public TimerItem(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mLabelView = (TextView) findViewById(R.id.timer_label);
-        mResetAddButton = (Button) findViewById(R.id.reset_add);
-        mCircleView = (TimerCircleView) findViewById(R.id.timer_time);
-        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 }));
-    }
-
-    /**
-     * Updates this view to display the latest state of the {@code timer}.
-     */
-    void update(Timer timer) {
-        // Update the time.
-        mTimerTextController.setTimeString(timer.getRemainingTime());
-
-        // Update the label if it changed.
-        final String label = timer.getLabel();
-        if (!TextUtils.equals(label, mLabelView.getText())) {
-            mLabelView.setText(label);
-        }
-
-        // Update visibility of things that may blink.
-        final boolean blinkOff = SystemClock.elapsedRealtime() % 1000 < 500;
-        if (mCircleView != null) {
-            final boolean hideCircle = (timer.isExpired() || timer.isMissed()) && blinkOff;
-            mCircleView.setVisibility(hideCircle ? INVISIBLE : VISIBLE);
-
-            if (!hideCircle) {
-                // Update the progress of the circle.
-                mCircleView.update(timer);
-            }
-        }
-        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:
-                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 = context.getString(R.string.timer_plus_one);
-                    mResetAddButton.setText(R.string.timer_add_minute);
-                    mResetAddButton.setContentDescription(addTimeDesc);
-                    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 EXPIRED:
-                case MISSED: {
-                    final String addTimeDesc = context.getString(R.string.timer_plus_one);
-                    mResetAddButton.setText(R.string.timer_add_minute);
-                    mResetAddButton.setContentDescription(addTimeDesc);
-                    mTimerText.setClickable(false);
-                    mTimerText.setActivated(true);
-                    mTimerText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
-                    break;
-                }
-            }
-        }
-    }
-}
diff --git a/src/com/android/deskclock/timer/TimerItem.kt b/src/com/android/deskclock/timer/TimerItem.kt
new file mode 100644
index 0000000..9cdcca4
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerItem.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.R.attr
+import android.content.Context
+import android.content.res.ColorStateList
+import android.os.SystemClock
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.View
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.ViewCompat
+
+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
+
+/**
+ * This view is a visual representation of a [Timer].
+ */
+class TimerItem @JvmOverloads constructor(
+    context: Context?,
+    attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+    /** Displays the remaining time or time since expiration.  */
+    private lateinit var mTimerText: TextView
+
+    /** Formats and displays the text in the timer.  */
+    private lateinit var mTimerTextController: TimerTextController
+
+    /** Displays timer progress as a color circle that changes from white to red.  */
+    private var mCircleView: TimerCircleView? = null
+
+    /** A button that either resets the timer or adds time to it, depending on its state.  */
+    private lateinit var mResetAddButton: Button
+
+    /** Displays the label associated with the timer. Tapping it presents an edit dialog.  */
+    private lateinit var mLabelView: TextView
+
+    /** The last state of the timer that was rendered; used to avoid expensive operations.  */
+    private var mLastState: Timer.State? = null
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        mLabelView = findViewById<View>(R.id.timer_label) as TextView
+        mResetAddButton = findViewById<View>(R.id.reset_add) as Button
+        mCircleView = findViewById<View>(R.id.timer_time) as TimerCircleView
+        mTimerText = findViewById<View>(R.id.timer_time_text) as TextView
+        mTimerTextController = TimerTextController(mTimerText)
+
+        val c = mTimerText.context
+        val colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent)
+        val textColorPrimary = ThemeUtils.resolveColor(c, attr.textColorPrimary)
+        mTimerText.setTextColor(ColorStateList(
+                arrayOf(intArrayOf(-attr.state_activated, -attr.state_pressed),
+                intArrayOf()),
+                intArrayOf(textColorPrimary, colorAccent)))
+    }
+
+    /**
+     * Updates this view to display the latest state of the `timer`.
+     */
+    fun update(timer: Timer) {
+        // Update the time.
+        mTimerTextController.setTimeString(timer.remainingTime)
+
+        // Update the label if it changed.
+        val label: String? = timer.label
+        if (!TextUtils.equals(label, mLabelView.text)) {
+            mLabelView.text = label
+        }
+
+        // Update visibility of things that may blink.
+        val blinkOff = SystemClock.elapsedRealtime() % 1000 < 500
+        if (mCircleView != null) {
+            val hideCircle = (timer.isExpired || timer.isMissed) && blinkOff
+            mCircleView!!.visibility = if (hideCircle) View.INVISIBLE else View.VISIBLE
+
+            if (!hideCircle) {
+                // Update the progress of the circle.
+                mCircleView!!.update(timer)
+            }
+        }
+        if (!timer.isPaused || !blinkOff || mTimerText.isPressed) {
+            mTimerText.alpha = 1f
+        } else {
+            mTimerText.alpha = 0f
+        }
+
+        // Update some potentially expensive areas of the user interface only on state changes.
+        if (timer.state != mLastState) {
+            mLastState = timer.state
+            val context = context
+            when (mLastState) {
+                Timer.State.RESET, Timer.State.PAUSED -> {
+                    mResetAddButton.setText(R.string.timer_reset)
+                    mResetAddButton.contentDescription = null
+                    mTimerText.isClickable = true
+                    mTimerText.isActivated = false
+                    mTimerText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+                    ViewCompat.setAccessibilityDelegate(mTimerText, ClickAccessibilityDelegate(
+                            context.getString(R.string.timer_start), true))
+                }
+                Timer.State.RUNNING -> {
+                    val addTimeDesc = context.getString(R.string.timer_plus_one)
+                    mResetAddButton.setText(R.string.timer_add_minute)
+                    mResetAddButton.contentDescription = addTimeDesc
+                    mTimerText.isClickable = true
+                    mTimerText.isActivated = false
+                    mTimerText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+                    ViewCompat.setAccessibilityDelegate(mTimerText, ClickAccessibilityDelegate(
+                            context.getString(R.string.timer_pause)))
+                }
+                Timer.State.EXPIRED, Timer.State.MISSED -> {
+                    val addTimeDesc = context.getString(R.string.timer_plus_one)
+                    mResetAddButton.setText(R.string.timer_add_minute)
+                    mResetAddButton.contentDescription = addTimeDesc
+                    mTimerText.isClickable = false
+                    mTimerText.isActivated = true
+                    mTimerText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerItemFragment.java b/src/com/android/deskclock/timer/TimerItemFragment.java
deleted file mode 100644
index 7ce6876..0000000
--- a/src/com/android/deskclock/timer/TimerItemFragment.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.deskclock.timer;
-
-import android.app.Fragment;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.LabelDialogFragment;
-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 {
-
-    private static final String KEY_TIMER_ID = "KEY_TIMER_ID";
-    private int mTimerId;
-
-    /** The public no-arg constructor required by all fragments. */
-    public TimerItemFragment() {}
-
-    public static TimerItemFragment newInstance(Timer timer) {
-        final TimerItemFragment fragment = new TimerItemFragment();
-        final Bundle args = new Bundle();
-        args.putInt(KEY_TIMER_ID, timer.getId());
-        fragment.setArguments(args);
-        return fragment;
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        mTimerId = getArguments().getInt(KEY_TIMER_ID);
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container,
-            Bundle savedInstanceState) {
-        final Timer timer = getTimer();
-        if (timer == null) {
-            return null;
-        }
-
-        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;
-    }
-
-    /**
-     * @return {@code true} iff the timer is in a state that requires continuous updates
-     */
-    boolean updateTime() {
-        final TimerItem view = (TimerItem) getView();
-        if (view != null) {
-            final Timer timer = getTimer();
-            view.update(timer);
-            return !timer.isReset();
-        }
-
-        return false;
-    }
-
-    int getTimerId() {
-        return mTimerId;
-    }
-
-    Timer getTimer() {
-        return DataModel.getDataModel().getTimer(getTimerId());
-    }
-
-    private final class ResetAddListener implements View.OnClickListener {
-        @Override
-        public void onClick(View v) {
-            final Timer timer = getTimer();
-            if (timer.isPaused()) {
-                DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_deskclock);
-            } 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 final class EditLabelListener implements View.OnClickListener {
-        @Override
-        public void onClick(View v) {
-            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);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerItemFragment.kt b/src/com/android/deskclock/timer/TimerItemFragment.kt
new file mode 100644
index 0000000..52cfd60
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerItemFragment.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+
+import com.android.deskclock.LabelDialogFragment
+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
+
+/** The public no-arg constructor required by all fragments.  */
+class TimerItemFragment : Fragment() {
+    var timerId = 0
+        private set
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        timerId = requireArguments().getInt(KEY_TIMER_ID)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val timer = timer ?: return null
+
+        val view = inflater.inflate(R.layout.timer_item, container, false) as TimerItem
+        view.findViewById<View>(R.id.reset_add).setOnClickListener(ResetAddListener())
+        view.findViewById<View>(R.id.timer_label).setOnClickListener(EditLabelListener())
+        view.findViewById<View>(R.id.timer_time_text).setOnClickListener(TimeTextListener())
+        view.update(timer)
+
+        return view
+    }
+
+    /**
+     * @return `true` iff the timer is in a state that requires continuous updates
+     */
+    fun updateTime(): Boolean {
+        val view = view as TimerItem?
+        if (view != null) {
+            val timer = timer!!
+            view.update(timer)
+            return !timer.isReset
+        }
+
+        return false
+    }
+
+    val timer: Timer?
+        get() = DataModel.dataModel.getTimer(timerId)
+
+    private inner class ResetAddListener : View.OnClickListener {
+        override fun onClick(v: View) {
+            val timer = timer!!
+            if (timer.isPaused) {
+                DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock)
+            } else if (timer.isRunning || timer.isExpired || timer.isMissed) {
+                DataModel.dataModel.addTimerMinute(timer)
+                Events.sendTimerEvent(R.string.action_add_minute, R.string.label_deskclock)
+
+                val context = v.context
+                // Must re-retrieve timer because old timer is no longer accurate.
+                val currentTime: Long = this@TimerItemFragment.timer!!.remainingTime
+                if (currentTime > 0) {
+                    v.announceForAccessibility(TimerStringFormatter.formatString(
+                            context, R.string.timer_accessibility_one_minute_added, currentTime,
+                            true))
+                }
+            }
+        }
+    }
+
+    private inner class EditLabelListener : View.OnClickListener {
+        override fun onClick(v: View) {
+            val fragment = LabelDialogFragment.newInstance(timer!!)
+            LabelDialogFragment.show(parentFragmentManager, fragment)
+        }
+    }
+
+    private inner class TimeTextListener : View.OnClickListener {
+        override fun onClick(view: View) {
+            val clickedTimer = timer!!
+            if (clickedTimer.isPaused || clickedTimer.isReset) {
+                DataModel.dataModel.startTimer(clickedTimer)
+            } else if (clickedTimer.isRunning) {
+                DataModel.dataModel.pauseTimer(clickedTimer)
+            }
+        }
+    }
+
+    companion object {
+        private const val KEY_TIMER_ID = "KEY_TIMER_ID"
+
+        fun newInstance(timer: Timer): TimerItemFragment {
+            val fragment = TimerItemFragment()
+            val args = Bundle()
+            args.putInt(KEY_TIMER_ID, timer.id)
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerKlaxon.java b/src/com/android/deskclock/timer/TimerKlaxon.java
deleted file mode 100644
index 1c40b4a..0000000
--- a/src/com/android/deskclock/timer/TimerKlaxon.java
+++ /dev/null
@@ -1,98 +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.timer;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Vibrator;
-
-import com.android.deskclock.AsyncRingtonePlayer;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-/**
- * 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;
-    private static AsyncRingtonePlayer sAsyncRingtonePlayer;
-
-    private TimerKlaxon() {
-    }
-
-    public static void stop(Context context) {
-        if (sStarted) {
-            LogUtils.i("TimerKlaxon.stop()");
-            sStarted = false;
-            getAsyncRingtonePlayer(context).stop();
-            ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).cancel();
-        }
-    }
-
-    public static void start(Context context) {
-        // Make sure we are stopped before starting
-        stop(context);
-        LogUtils.i("TimerKlaxon.start()");
-
-        // Look up user-selected timer ringtone.
-        if (DataModel.getDataModel().isTimerRingtoneSilent()) {
-            // Special case: Silent ringtone
-            LogUtils.i("Playing silent ringtone for timer");
-        } else {
-            final Uri uri = DataModel.getDataModel().getTimerRingtoneUri();
-            final long crescendoDuration = DataModel.getDataModel().getTimerCrescendoDuration();
-            getAsyncRingtonePlayer(context).play(uri, crescendoDuration);
-        }
-
-        if (DataModel.getDataModel().getTimerVibrate()) {
-            final Vibrator vibrator = getVibrator(context);
-            if (Utils.isLOrLater()) {
-                vibrateLOrLater(vibrator);
-            } else {
-                vibrator.vibrate(VIBRATE_PATTERN, 0);
-            }
-        }
-        sStarted = true;
-    }
-
-    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    private static void vibrateLOrLater(Vibrator vibrator) {
-        vibrator.vibrate(VIBRATE_PATTERN, 0, new AudioAttributes.Builder()
-                .setUsage(AudioAttributes.USAGE_ALARM)
-                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                .build());
-    }
-
-    private static Vibrator getVibrator(Context context) {
-        return ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE));
-    }
-
-    private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
-        if (sAsyncRingtonePlayer == null) {
-            sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
-        }
-
-        return sAsyncRingtonePlayer;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerKlaxon.kt b/src/com/android/deskclock/timer/TimerKlaxon.kt
new file mode 100644
index 0000000..9d1fde5
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerKlaxon.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.TargetApi
+import android.content.Context
+import android.media.AudioAttributes
+import android.net.Uri
+import android.os.Build
+import android.os.Vibrator
+
+import com.android.deskclock.AsyncRingtonePlayer
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+/**
+ * Manages playing the timer ringtone and vibrating the device.
+ */
+object TimerKlaxon {
+    private val VIBRATE_PATTERN = longArrayOf(500, 500)
+
+    private var sStarted = false
+    private var sAsyncRingtonePlayer: AsyncRingtonePlayer? = null
+
+    @JvmStatic
+    fun stop(context: Context) {
+        if (sStarted) {
+            LogUtils.i("TimerKlaxon.stop()")
+            sStarted = false
+            getAsyncRingtonePlayer(context)!!.stop()
+            (context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).cancel()
+        }
+    }
+
+    @JvmStatic
+    fun start(context: Context) {
+        // Make sure we are stopped before starting
+        stop(context)
+        LogUtils.i("TimerKlaxon.start()")
+
+        // Look up user-selected timer ringtone.
+        if (DataModel.dataModel.isTimerRingtoneSilent) {
+            // Special case: Silent ringtone
+            LogUtils.i("Playing silent ringtone for timer")
+        } else {
+            val uri: Uri = DataModel.dataModel.timerRingtoneUri
+            val crescendoDuration: Long = DataModel.dataModel.timerCrescendoDuration
+            getAsyncRingtonePlayer(context)!!.play(uri, crescendoDuration)
+        }
+
+        if (DataModel.dataModel.timerVibrate) {
+            val vibrator = getVibrator(context)
+            if (Utils.isLOrLater) {
+                vibrateLOrLater(vibrator)
+            } else {
+                vibrator.vibrate(VIBRATE_PATTERN, 0)
+            }
+        }
+        sStarted = true
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun vibrateLOrLater(vibrator: Vibrator) {
+        vibrator.vibrate(VIBRATE_PATTERN, 0, AudioAttributes.Builder()
+                .setUsage(AudioAttributes.USAGE_ALARM)
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                .build())
+    }
+
+    private fun getVibrator(context: Context): Vibrator {
+        return context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+    }
+
+    @Synchronized
+    private fun getAsyncRingtonePlayer(context: Context): AsyncRingtonePlayer? {
+        if (sAsyncRingtonePlayer == null) {
+            sAsyncRingtonePlayer = AsyncRingtonePlayer(context.applicationContext)
+        }
+
+        return sAsyncRingtonePlayer
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerPagerAdapter.java b/src/com/android/deskclock/timer/TimerPagerAdapter.java
deleted file mode 100644
index 5255f16..0000000
--- a/src/com/android/deskclock/timer/TimerPagerAdapter.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.timer;
-
-import android.annotation.SuppressLint;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import androidx.legacy.app.FragmentCompat;
-import androidx.viewpager.widget.PagerAdapter;
-import android.util.ArrayMap;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.data.TimerListener;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * This adapter produces a {@link TimerItemFragment} for each timer.
- */
-class TimerPagerAdapter extends PagerAdapter implements TimerListener {
-
-    private final FragmentManager mFragmentManager;
-
-    /** Maps each timer id to the corresponding {@link TimerItemFragment} that draws it. */
-    private final Map<Integer, TimerItemFragment> mFragments = new ArrayMap<>();
-
-    /** The current fragment transaction in play or {@code null}. */
-    private FragmentTransaction mCurrentTransaction;
-
-    /** The {@link TimerItemFragment} that is current visible on screen. */
-    private Fragment mCurrentPrimaryItem;
-
-    public TimerPagerAdapter(FragmentManager fragmentManager) {
-        mFragmentManager = fragmentManager;
-    }
-
-    @Override
-    public int getCount() {
-        return getTimers().size();
-    }
-
-    @Override
-    public boolean isViewFromObject(View view, Object object) {
-        return ((Fragment) object).getView() == view;
-    }
-
-    @Override
-    public int getItemPosition(Object object) {
-        final TimerItemFragment fragment = (TimerItemFragment) object;
-        final Timer timer = fragment.getTimer();
-
-        final int position = getTimers().indexOf(timer);
-        return position == -1 ? POSITION_NONE : position;
-    }
-
-    @Override
-    @SuppressLint("CommitTransaction")
-    public Fragment instantiateItem(ViewGroup container, int position) {
-        if (mCurrentTransaction == null) {
-            mCurrentTransaction = mFragmentManager.beginTransaction();
-        }
-
-        final Timer timer = getTimers().get(position);
-
-        // Search for the existing fragment by tag.
-        final String tag = getClass().getSimpleName() + timer.getId();
-        TimerItemFragment fragment = (TimerItemFragment) mFragmentManager.findFragmentByTag(tag);
-
-        if (fragment != null) {
-            // Reattach the existing fragment.
-            mCurrentTransaction.attach(fragment);
-        } else {
-            // Create and add a new fragment.
-            fragment = TimerItemFragment.newInstance(timer);
-            mCurrentTransaction.add(container.getId(), fragment, tag);
-        }
-
-        if (fragment != mCurrentPrimaryItem) {
-            setItemVisible(fragment, false);
-        }
-
-        mFragments.put(timer.getId(), fragment);
-
-        return fragment;
-    }
-
-    @Override
-    @SuppressLint("CommitTransaction")
-    public void destroyItem(ViewGroup container, int position, Object object) {
-        final TimerItemFragment fragment = (TimerItemFragment) object;
-
-        if (mCurrentTransaction == null) {
-            mCurrentTransaction = mFragmentManager.beginTransaction();
-        }
-
-        mFragments.remove(fragment.getTimerId());
-        mCurrentTransaction.remove(fragment);
-    }
-
-    @Override
-    public void setPrimaryItem(ViewGroup container, int position, Object object) {
-        final Fragment fragment = (Fragment) object;
-        if (fragment != mCurrentPrimaryItem) {
-            if (mCurrentPrimaryItem != null) {
-                setItemVisible(mCurrentPrimaryItem, false);
-            }
-
-            mCurrentPrimaryItem = fragment;
-
-            if (mCurrentPrimaryItem != null) {
-                setItemVisible(mCurrentPrimaryItem, true);
-            }
-        }
-    }
-
-    @Override
-    public void finishUpdate(ViewGroup container) {
-        if (mCurrentTransaction != null) {
-            mCurrentTransaction.commitAllowingStateLoss();
-            mCurrentTransaction = null;
-            mFragmentManager.executePendingTransactions();
-        }
-    }
-
-    @Override
-    public void timerAdded(Timer timer) {
-        notifyDataSetChanged();
-    }
-
-    @Override
-    public void timerRemoved(Timer timer) {
-        notifyDataSetChanged();
-    }
-
-    @Override
-    public void timerUpdated(Timer before, Timer after) {
-        final TimerItemFragment timerItemFragment = mFragments.get(after.getId());
-        if (timerItemFragment != null) {
-            timerItemFragment.updateTime();
-        }
-    }
-
-    /**
-     * @return {@code true} if at least one timer is in a state requiring continuous updates
-     */
-    boolean updateTime() {
-        boolean continuousUpdates = false;
-        for (TimerItemFragment fragment : mFragments.values()) {
-            continuousUpdates |= fragment.updateTime();
-        }
-        return continuousUpdates;
-    }
-
-    Timer getTimer(int index) {
-        return getTimers().get(index);
-    }
-
-    private List<Timer> getTimers() {
-        return DataModel.getDataModel().getTimers();
-    }
-
-    private static void setItemVisible(Fragment item, boolean visible) {
-        FragmentCompat.setMenuVisibility(item, visible);
-        FragmentCompat.setUserVisibleHint(item, visible);
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerPagerAdapter.kt b/src/com/android/deskclock/timer/TimerPagerAdapter.kt
new file mode 100644
index 0000000..fa49933
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerPagerAdapter.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.SuppressLint
+import android.util.ArrayMap
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.viewpager.widget.PagerAdapter
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.data.TimerListener
+
+/**
+ * This adapter produces a [TimerItemFragment] for each timer.
+ */
+internal class TimerPagerAdapter(
+    private val mFragmentManager: FragmentManager
+) : PagerAdapter(), TimerListener {
+
+    /** Maps each timer id to the corresponding [TimerItemFragment] that draws it.  */
+    private val mFragments: MutableMap<Int, TimerItemFragment?> = ArrayMap()
+
+    /** The current fragment transaction in play or `null`.  */
+    private var mCurrentTransaction: FragmentTransaction? = null
+
+    /** The [TimerItemFragment] that is current visible on screen.  */
+    private var mCurrentPrimaryItem: Fragment? = null
+
+    override fun getCount(): Int = timers.size
+
+    override fun isViewFromObject(view: View, any: Any): Boolean {
+        return (any as Fragment).view === view
+    }
+
+    override fun getItemPosition(any: Any): Int {
+        val fragment = any as TimerItemFragment
+        val timer = fragment.timer
+
+        val position = timers.indexOf(timer)
+        return if (position == -1) POSITION_NONE else position
+    }
+
+    @SuppressLint("CommitTransaction")
+    override fun instantiateItem(container: ViewGroup, position: Int): Fragment {
+        if (mCurrentTransaction == null) {
+            mCurrentTransaction = mFragmentManager.beginTransaction()
+        }
+
+        val timer = timers[position]
+
+        // Search for the existing fragment by tag.
+        val tag = javaClass.simpleName + timer.id
+        var fragment = mFragmentManager.findFragmentByTag(tag) as TimerItemFragment?
+
+        if (fragment != null) {
+            // Reattach the existing fragment.
+            mCurrentTransaction!!.attach(fragment)
+        } else {
+            // Create and add a new fragment.
+            fragment = TimerItemFragment.newInstance(timer)
+            mCurrentTransaction!!.add(container.id, fragment, tag)
+        }
+
+        if (fragment !== mCurrentPrimaryItem) {
+            setItemVisible(fragment, false)
+        }
+
+        mFragments[timer.id] = fragment
+
+        return fragment
+    }
+
+    @SuppressLint("CommitTransaction")
+    override fun destroyItem(container: ViewGroup, position: Int, any: Any) {
+        val fragment = any as TimerItemFragment
+
+        if (mCurrentTransaction == null) {
+            mCurrentTransaction = mFragmentManager.beginTransaction()
+        }
+
+        mFragments.remove(fragment.timerId)
+        mCurrentTransaction!!.remove(fragment)
+    }
+
+    override fun setPrimaryItem(container: ViewGroup, position: Int, any: Any) {
+        val fragment = any as Fragment
+        if (fragment !== mCurrentPrimaryItem) {
+            mCurrentPrimaryItem?.let {
+                setItemVisible(it, false)
+            }
+
+            mCurrentPrimaryItem = fragment
+
+            mCurrentPrimaryItem?.let {
+                setItemVisible(it, true)
+            }
+        }
+    }
+
+    override fun finishUpdate(container: ViewGroup) {
+        if (mCurrentTransaction != null) {
+            mCurrentTransaction!!.commitAllowingStateLoss()
+            mCurrentTransaction = null
+
+            if (!mFragmentManager.isDestroyed) {
+                mFragmentManager.executePendingTransactions()
+            }
+        }
+    }
+
+    override fun timerAdded(timer: Timer) {
+        notifyDataSetChanged()
+    }
+
+    override fun timerRemoved(timer: Timer) {
+        notifyDataSetChanged()
+    }
+
+    override fun timerUpdated(before: Timer, after: Timer) {
+        val timerItemFragment = mFragments[after.id]
+        timerItemFragment?.updateTime()
+    }
+
+    /**
+     * @return `true` if at least one timer is in a state requiring continuous updates
+     */
+    fun updateTime(): Boolean {
+        var continuousUpdates = false
+        for (fragment in mFragments.values) {
+            continuousUpdates = continuousUpdates or fragment!!.updateTime()
+        }
+        return continuousUpdates
+    }
+
+    fun getTimer(index: Int): Timer {
+        return timers[index]
+    }
+
+    private val timers: List<Timer>
+        get() = DataModel.dataModel.timers
+
+    companion object {
+        private fun setItemVisible(item: Fragment, visible: Boolean) {
+            item.setMenuVisibility(visible)
+            item.setUserVisibleHint(visible)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerReceiver.java b/src/com/android/deskclock/timer/TimerReceiver.java
deleted file mode 100644
index 7a41789..0000000
--- a/src/com/android/deskclock/timer/TimerReceiver.java
+++ /dev/null
@@ -1,43 +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.timer;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-
-/**
- * This broadcast receiver exists to handle timer expiry scheduled in 4.2.1 and prior. It must exist
- * for at least one release cycle before removal to honor these old scheduled timers after upgrading
- * beyond 4.2.1. After 4.2.1, all timer expiration is directed to TimerService.
- */
-public class TimerReceiver extends BroadcastReceiver {
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        LogUtils.e("TimerReceiver", "Received legacy timer broadcast: %s", intent.getAction());
-
-        if ("times_up".equals(intent.getAction())) {
-            final int timerId = intent.getIntExtra("timer.intent.extra", -1);
-            final Timer timer = DataModel.getDataModel().getTimer(timerId);
-            context.startService(TimerService.createTimerExpiredIntent(context, timer));
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerReceiver.kt b/src/com/android/deskclock/timer/TimerReceiver.kt
new file mode 100644
index 0000000..aa74177
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerReceiver.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+
+/**
+ * This broadcast receiver exists to handle timer expiry scheduled in 4.2.1 and prior. It must exist
+ * for at least one release cycle before removal to honor these old scheduled timers after upgrading
+ * beyond 4.2.1. After 4.2.1, all timer expiration is directed to TimerService.
+ */
+class TimerReceiver : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent) {
+        LogUtils.e("TimerReceiver", "Received legacy timer broadcast: %s", intent.action)
+
+        if ("times_up" == intent.action) {
+            val timerId = intent.getIntExtra("timer.intent.extra", -1)
+            val timer: Timer? = DataModel.dataModel.getTimer(timerId)
+            context.startService(TimerService.createTimerExpiredIntent(context, timer))
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerService.java b/src/com/android/deskclock/timer/TimerService.java
deleted file mode 100644
index a82652e..0000000
--- a/src/com/android/deskclock/timer/TimerService.java
+++ /dev/null
@@ -1,190 +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.timer;
-
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
-
-/**
- * <p>This service exists solely to allow {@link android.app.AlarmManager} and timer notifications
- * to alter the state of timers without disturbing the notification shade. If an activity were used
- * instead (even one that is not displayed) the notification manager implicitly closes the
- * notification shade which clashes with the use case of starting/pausing/resetting timers without
- * disturbing the notification shade.</p>
- *
- * <p>The service has a second benefit. It is used to start heads-up notifications for expired
- * timers in the foreground. This keeps the entire application in the foreground and thus prevents
- * the operating system from killing it while expired timers are firing.</p>
- */
-public final class TimerService extends Service {
-
-    private static final String ACTION_PREFIX = "com.android.deskclock.action.";
-
-    /** Shows the tab with timers; scrolls to a specific timer. */
-    public static final String ACTION_SHOW_TIMER = ACTION_PREFIX + "SHOW_TIMER";
-    /** Pauses running timers; resets expired timers. */
-    public static final String ACTION_PAUSE_TIMER = ACTION_PREFIX + "PAUSE_TIMER";
-    /** Starts the sole timer. */
-    public static final String ACTION_START_TIMER = ACTION_PREFIX + "START_TIMER";
-    /** Resets the timer. */
-    public static final String ACTION_RESET_TIMER = ACTION_PREFIX + "RESET_TIMER";
-    /** Adds an extra minute to the timer. */
-    public static final String ACTION_ADD_MINUTE_TIMER = ACTION_PREFIX + "ADD_MINUTE_TIMER";
-
-    /** Extra for many actions specific to a given timer. */
-    public static final String EXTRA_TIMER_ID = "com.android.deskclock.extra.TIMER_ID";
-
-    private static final String ACTION_TIMER_EXPIRED =
-            ACTION_PREFIX + "TIMER_EXPIRED";
-    private static final String ACTION_UPDATE_NOTIFICATION =
-            ACTION_PREFIX + "UPDATE_NOTIFICATION";
-    private static final String ACTION_RESET_EXPIRED_TIMERS =
-            ACTION_PREFIX + "RESET_EXPIRED_TIMERS";
-    private static final String ACTION_RESET_UNEXPIRED_TIMERS =
-            ACTION_PREFIX + "RESET_UNEXPIRED_TIMERS";
-    private static final String ACTION_RESET_MISSED_TIMERS =
-            ACTION_PREFIX + "RESET_MISSED_TIMERS";
-
-    public static Intent createTimerExpiredIntent(Context context, Timer timer) {
-        final int timerId = timer == null ? -1 : timer.getId();
-        return new Intent(context, TimerService.class)
-                .setAction(ACTION_TIMER_EXPIRED)
-                .putExtra(EXTRA_TIMER_ID, timerId);
-    }
-
-    public static Intent createResetExpiredTimersIntent(Context context) {
-        return new Intent(context, TimerService.class)
-                .setAction(ACTION_RESET_EXPIRED_TIMERS);
-    }
-
-    public static Intent createResetUnexpiredTimersIntent(Context context) {
-        return new Intent(context, TimerService.class)
-                .setAction(ACTION_RESET_UNEXPIRED_TIMERS);
-    }
-
-    public static Intent createResetMissedTimersIntent(Context context) {
-        return new Intent(context, TimerService.class)
-                .setAction(ACTION_RESET_MISSED_TIMERS);
-    }
-
-
-    public static Intent createAddMinuteTimerIntent(Context context, int timerId) {
-        return new Intent(context, TimerService.class)
-                .setAction(ACTION_ADD_MINUTE_TIMER)
-                .putExtra(EXTRA_TIMER_ID, timerId);
-    }
-
-    public static Intent createUpdateNotificationIntent(Context context) {
-        return new Intent(context, TimerService.class)
-                .setAction(ACTION_UPDATE_NOTIFICATION);
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        try {
-            final String action = intent.getAction();
-            final int label = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent);
-            switch (action) {
-                case ACTION_UPDATE_NOTIFICATION: {
-                    DataModel.getDataModel().updateTimerNotification();
-                    return START_NOT_STICKY;
-                }
-                case ACTION_RESET_EXPIRED_TIMERS: {
-                    DataModel.getDataModel().resetOrDeleteExpiredTimers(label);
-                    return START_NOT_STICKY;
-                }
-                case ACTION_RESET_UNEXPIRED_TIMERS: {
-                    DataModel.getDataModel().resetUnexpiredTimers(label);
-                    return START_NOT_STICKY;
-                }
-                case ACTION_RESET_MISSED_TIMERS: {
-                    DataModel.getDataModel().resetMissedTimers(label);
-                    return START_NOT_STICKY;
-                }
-            }
-
-            // Look up the timer in question.
-            final int timerId = intent.getIntExtra(EXTRA_TIMER_ID, -1);
-            final Timer timer = DataModel.getDataModel().getTimer(timerId);
-
-            // If the timer cannot be located, ignore the action.
-            if (timer == null) {
-                return START_NOT_STICKY;
-            }
-
-            // Perform the action on the timer.
-            switch (action) {
-                case ACTION_SHOW_TIMER: {
-                    Events.sendTimerEvent(R.string.action_show, label);
-
-                    // Change to the timers tab.
-                    UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
-
-                    // Open DeskClock which is now positioned on the timers tab and show the timer
-                    // in question.
-                    final Intent showTimers = new Intent(this, DeskClock.class)
-                            .putExtra(EXTRA_TIMER_ID, timerId)
-                            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                    startActivity(showTimers);
-                    break;
-                } case ACTION_START_TIMER: {
-                    Events.sendTimerEvent(R.string.action_start, label);
-                    DataModel.getDataModel().startTimer(this, timer);
-                    break;
-                } case ACTION_PAUSE_TIMER: {
-                    Events.sendTimerEvent(R.string.action_pause, label);
-                    DataModel.getDataModel().pauseTimer(timer);
-                    break;
-                } case ACTION_ADD_MINUTE_TIMER: {
-                    Events.sendTimerEvent(R.string.action_add_minute, label);
-                    DataModel.getDataModel().addTimerMinute(timer);
-                    break;
-                } case ACTION_RESET_TIMER: {
-                    DataModel.getDataModel().resetOrDeleteTimer(timer, label);
-                    break;
-                } case ACTION_TIMER_EXPIRED: {
-                    Events.sendTimerEvent(R.string.action_fire, label);
-                    DataModel.getDataModel().expireTimer(this, timer);
-                    break;
-                }
-            }
-        } finally {
-            // This service is foreground when expired timers exist and stopped when none exist.
-            if (DataModel.getDataModel().getExpiredTimers().isEmpty()) {
-                stopSelf();
-            }
-        }
-
-        return START_NOT_STICKY;
-    }
-}
diff --git a/src/com/android/deskclock/timer/TimerService.kt b/src/com/android/deskclock/timer/TimerService.kt
new file mode 100644
index 0000000..8e37a84
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerService.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ *
+ * This service exists solely to allow [android.app.AlarmManager] and timer notifications
+ * to alter the state of timers without disturbing the notification shade. If an activity were used
+ * instead (even one that is not displayed) the notification manager implicitly closes the
+ * notification shade which clashes with the use case of starting/pausing/resetting timers without
+ * disturbing the notification shade.
+ *
+ * The service has a second benefit. It is used to start heads-up notifications for expired
+ * timers in the foreground. This keeps the entire application in the foreground and thus prevents
+ * the operating system from killing it while expired timers are firing.
+ */
+class TimerService : Service() {
+    override fun onBind(intent: Intent): IBinder? {
+        return null
+    }
+
+    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+        try {
+            val action = intent.action
+            val label = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent)
+            when (action) {
+                ACTION_UPDATE_NOTIFICATION -> {
+                    DataModel.dataModel.updateTimerNotification()
+                    return START_NOT_STICKY
+                }
+                ACTION_RESET_EXPIRED_TIMERS -> {
+                    DataModel.dataModel.resetOrDeleteExpiredTimers(label)
+                    return START_NOT_STICKY
+                }
+                ACTION_RESET_UNEXPIRED_TIMERS -> {
+                    DataModel.dataModel.resetUnexpiredTimers(label)
+                    return START_NOT_STICKY
+                }
+                ACTION_RESET_MISSED_TIMERS -> {
+                    DataModel.dataModel.resetMissedTimers(label)
+                    return START_NOT_STICKY
+                }
+            }
+
+            // Look up the timer in question.
+            val timerId = intent.getIntExtra(EXTRA_TIMER_ID, -1)
+            // If the timer cannot be located, ignore the action.
+            val timer: Timer = DataModel.dataModel.getTimer(timerId) ?: return START_NOT_STICKY
+
+            when (action) {
+                ACTION_SHOW_TIMER -> {
+                    Events.sendTimerEvent(R.string.action_show, label)
+
+                    // Change to the timers tab.
+                    UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+
+                    // Open DeskClock which is now positioned on the timers tab and show the timer
+                    // in question.
+                    val showTimers = Intent(this, DeskClock::class.java)
+                            .putExtra(EXTRA_TIMER_ID, timerId)
+                            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    startActivity(showTimers)
+                }
+                ACTION_START_TIMER -> {
+                    Events.sendTimerEvent(R.string.action_start, label)
+                    DataModel.dataModel.startTimer(this, timer)
+                }
+                ACTION_PAUSE_TIMER -> {
+                    Events.sendTimerEvent(R.string.action_pause, label)
+                    DataModel.dataModel.pauseTimer(timer)
+                }
+                ACTION_ADD_MINUTE_TIMER -> {
+                    Events.sendTimerEvent(R.string.action_add_minute, label)
+                    DataModel.dataModel.addTimerMinute(timer)
+                }
+                ACTION_RESET_TIMER -> {
+                    DataModel.dataModel.resetOrDeleteTimer(timer, label)
+                }
+                ACTION_TIMER_EXPIRED -> {
+                    Events.sendTimerEvent(R.string.action_fire, label)
+                    DataModel.dataModel.expireTimer(this, timer)
+                }
+            }
+        } finally {
+            // This service is foreground when expired timers exist and stopped when none exist.
+            if (DataModel.dataModel.expiredTimers.isEmpty()) {
+                stopSelf()
+            }
+        }
+
+        return START_NOT_STICKY
+    }
+
+    companion object {
+        private const val ACTION_PREFIX = "com.android.deskclock.action."
+
+        /** Shows the tab with timers; scrolls to a specific timer.  */
+        const val ACTION_SHOW_TIMER = ACTION_PREFIX + "SHOW_TIMER"
+        /** Pauses running timers; resets expired timers.  */
+        const val ACTION_PAUSE_TIMER = ACTION_PREFIX + "PAUSE_TIMER"
+        /** Starts the sole timer.  */
+        const val ACTION_START_TIMER = ACTION_PREFIX + "START_TIMER"
+        /** Resets the timer.  */
+        const val ACTION_RESET_TIMER = ACTION_PREFIX + "RESET_TIMER"
+        /** Adds an extra minute to the timer.  */
+        const val ACTION_ADD_MINUTE_TIMER = ACTION_PREFIX + "ADD_MINUTE_TIMER"
+        /** Extra for many actions specific to a given timer.  */
+        const val EXTRA_TIMER_ID = "com.android.deskclock.extra.TIMER_ID"
+
+        private const val ACTION_TIMER_EXPIRED = ACTION_PREFIX + "TIMER_EXPIRED"
+        private const val ACTION_UPDATE_NOTIFICATION = ACTION_PREFIX + "UPDATE_NOTIFICATION"
+        private const val ACTION_RESET_EXPIRED_TIMERS = ACTION_PREFIX + "RESET_EXPIRED_TIMERS"
+        private const val ACTION_RESET_UNEXPIRED_TIMERS = ACTION_PREFIX + "RESET_UNEXPIRED_TIMERS"
+        private const val ACTION_RESET_MISSED_TIMERS = ACTION_PREFIX + "RESET_MISSED_TIMERS"
+
+        @JvmStatic
+        fun createTimerExpiredIntent(context: Context, timer: Timer?): Intent {
+            val timerId = timer?.id ?: -1
+            return Intent(context, TimerService::class.java)
+                    .setAction(ACTION_TIMER_EXPIRED)
+                    .putExtra(EXTRA_TIMER_ID, timerId)
+        }
+
+        fun createResetExpiredTimersIntent(context: Context): Intent {
+            return Intent(context, TimerService::class.java)
+                    .setAction(ACTION_RESET_EXPIRED_TIMERS)
+        }
+
+        fun createResetUnexpiredTimersIntent(context: Context): Intent {
+            return Intent(context, TimerService::class.java)
+                    .setAction(ACTION_RESET_UNEXPIRED_TIMERS)
+        }
+
+        fun createResetMissedTimersIntent(context: Context): Intent {
+            return Intent(context, TimerService::class.java)
+                    .setAction(ACTION_RESET_MISSED_TIMERS)
+        }
+
+        fun createAddMinuteTimerIntent(context: Context, timerId: Int): Intent {
+            return Intent(context, TimerService::class.java)
+                    .setAction(ACTION_ADD_MINUTE_TIMER)
+                    .putExtra(EXTRA_TIMER_ID, timerId)
+        }
+
+        fun createUpdateNotificationIntent(context: Context): Intent {
+            return Intent(context, TimerService::class.java)
+                    .setAction(ACTION_UPDATE_NOTIFICATION)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerSetupView.java b/src/com/android/deskclock/timer/TimerSetupView.java
deleted file mode 100644
index 557a7d6..0000000
--- a/src/com/android/deskclock/timer/TimerSetupView.java
+++ /dev/null
@@ -1,337 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.PorterDuff;
-import androidx.annotation.IdRes;
-import androidx.core.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.LinearLayout;
-import android.widget.TextView;
-
-import com.android.deskclock.FabContainer;
-import com.android.deskclock.FormattedTextUtils;
-import com.android.deskclock.R;
-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.FAB_REQUEST_FOCUS;
-import static com.android.deskclock.FabContainer.FAB_SHRINK_AND_EXPAND;
-
-public class TimerSetupView extends LinearLayout implements View.OnClickListener,
-        View.OnLongClickListener {
-
-    private final int[] mInput = { 0, 0, 0, 0, 0, 0 };
-
-    private int mInputPointer = -1;
-    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;
-
-    public TimerSetupView(Context context) {
-        this(context, null /* attrs */);
-    }
-
-    public TimerSetupView(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        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));
-
-        // 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)));
-
-        LayoutInflater.from(context).inflate(R.layout.timer_setup_container, this);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        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),
-        };
-
-        // 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);
-
-        // 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();
-        updateDeleteAndDivider();
-    }
-
-    public void setFabContainer(FabContainer fabContainer) {
-        mFabContainer = fabContainer;
-    }
-
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        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 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;
-            updateTime();
-            updateDeleteAndDivider();
-        }
-    }
-
-    public boolean hasValidInput() {
-        return mInputPointer != -1;
-    }
-
-    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;
-    }
-
-    /**
-     * @return an opaque representation of the state of timer setup
-     */
-    public Serializable getState() {
-        return Arrays.copyOf(mInput, mInput.length);
-    }
-
-    /**
-     * @param state an opaque state of this view previously produced by {@link #getState()}
-     */
-    public void setState(Serializable state) {
-        final int[] input = (int[]) state;
-        if (input != null && mInput.length == input.length) {
-            for (int i = 0; i < mInput.length; i++) {
-                mInput[i] = input[i];
-                if (mInput[i] != 0) {
-                    mInputPointer = i;
-                }
-            }
-            updateTime();
-            updateDeleteAndDivider();
-        }
-    }
-}
diff --git a/src/com/android/deskclock/timer/TimerSetupView.kt b/src/com/android/deskclock/timer/TimerSetupView.kt
new file mode 100644
index 0000000..0d0f75f
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerSetupView.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ColorStateList
+import android.graphics.PorterDuff
+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.view.View.OnLongClickListener
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.core.view.ViewCompat
+
+import com.android.deskclock.FabContainer
+import com.android.deskclock.FormattedTextUtils
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.uidata.UiDataModel
+
+import java.io.Serializable
+
+class TimerSetupView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : LinearLayout(context, attrs), View.OnClickListener, OnLongClickListener {
+    private val mInput = intArrayOf(0, 0, 0, 0, 0, 0)
+
+    private var mInputPointer = -1
+    private val mTimeTemplate: CharSequence
+
+    private lateinit var mTimeView: TextView
+    private lateinit var mDeleteView: View
+    private lateinit var mDividerView: View
+    private lateinit var mDigitViews: Array<TextView>
+
+    /** Updates to the fab are requested via this container.  */
+    private lateinit var mFabContainer: FabContainer
+
+    init {
+        val bf = BidiFormatter.getInstance(false /* rtlContext */)
+        val hoursLabel = bf.unicodeWrap(context.getString(R.string.hours_label))
+        val minutesLabel = bf.unicodeWrap(context.getString(R.string.minutes_label))
+        val secondsLabel = bf.unicodeWrap(context.getString(R.string.seconds_label))
+
+        // 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, RelativeSizeSpan(0.5f)),
+                FormattedTextUtils.formatText(minutesLabel, RelativeSizeSpan(0.5f)),
+                FormattedTextUtils.formatText(secondsLabel, RelativeSizeSpan(0.5f)))
+
+        LayoutInflater.from(context).inflate(R.layout.timer_setup_container, this)
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+
+        mTimeView = findViewById<View>(R.id.timer_setup_time) as TextView
+        mDeleteView = findViewById(R.id.timer_setup_delete)
+        mDividerView = findViewById(R.id.timer_setup_divider)
+        mDigitViews = arrayOf(
+                findViewById<View>(R.id.timer_setup_digit_0) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_1) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_2) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_3) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_4) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_5) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_6) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_7) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_8) as TextView,
+                findViewById<View>(R.id.timer_setup_digit_9) as TextView)
+
+        // Tint the divider to match the disabled control color by default and used the activated
+        // control color when there is valid input.
+        val dividerContext = mDividerView.context
+        val colorControlActivated = ThemeUtils.resolveColor(dividerContext,
+                R.attr.colorControlActivated)
+        val colorControlDisabled = ThemeUtils.resolveColor(dividerContext,
+                R.attr.colorControlNormal, intArrayOf(android.R.attr.state_enabled.inv()))
+        ViewCompat.setBackgroundTintList(mDividerView,
+                ColorStateList(
+                        arrayOf(intArrayOf(android.R.attr.state_activated), intArrayOf()),
+                        intArrayOf(colorControlActivated, colorControlDisabled)))
+        ViewCompat.setBackgroundTintMode(mDividerView, PorterDuff.Mode.SRC)
+
+        // Initialize the digit buttons.
+        val uidm = UiDataModel.uiDataModel
+        for (digitView in mDigitViews) {
+            val digit = getDigitForId(digitView.id)
+            digitView.text = uidm.getFormattedNumber(digit, 1)
+            digitView.setOnClickListener(this)
+        }
+
+        mDeleteView.setOnClickListener(this)
+        mDeleteView.setOnLongClickListener(this)
+
+        updateTime()
+        updateDeleteAndDivider()
+    }
+
+    fun setFabContainer(fabContainer: FabContainer) {
+        mFabContainer = fabContainer
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+        var 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) {
+            val result = view.performClick()
+            if (result && hasValidInput()) {
+                mFabContainer.updateFab(FabContainer.FAB_REQUEST_FOCUS)
+            }
+            return result
+        }
+
+        return false
+    }
+
+    override fun onClick(view: View) {
+        if (view === mDeleteView) {
+            delete()
+        } else {
+            append(getDigitForId(view.id))
+        }
+    }
+
+    override fun onLongClick(view: View): Boolean {
+        if (view === mDeleteView) {
+            reset()
+            updateFab()
+            return true
+        }
+        return false
+    }
+
+    private fun getDigitForId(@IdRes id: Int): Int = when (id) {
+        R.id.timer_setup_digit_0 -> 0
+        R.id.timer_setup_digit_1 -> 1
+        R.id.timer_setup_digit_2 -> 2
+        R.id.timer_setup_digit_3 -> 3
+        R.id.timer_setup_digit_4 -> 4
+        R.id.timer_setup_digit_5 -> 5
+        R.id.timer_setup_digit_6 -> 6
+        R.id.timer_setup_digit_7 -> 7
+        R.id.timer_setup_digit_8 -> 8
+        R.id.timer_setup_digit_9 -> 9
+        else -> throw IllegalArgumentException("Invalid id: $id")
+    }
+
+    private fun updateTime() {
+        val seconds = mInput[1] * 10 + mInput[0]
+        val minutes = mInput[3] * 10 + mInput[2]
+        val hours = mInput[5] * 10 + mInput[4]
+
+        val uidm = UiDataModel.uiDataModel
+        mTimeView.text = TextUtils.expandTemplate(mTimeTemplate,
+                uidm.getFormattedNumber(hours, 2),
+                uidm.getFormattedNumber(minutes, 2),
+                uidm.getFormattedNumber(seconds, 2))
+
+        val r = resources
+        mTimeView.contentDescription = 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 fun updateDeleteAndDivider() {
+        val enabled = hasValidInput()
+        mDeleteView.isEnabled = enabled
+        mDividerView.isActivated = enabled
+    }
+
+    private fun updateFab() {
+        mFabContainer.updateFab(FabContainer.FAB_SHRINK_AND_EXPAND)
+    }
+
+    private fun append(digit: Int) {
+        require(!(digit < 0 || digit > 9)) { "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.size - 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.contentDescription = context.getString(
+                R.string.timer_descriptive_delete,
+                UiDataModel.uiDataModel.getFormattedNumber(digit))
+
+        // Update the fab, delete, and divider when we have valid input.
+        if (mInputPointer == 0) {
+            updateFab()
+            updateDeleteAndDivider()
+        }
+    }
+
+    private fun 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.contentDescription = context.getString(
+                    R.string.timer_descriptive_delete,
+                    UiDataModel.uiDataModel.getFormattedNumber(mInput[0]))
+        } else {
+            mDeleteView.contentDescription = context.getString(R.string.timer_delete)
+        }
+
+        // Update the fab, delete, and divider when we no longer have valid input.
+        if (mInputPointer == -1) {
+            updateFab()
+            updateDeleteAndDivider()
+        }
+    }
+
+    fun reset() {
+        if (mInputPointer != -1) {
+            mInput.fill(0)
+            mInputPointer = -1
+            updateTime()
+            updateDeleteAndDivider()
+        }
+    }
+
+    fun hasValidInput(): Boolean {
+        return mInputPointer != -1
+    }
+
+    val timeInMillis: Long
+        get() {
+            val seconds = mInput[1] * 10 + mInput[0]
+            val minutes = mInput[3] * 10 + mInput[2]
+            val hours = mInput[5] * 10 + mInput[4]
+            return seconds * DateUtils.SECOND_IN_MILLIS +
+                    minutes * DateUtils.MINUTE_IN_MILLIS +
+                    hours * DateUtils.HOUR_IN_MILLIS
+        }
+
+    var state: Serializable?
+        /**
+         * @return an opaque representation of the state of timer setup
+         */
+        get() = mInput.copyOf(mInput.size)
+        /**
+         * @param state an opaque state of this view previously produced by [.getState]
+         */
+        set(state) {
+            val input = state as IntArray?
+            if (input != null && mInput.size == input.size) {
+                for (i in mInput.indices) {
+                    mInput[i] = input[i]
+                    if (mInput[i] != 0) {
+                        mInputPointer = i
+                    }
+                }
+                updateTime()
+                updateDeleteAndDivider()
+            }
+        }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/FormattedStringModel.java b/src/com/android/deskclock/uidata/FormattedStringModel.java
deleted file mode 100644
index e630d74..0000000
--- a/src/com/android/deskclock/uidata/FormattedStringModel.java
+++ /dev/null
@@ -1,197 +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.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/FormattedStringModel.kt b/src/com/android/deskclock/uidata/FormattedStringModel.kt
new file mode 100644
index 0000000..f720299
--- /dev/null
+++ b/src/com/android/deskclock/uidata/FormattedStringModel.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+/**
+ * All formatted strings that are cached for performance are accessed via this model.
+ */
+internal class FormattedStringModel(context: Context) {
+    /** Clears data structures containing data that is locale-sensitive.  */
+    private val mLocaleChangedReceiver: BroadcastReceiver = 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 val mNumberFormatCache = SparseArray<SparseArray<String>>(3)
+
+    /** Single-character version of weekday names; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'  */
+    private var mShortWeekdayNames: MutableMap<Int, String>? = null
+
+    /** Full weekday names; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.  */
+    private var mLongWeekdayNames: MutableMap<Int, String>? = null
+
+    init {
+        // Clear caches affected by locale when locale changes.
+        val localeBroadcastFilter = 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 `value` formatted as a String in the current locale
+     * @throws IllegalArgumentException if `value` is negative
+     */
+    fun getFormattedNumber(value: Int): String {
+        val length = if (value == 0) 1 else Math.log10(value.toDouble()).toInt() + 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 `value` formatted as a String in the current locale and padded to the
+     * requested `length`
+     * @throws IllegalArgumentException if `value` is negative
+     */
+    fun getFormattedNumber(value: Int, length: Int): String {
+        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 `value` is `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
+     * `negative` is `true` the return value will contain a minus sign and a total
+     * length of `length + 1`.
+     * @return the `value` formatted as a String in the current locale and padded to the
+     * requested `length`
+     * @throws IllegalArgumentException if `value` is negative
+     */
+    fun getFormattedNumber(negative: Boolean, value: Int, length: Int): String {
+        require(value >= 0) { "value may not be negative: $value" }
+
+        // Look up the value cache using the length; -ve and +ve values are cached separately.
+        val lengthCacheKey = if (negative) -length else length
+        var valueCache = mNumberFormatCache[lengthCacheKey]
+        if (valueCache == null) {
+            valueCache = SparseArray(Math.pow(10.0, length.toDouble()).toInt())
+            mNumberFormatCache.put(lengthCacheKey, valueCache)
+        }
+
+        // Look up the cached formatted value using the value.
+        var formatted = valueCache[value]
+        if (formatted == null) {
+            val sign = if (negative) "−" else ""
+            formatted = String.format(Locale.getDefault(), sign + "%0" + length + "d", value)
+            valueCache.put(value, formatted)
+        }
+
+        return formatted
+    }
+
+    /**
+     * @param calendarDay any of the following values
+     *
+     *  * [Calendar.SUNDAY]
+     *  * [Calendar.MONDAY]
+     *  * [Calendar.TUESDAY]
+     *  * [Calendar.WEDNESDAY]
+     *  * [Calendar.THURSDAY]
+     *  * [Calendar.FRIDAY]
+     *  * [Calendar.SATURDAY]
+     *
+     * @return single-character weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
+     */
+    fun getShortWeekday(calendarDay: Int): String? {
+        if (mShortWeekdayNames == null) {
+            mShortWeekdayNames = ArrayMap(7)
+
+            val format = SimpleDateFormat("ccccc", Locale.getDefault())
+            for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+                val calendar: Calendar = GregorianCalendar(2014, Calendar.JULY, 20 + i - 1)
+                val weekday = format.format(calendar.time)
+                mShortWeekdayNames!![i] = weekday
+            }
+        }
+
+        return mShortWeekdayNames!![calendarDay]
+    }
+
+    /**
+     * @param calendarDay any of the following values
+     *
+     *  * [Calendar.SUNDAY]
+     *  * [Calendar.MONDAY]
+     *  * [Calendar.TUESDAY]
+     *  * [Calendar.WEDNESDAY]
+     *  * [Calendar.THURSDAY]
+     *  * [Calendar.FRIDAY]
+     *  * [Calendar.SATURDAY]
+     *
+     * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
+     */
+    fun getLongWeekday(calendarDay: Int): String? {
+        if (mLongWeekdayNames == null) {
+            mLongWeekdayNames = ArrayMap(7)
+
+            val calendar: Calendar = GregorianCalendar(2014, Calendar.JULY, 20)
+            val format = SimpleDateFormat("EEEE", Locale.getDefault())
+            for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+                val weekday = format.format(calendar.time)
+                mLongWeekdayNames!![i] = weekday
+                calendar.add(Calendar.DAY_OF_YEAR, 1)
+            }
+        }
+
+        return mLongWeekdayNames!![calendarDay]
+    }
+
+    /**
+     * Cached information that is locale-sensitive must be cleared in response to locale changes.
+     */
+    private inner class LocaleChangedReceiver : BroadcastReceiver() {
+        override fun 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/PeriodicCallbackModel.java b/src/com/android/deskclock/uidata/PeriodicCallbackModel.java
deleted file mode 100644
index 4b522d1..0000000
--- a/src/com/android/deskclock/uidata/PeriodicCallbackModel.java
+++ /dev/null
@@ -1,230 +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.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Handler;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.deskclock.LogUtils;
-
-import java.util.Calendar;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import static android.content.Intent.ACTION_DATE_CHANGED;
-import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
-import static android.content.Intent.ACTION_TIME_CHANGED;
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static com.android.deskclock.Utils.enforceMainLooper;
-import static java.util.Calendar.DATE;
-import static java.util.Calendar.HOUR_OF_DAY;
-import static java.util.Calendar.MILLISECOND;
-import static java.util.Calendar.MINUTE;
-import static java.util.Calendar.SECOND;
-
-/**
- * All callbacks to be delivered at requested times on the main thread if the application is in the
- * foreground when the callback time passes.
- */
-final class PeriodicCallbackModel {
-
-    private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Periodic");
-
-    @VisibleForTesting
-    enum Period {MINUTE, QUARTER_HOUR, HOUR, MIDNIGHT}
-
-    private static final long QUARTER_HOUR_IN_MILLIS = 15 * MINUTE_IN_MILLIS;
-
-    private static Handler sHandler;
-
-    /** Reschedules callbacks when the device time changes. */
-    @SuppressWarnings("FieldCanBeLocal")
-    private final BroadcastReceiver mTimeChangedReceiver = new TimeChangedReceiver();
-
-    private final List<PeriodicRunnable> mPeriodicRunnables = new CopyOnWriteArrayList<>();
-
-    PeriodicCallbackModel(Context context) {
-        // Reschedules callbacks when the device time changes.
-        final IntentFilter timeChangedBroadcastFilter = new IntentFilter();
-        timeChangedBroadcastFilter.addAction(ACTION_TIME_CHANGED);
-        timeChangedBroadcastFilter.addAction(ACTION_DATE_CHANGED);
-        timeChangedBroadcastFilter.addAction(ACTION_TIMEZONE_CHANGED);
-        context.registerReceiver(mTimeChangedReceiver, timeChangedBroadcastFilter);
-    }
-
-    /**
-     * @param runnable to be called every minute
-     * @param offset an offset applied to the minute to control when the callback occurs
-     */
-    void addMinuteCallback(Runnable runnable, long offset) {
-        addPeriodicCallback(runnable, Period.MINUTE, offset);
-    }
-
-    /**
-     * @param runnable to be called every quarter-hour
-     * @param offset an offset applied to the quarter-hour to control when the callback occurs
-     */
-    void addQuarterHourCallback(Runnable runnable, long offset) {
-        addPeriodicCallback(runnable, Period.QUARTER_HOUR, offset);
-    }
-
-    /**
-     * @param runnable to be called every hour
-     * @param offset an offset applied to the hour to control when the callback occurs
-     */
-    void addHourCallback(Runnable runnable, long offset) {
-        addPeriodicCallback(runnable, Period.HOUR, offset);
-    }
-
-    /**
-     * @param runnable to be called every midnight
-     * @param offset an offset applied to the midnight to control when the callback occurs
-     */
-    void addMidnightCallback(Runnable runnable, long offset) {
-        addPeriodicCallback(runnable, Period.MIDNIGHT, offset);
-    }
-
-    /**
-     * @param runnable to be called periodically
-     */
-    private void addPeriodicCallback(Runnable runnable, Period period, long offset) {
-        final PeriodicRunnable periodicRunnable = new PeriodicRunnable(runnable, period, offset);
-        mPeriodicRunnables.add(periodicRunnable);
-        periodicRunnable.schedule();
-    }
-
-    /**
-     * @param runnable to no longer be called periodically
-     */
-    void removePeriodicCallback(Runnable runnable) {
-        for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
-            if (periodicRunnable.mDelegate == runnable) {
-                periodicRunnable.unSchedule();
-                mPeriodicRunnables.remove(periodicRunnable);
-                return;
-            }
-        }
-    }
-
-    /**
-     * Return the delay until the given {@code period} elapses adjusted by the given {@code offset}.
-     *
-     * @param now the current time
-     * @param period the frequency with which callbacks should be given
-     * @param offset an offset to add to the normal period; allows the callback to be made relative
-     *      to the normally scheduled period end
-     * @return the time delay from {@code now} to schedule the callback
-     */
-    @VisibleForTesting
-    static long getDelay(long now, Period period, long offset) {
-        final long periodStart = now - offset;
-
-        switch (period) {
-            case MINUTE:
-                final long lastMinute = periodStart - (periodStart % MINUTE_IN_MILLIS);
-                final long nextMinute = lastMinute + MINUTE_IN_MILLIS;
-                return nextMinute - now + offset;
-
-            case QUARTER_HOUR:
-                final long lastQuarterHour = periodStart - (periodStart % QUARTER_HOUR_IN_MILLIS);
-                final long nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS;
-                return nextQuarterHour - now + offset;
-
-            case HOUR:
-                final long lastHour = periodStart - (periodStart % HOUR_IN_MILLIS);
-                final long nextHour = lastHour + HOUR_IN_MILLIS;
-                return nextHour - now + offset;
-
-            case MIDNIGHT:
-                final Calendar nextMidnight = Calendar.getInstance();
-                nextMidnight.setTimeInMillis(periodStart);
-                nextMidnight.add(DATE, 1);
-                nextMidnight.set(HOUR_OF_DAY, 0);
-                nextMidnight.set(MINUTE, 0);
-                nextMidnight.set(SECOND, 0);
-                nextMidnight.set(MILLISECOND, 0);
-                return nextMidnight.getTimeInMillis() - now + offset;
-
-            default:
-                throw new IllegalArgumentException("unexpected period: " + period);
-        }
-    }
-
-    private static Handler getHandler() {
-        enforceMainLooper();
-        if (sHandler == null) {
-            sHandler = new Handler();
-        }
-        return sHandler;
-    }
-
-    /**
-     * Schedules the execution of the given delegate Runnable at the next callback time.
-     */
-    private static final class PeriodicRunnable implements Runnable {
-
-        private final Runnable mDelegate;
-        private final Period mPeriod;
-        private final long mOffset;
-
-        public PeriodicRunnable(Runnable delegate, Period period, long offset) {
-            mDelegate = delegate;
-            mPeriod = period;
-            mOffset = offset;
-        }
-
-        @Override
-        public void run() {
-            LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod);
-            mDelegate.run();
-            schedule();
-        }
-
-        private void runAndReschedule() {
-            LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod);
-            unSchedule();
-            mDelegate.run();
-            schedule();
-        }
-
-        private void schedule() {
-            final long delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset);
-            getHandler().postDelayed(this, delay);
-        }
-
-        private void unSchedule() {
-            getHandler().removeCallbacks(this);
-        }
-    }
-
-    /**
-     * Reschedules callbacks when the device time changes.
-     */
-    private final class TimeChangedReceiver extends BroadcastReceiver {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
-                periodicRunnable.runAndReschedule();
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt b/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt
new file mode 100644
index 0000000..0a76c94
--- /dev/null
+++ b/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.os.Handler
+import android.os.Looper
+import android.text.format.DateUtils
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.Calendar
+
+/**
+ * All callbacks to be delivered at requested times on the main thread if the application is in the
+ * foreground when the callback time passes.
+ */
+internal class PeriodicCallbackModel(context: Context) {
+
+    @VisibleForTesting
+    internal enum class Period {
+        MINUTE, QUARTER_HOUR, HOUR, MIDNIGHT
+    }
+
+    /** Reschedules callbacks when the device time changes.  */
+    private val mTimeChangedReceiver: BroadcastReceiver = TimeChangedReceiver()
+
+    private val mPeriodicRunnables: MutableList<PeriodicRunnable> = CopyOnWriteArrayList()
+
+    init {
+        // Reschedules callbacks when the device time changes.
+        val timeChangedBroadcastFilter = IntentFilter()
+        timeChangedBroadcastFilter.addAction(Intent.ACTION_TIME_CHANGED)
+        timeChangedBroadcastFilter.addAction(Intent.ACTION_DATE_CHANGED)
+        timeChangedBroadcastFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
+        context.registerReceiver(mTimeChangedReceiver, timeChangedBroadcastFilter)
+    }
+
+    /**
+     * @param runnable to be called every minute
+     * @param offset an offset applied to the minute to control when the callback occurs
+     */
+    fun addMinuteCallback(runnable: Runnable, offset: Long) {
+        addPeriodicCallback(runnable, Period.MINUTE, offset)
+    }
+
+    /**
+     * @param runnable to be called every quarter-hour
+     */
+    fun addQuarterHourCallback(runnable: Runnable) {
+        // Callbacks *can* occur early so pad in an extra 100ms on the quarter-hour callback
+        // to ensure the sampled wallclock time reflects the subsequent quarter-hour.
+        addPeriodicCallback(runnable, Period.QUARTER_HOUR, 100L)
+    }
+
+    /**
+     * @param runnable to be called every hour
+     */
+    fun addHourCallback(runnable: Runnable) {
+        // Callbacks *can* occur early so pad in an extra 100ms on the hour callback to ensure
+        // the sampled wallclock time reflects the subsequent hour.
+        addPeriodicCallback(runnable, Period.HOUR, 100L)
+    }
+
+    /**
+     * @param runnable to be called every midnight
+     */
+    fun addMidnightCallback(runnable: Runnable) {
+        // Callbacks *can* occur early so pad in an extra 100ms on the midnight callback to ensure
+        // the sampled wallclock time reflects the subsequent day.
+        addPeriodicCallback(runnable, Period.MIDNIGHT, 100L)
+    }
+
+    /**
+     * @param runnable to be called periodically
+     */
+    private fun addPeriodicCallback(runnable: Runnable, period: Period, offset: Long) {
+        val periodicRunnable = PeriodicRunnable(runnable, period, offset)
+        mPeriodicRunnables.add(periodicRunnable)
+        periodicRunnable.schedule()
+    }
+
+    /**
+     * @param runnable to no longer be called periodically
+     */
+    fun removePeriodicCallback(runnable: Runnable) {
+        for (periodicRunnable in mPeriodicRunnables) {
+            if (periodicRunnable.mDelegate === runnable) {
+                periodicRunnable.unSchedule()
+                mPeriodicRunnables.remove(periodicRunnable)
+                return
+            }
+        }
+    }
+
+    /**
+     * Schedules the execution of the given delegate Runnable at the next callback time.
+     */
+    private class PeriodicRunnable(
+        val mDelegate: Runnable,
+        private val mPeriod: Period,
+        private val mOffset: Long
+    ) : Runnable {
+        override fun run() {
+            LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod)
+            mDelegate.run()
+            schedule()
+        }
+
+        fun runAndReschedule() {
+            LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod)
+            unSchedule()
+            mDelegate.run()
+            schedule()
+        }
+
+        fun schedule() {
+            val delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset)
+            handler.postDelayed(this, delay)
+        }
+
+        fun unSchedule() {
+            handler.removeCallbacks(this)
+        }
+    }
+
+    /**
+     * Reschedules callbacks when the device time changes.
+     */
+    private inner class TimeChangedReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            for (periodicRunnable in mPeriodicRunnables) {
+                periodicRunnable.runAndReschedule()
+            }
+        }
+    }
+
+    companion object {
+        private val LOGGER = LogUtils.Logger("Periodic")
+
+        private const val QUARTER_HOUR_IN_MILLIS = 15 * DateUtils.MINUTE_IN_MILLIS
+
+        private var sHandler: Handler? = null
+
+        /**
+         * Return the delay until the given `period` elapses adjusted by the given `offset`.
+         *
+         * @param now the current time
+         * @param period the frequency with which callbacks should be given
+         * @param offset an offset to add to the normal period; allows the callback to
+         * be made relative to the normally scheduled period end
+         * @return the time delay from `now` to schedule the callback
+         */
+        @VisibleForTesting
+        @JvmStatic
+        fun getDelay(now: Long, period: Period, offset: Long): Long {
+            val periodStart = now - offset
+
+            return when (period) {
+                Period.MINUTE -> {
+                    val lastMinute = periodStart - periodStart % DateUtils.MINUTE_IN_MILLIS
+                    val nextMinute = lastMinute + DateUtils.MINUTE_IN_MILLIS
+                    nextMinute - now + offset
+                }
+                Period.QUARTER_HOUR -> {
+                    val lastQuarterHour = periodStart - periodStart % QUARTER_HOUR_IN_MILLIS
+                    val nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS
+                    nextQuarterHour - now + offset
+                }
+                Period.HOUR -> {
+                    val lastHour = periodStart - periodStart % DateUtils.HOUR_IN_MILLIS
+                    val nextHour = lastHour + DateUtils.HOUR_IN_MILLIS
+                    nextHour - now + offset
+                }
+                Period.MIDNIGHT -> {
+                    val nextMidnight = Calendar.getInstance()
+                    nextMidnight.timeInMillis = periodStart
+                    nextMidnight.add(Calendar.DATE, 1)
+                    nextMidnight[Calendar.HOUR_OF_DAY] = 0
+                    nextMidnight[Calendar.MINUTE] = 0
+                    nextMidnight[Calendar.SECOND] = 0
+                    nextMidnight[Calendar.MILLISECOND] = 0
+                    nextMidnight.timeInMillis - now + offset
+                }
+            }
+        }
+
+        private val handler: Handler
+            get() {
+                Utils.enforceMainLooper()
+                if (sHandler == null) {
+                    sHandler = Handler(Looper.myLooper()!!)
+                }
+                return sHandler!!
+            }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabDAO.java b/src/com/android/deskclock/uidata/TabDAO.java
deleted file mode 100644
index 3be5d19..0000000
--- a/src/com/android/deskclock/uidata/TabDAO.java
+++ /dev/null
@@ -1,47 +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.uidata;
-
-import android.content.SharedPreferences;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab;
-
-/**
- * This class encapsulates the storage of tab data in {@link SharedPreferences}.
- */
-final class TabDAO {
-
-    /** Key to a preference that stores the ordinal of the selected tab. */
-    private static final String KEY_SELECTED_TAB = "selected_tab";
-
-    private TabDAO() {}
-
-    /**
-     * @return an enumerated value indicating the currently selected primary tab
-     */
-    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(SharedPreferences prefs, Tab tab) {
-        prefs.edit().putInt(KEY_SELECTED_TAB, tab.ordinal()).apply();
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabDAO.kt b/src/com/android/deskclock/uidata/TabDAO.kt
new file mode 100644
index 0000000..fdbf3e1
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabDAO.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.SharedPreferences
+
+/**
+ * This class encapsulates the storage of tab data in [SharedPreferences].
+ */
+internal object TabDAO {
+    /** Key to a preference that stores the ordinal of the selected tab.  */
+    private const val KEY_SELECTED_TAB = "selected_tab"
+
+    /**
+     * @return an enumerated value indicating the currently selected primary tab
+     */
+    fun getSelectedTab(prefs: SharedPreferences): UiDataModel.Tab {
+        val ordinal = prefs.getInt(KEY_SELECTED_TAB, UiDataModel.Tab.CLOCKS.ordinal)
+        return UiDataModel.Tab.values()[ordinal]
+    }
+
+    /**
+     * @param tab an enumerated value indicating the newly selected primary tab
+     */
+    fun setSelectedTab(prefs: SharedPreferences, tab: UiDataModel.Tab) {
+        prefs.edit().putInt(KEY_SELECTED_TAB, tab.ordinal).apply()
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabListener.java b/src/com/android/deskclock/uidata/TabListener.kt
similarity index 77%
rename from src/com/android/deskclock/uidata/TabListener.java
rename to src/com/android/deskclock/uidata/TabListener.kt
index 918b893..c0e7721 100644
--- a/src/com/android/deskclock/uidata/TabListener.java
+++ b/src/com/android/deskclock/uidata/TabListener.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,18 +14,15 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.uidata;
-
-import com.android.deskclock.uidata.UiDataModel.Tab;
+package com.android.deskclock.uidata
 
 /**
  * The interface through which interested parties are notified of changes to the selected tab.
  */
-public interface TabListener {
-
+interface TabListener {
     /**
      * @param oldSelectedTab an enumerated value indicating the prior selected tab
      * @param newSelectedTab an enumerated value indicating the newly selected tab
      */
-    void selectedTabChanged(Tab oldSelectedTab, Tab newSelectedTab);
+    fun selectedTabChanged(oldSelectedTab: UiDataModel.Tab, newSelectedTab: UiDataModel.Tab)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabModel.java b/src/com/android/deskclock/uidata/TabModel.java
deleted file mode 100644
index 5b878ed..0000000
--- a/src/com/android/deskclock/uidata/TabModel.java
+++ /dev/null
@@ -1,177 +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.uidata;
-
-import android.content.SharedPreferences;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-import static android.view.View.LAYOUT_DIRECTION_RTL;
-import static com.android.deskclock.uidata.UiDataModel.Tab;
-
-/**
- * All tab data is accessed via this model.
- */
-final class TabModel {
-
-    private final SharedPreferences mPrefs;
-
-    /** The listeners to notify when the selected tab is changed. */
-    private final List<TabListener> mTabListeners = new ArrayList<>();
-
-    /** The listeners to notify when the vertical scroll state of the selected tab is changed. */
-    private final List<TabScrollListener> mTabScrollListeners = new ArrayList<>();
-
-    /** The scrolled-to-top state of each tab. */
-    private final boolean[] mTabScrolledToTop = new boolean[Tab.values().length];
-
-    /** An enumerated value indicating the currently selected tab. */
-    private Tab mSelectedTab;
-
-    TabModel(SharedPreferences prefs) {
-        mPrefs = prefs;
-        Arrays.fill(mTabScrolledToTop, true);
-    }
-
-    //
-    // Selected tab
-    //
-
-    /**
-     * @param tabListener to be notified when the selected tab changes
-     */
-    void addTabListener(TabListener tabListener) {
-        mTabListeners.add(tabListener);
-    }
-
-    /**
-     * @param tabListener to no longer be notified when the selected tab changes
-     */
-    void removeTabListener(TabListener tabListener) {
-        mTabListeners.remove(tabListener);
-    }
-
-    /**
-     * @return the number of tabs
-     */
-    int getTabCount() {
-        return Tab.values().length;
-    }
-
-    /**
-     * @param ordinal the ordinal (left-to-right index) of the tab
-     * @return the tab at the given {@code ordinal}
-     */
-    Tab getTab(int ordinal) {
-        return Tab.values()[ordinal];
-    }
-
-    /**
-     * @param position the position of the tab in the user interface
-     * @return the tab at the given {@code ordinal}
-     */
-    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);
-    }
-
-    /**
-     * @return an enumerated value indicating the currently selected primary tab
-     */
-    Tab getSelectedTab() {
-        if (mSelectedTab == null) {
-            mSelectedTab = TabDAO.getSelectedTab(mPrefs);
-        }
-        return mSelectedTab;
-    }
-
-    /**
-     * @param tab an enumerated value indicating the newly selected primary tab
-     */
-    void setSelectedTab(Tab tab) {
-        final Tab oldSelectedTab = getSelectedTab();
-        if (oldSelectedTab != tab) {
-            mSelectedTab = tab;
-            TabDAO.setSelectedTab(mPrefs, tab);
-
-            // Notify of the tab change.
-            for (TabListener tl : mTabListeners) {
-                tl.selectedTabChanged(oldSelectedTab, tab);
-            }
-
-            // Notify of the vertical scroll position change if there is one.
-            final boolean tabScrolledToTop = isTabScrolledToTop(tab);
-            if (isTabScrolledToTop(oldSelectedTab) != tabScrolledToTop) {
-                for (TabScrollListener tsl : mTabScrollListeners) {
-                    tsl.selectedTabScrollToTopChanged(tab, tabScrolledToTop);
-                }
-            }
-        }
-    }
-
-    //
-    // Tab scrolling
-    //
-
-    /**
-     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
-     */
-    void addTabScrollListener(TabScrollListener tabScrollListener) {
-        mTabScrollListeners.add(tabScrollListener);
-    }
-
-    /**
-     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
-     */
-    void removeTabScrollListener(TabScrollListener tabScrollListener) {
-        mTabScrollListeners.remove(tabScrollListener);
-    }
-
-    /**
-     * Updates the scrolling state in the {@link UiDataModel} for this tab.
-     *
-     * @param tab an enumerated value indicating the tab reporting its vertical scroll position
-     * @param scrolledToTop {@code true} iff the vertical scroll position of this tab is at the top
-     */
-    void setTabScrolledToTop(Tab tab, boolean scrolledToTop) {
-        if (isTabScrolledToTop(tab) != scrolledToTop) {
-            mTabScrolledToTop[tab.ordinal()] = scrolledToTop;
-            if (tab == getSelectedTab()) {
-                for (TabScrollListener tsl : mTabScrollListeners) {
-                    tsl.selectedTabScrollToTopChanged(tab, scrolledToTop);
-                }
-            }
-        }
-    }
-
-    /**
-     * @param tab identifies the tab
-     * @return {@code true} iff the content in the given {@code tab} is currently scrolled to top
-     */
-    boolean isTabScrolledToTop(Tab tab) {
-        return mTabScrolledToTop[tab.ordinal()];
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabModel.kt b/src/com/android/deskclock/uidata/TabModel.kt
new file mode 100644
index 0000000..8d0c9f0
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabModel.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.SharedPreferences
+import android.text.TextUtils
+import android.view.View
+
+import java.util.Locale
+
+/**
+ * All tab data is accessed via this model.
+ */
+internal class TabModel(private val mPrefs: SharedPreferences) {
+
+    /** The listeners to notify when the selected tab is changed.  */
+    private val mTabListeners: MutableList<TabListener> = ArrayList()
+
+    /** The listeners to notify when the vertical scroll state of the selected tab is changed.  */
+    private val mTabScrollListeners: MutableList<TabScrollListener> = ArrayList()
+
+    /** The scrolled-to-top state of each tab.  */
+    private val mTabScrolledToTop = BooleanArray(UiDataModel.Tab.values().size)
+
+    /** An enumerated value indicating the currently selected tab.  */
+    private var mSelectedTab: UiDataModel.Tab? = null
+
+    init {
+        mTabScrolledToTop.fill(true)
+    }
+
+    //
+    // Selected tab
+    //
+
+    /**
+     * @param tabListener to be notified when the selected tab changes
+     */
+    fun addTabListener(tabListener: TabListener) {
+        mTabListeners.add(tabListener)
+    }
+
+    /**
+     * @param tabListener to no longer be notified when the selected tab changes
+     */
+    fun removeTabListener(tabListener: TabListener) {
+        mTabListeners.remove(tabListener)
+    }
+
+    /**
+     * @return the number of tabs
+     */
+    val tabCount: Int
+        get() = UiDataModel.Tab.values().size
+
+    /**
+     * @param ordinal the ordinal (left-to-right index) of the tab
+     * @return the tab at the given `ordinal`
+     */
+    fun getTab(ordinal: Int): UiDataModel.Tab {
+        return UiDataModel.Tab.values()[ordinal]
+    }
+
+    /**
+     * @param position the position of the tab in the user interface
+     * @return the tab at the given `ordinal`
+     */
+    fun getTabAt(position: Int): UiDataModel.Tab {
+        val layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+        val ordinal: Int = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+            tabCount - position - 1
+        } else {
+            position
+        }
+        return getTab(ordinal)
+    }
+
+    /**
+     * @return an enumerated value indicating the currently selected primary tab
+     */
+    val selectedTab: UiDataModel.Tab
+        get() {
+            if (mSelectedTab == null) {
+                mSelectedTab = TabDAO.getSelectedTab(mPrefs)
+            }
+            return mSelectedTab!!
+        }
+
+    /**
+     * @param tab an enumerated value indicating the newly selected primary tab
+     */
+    fun setSelectedTab(tab: UiDataModel.Tab) {
+        val oldSelectedTab = selectedTab
+        if (oldSelectedTab != tab) {
+            mSelectedTab = tab
+            TabDAO.setSelectedTab(mPrefs, tab)
+
+            // Notify of the tab change.
+            for (tl in mTabListeners) {
+                tl.selectedTabChanged(oldSelectedTab, tab)
+            }
+
+            // Notify of the vertical scroll position change if there is one.
+            val tabScrolledToTop = isTabScrolledToTop(tab)
+            if (isTabScrolledToTop(oldSelectedTab) != tabScrolledToTop) {
+                for (tsl in mTabScrollListeners) {
+                    tsl.selectedTabScrollToTopChanged(tab, tabScrolledToTop)
+                }
+            }
+        }
+    }
+
+    //
+    // Tab scrolling
+    //
+
+    /**
+     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+     */
+    fun addTabScrollListener(tabScrollListener: TabScrollListener) {
+        mTabScrollListeners.add(tabScrollListener)
+    }
+
+    /**
+     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+     */
+    fun removeTabScrollListener(tabScrollListener: TabScrollListener) {
+        mTabScrollListeners.remove(tabScrollListener)
+    }
+
+    /**
+     * Updates the scrolling state in the [UiDataModel] for this tab.
+     *
+     * @param tab an enumerated value indicating the tab reporting its vertical scroll position
+     * @param scrolledToTop `true` iff the vertical scroll position of this tab is at the top
+     */
+    fun setTabScrolledToTop(tab: UiDataModel.Tab, scrolledToTop: Boolean) {
+        if (isTabScrolledToTop(tab) != scrolledToTop) {
+            mTabScrolledToTop[tab.ordinal] = scrolledToTop
+            if (tab == selectedTab) {
+                for (tsl in mTabScrollListeners) {
+                    tsl.selectedTabScrollToTopChanged(tab, scrolledToTop)
+                }
+            }
+        }
+    }
+
+    /**
+     * @param tab identifies the tab
+     * @return `true` iff the content in the given `tab` is currently scrolled to top
+     */
+    fun isTabScrolledToTop(tab: UiDataModel.Tab): Boolean {
+        return mTabScrolledToTop[tab.ordinal]
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabScrollListener.java b/src/com/android/deskclock/uidata/TabScrollListener.kt
similarity index 63%
rename from src/com/android/deskclock/uidata/TabScrollListener.java
rename to src/com/android/deskclock/uidata/TabScrollListener.kt
index 1a7f155..82d78e4 100644
--- a/src/com/android/deskclock/uidata/TabScrollListener.java
+++ b/src/com/android/deskclock/uidata/TabScrollListener.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,25 +14,22 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.uidata;
-
-import com.android.deskclock.uidata.UiDataModel.Tab;
+package com.android.deskclock.uidata
 
 /**
  * The interface through which interested parties are notified of changes to the vertical scroll
  * position of the selected tab. Callbacks to listener occur when any of these events occur:
  *
  * <ul>
- *     <li>the vertical scroll position of the selected tab is now scrolled to the top</li>
- *     <li>the vertical scroll position of the selected tab is no longer scrolled to the top</li>
- *     <li>the selected tab changed and the new tab scroll state does not match the prior tab</li>
+ *   <li>the vertical scroll position of the selected tab is now scrolled to the top
+ *   <li>the vertical scroll position of the selected tab is no longer scrolled to the top
+ *   <li>the selected tab changed and the new tab scroll state does not match the prior tab
  * </ul>
  */
-public interface TabScrollListener {
-
+interface TabScrollListener {
     /**
      * @param selectedTab an enumerated value indicating the current selected tab
      * @param scrolledToTop indicates whether the current selected tab is scrolled to its top
      */
-    void selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop);
+    fun selectedTabScrollToTopChanged(selectedTab: UiDataModel.Tab, scrolledToTop: Boolean)
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/UiDataModel.java b/src/com/android/deskclock/uidata/UiDataModel.java
deleted file mode 100644
index f7a6917..0000000
--- a/src/com/android/deskclock/uidata/UiDataModel.java
+++ /dev/null
@@ -1,373 +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.uidata;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.Typeface;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.ClockFragment;
-import com.android.deskclock.R;
-import com.android.deskclock.stopwatch.StopwatchFragment;
-import com.android.deskclock.timer.TimerFragment;
-
-import java.util.Calendar;
-
-import static com.android.deskclock.Utils.enforceMainLooper;
-
-/**
- * All application-wide user interface data is accessible through this singleton.
- */
-public final class UiDataModel {
-
-    /** Identifies each of the primary tabs within the application. */
-    public enum Tab {
-        ALARMS(AlarmClockFragment.class, R.drawable.ic_tab_alarm, R.string.menu_alarm),
-        CLOCKS(ClockFragment.class, R.drawable.ic_tab_clock, R.string.menu_clock),
-        TIMERS(TimerFragment.class, R.drawable.ic_tab_timer, R.string.menu_timer),
-        STOPWATCH(StopwatchFragment.class, R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);
-
-        private final String mFragmentClassName;
-        private final @DrawableRes int mIconResId;
-        private final @StringRes int mLabelResId;
-
-        Tab(Class fragmentClass, @DrawableRes int iconResId, @StringRes int labelResId) {
-            mFragmentClassName = fragmentClass.getName();
-            mIconResId = iconResId;
-            mLabelResId = labelResId;
-        }
-
-        public String getFragmentClassName() { return mFragmentClassName; }
-        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. */
-    private static final UiDataModel sUiDataModel = new UiDataModel();
-
-    public static UiDataModel getUiDataModel() {
-        return sUiDataModel;
-    }
-
-    private Context mContext;
-
-    /** The model from which tab data are fetched. */
-    private TabModel mTabModel;
-
-    /** The model from which formatted strings are fetched. */
-    private FormattedStringModel mFormattedStringModel;
-
-    /** The model from which timed callbacks originate. */
-    private PeriodicCallbackModel mPeriodicCallbackModel;
-
-    private UiDataModel() {}
-
-    /**
-     * The context may be set precisely once during the application life.
-     */
-    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);
-        }
-    }
-
-    /**
-     * To display the alarm clock in this font, use the character {@link R.string#clock_emoji}.
-     *
-     * @return a special font containing a glyph that draws an alarm clock
-     */
-    public Typeface getAlarmIconTypeface() {
-        return Typeface.createFromAsset(mContext.getAssets(), "fonts/clock.ttf");
-    }
-
-    //
-    // Formatted Strings
-    //
-
-    /**
-     * 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
-     */
-    public String getFormattedNumber(int value) {
-        enforceMainLooper();
-        return mFormattedStringModel.getFormattedNumber(value);
-    }
-
-    /**
-     * 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
-     */
-    public String getFormattedNumber(int value, int length) {
-        enforceMainLooper();
-        return mFormattedStringModel.getFormattedNumber(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
-     */
-    public String getFormattedNumber(boolean negative, int value, int length) {
-        enforceMainLooper();
-        return mFormattedStringModel.getFormattedNumber(negative, value, length);
-    }
-
-    /**
-     * @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 String getShortWeekday(int calendarDay) {
-        enforceMainLooper();
-        return mFormattedStringModel.getShortWeekday(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.
-     */
-    public String getLongWeekday(int calendarDay) {
-        enforceMainLooper();
-        return mFormattedStringModel.getLongWeekday(calendarDay);
-    }
-
-    //
-    // Animations
-    //
-
-    /**
-     * @return the duration in milliseconds of short animations
-     */
-    public long getShortAnimationDuration() {
-        enforceMainLooper();
-        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
-    //
-
-    /**
-     * @param tabListener to be notified when the selected tab changes
-     */
-    public void addTabListener(TabListener tabListener) {
-        enforceMainLooper();
-        mTabModel.addTabListener(tabListener);
-    }
-
-    /**
-     * @param tabListener to no longer be notified when the selected tab changes
-     */
-    public void removeTabListener(TabListener tabListener) {
-        enforceMainLooper();
-        mTabModel.removeTabListener(tabListener);
-    }
-
-    /**
-     * @return the number of tabs
-     */
-    public int getTabCount() {
-        enforceMainLooper();
-        return mTabModel.getTabCount();
-    }
-
-    /**
-     * @param ordinal the ordinal of the tab
-     * @return the tab at the given {@code ordinal}
-     */
-    public Tab getTab(int ordinal) {
-        enforceMainLooper();
-        return mTabModel.getTab(ordinal);
-    }
-
-    /**
-     * @param position the position of the tab in the user interface
-     * @return the tab at the given {@code ordinal}
-     */
-    public Tab getTabAt(int position) {
-        enforceMainLooper();
-        return mTabModel.getTabAt(position);
-    }
-
-    /**
-     * @return an enumerated value indicating the currently selected primary tab
-     */
-    public Tab getSelectedTab() {
-        enforceMainLooper();
-        return mTabModel.getSelectedTab();
-    }
-
-    /**
-     * @param tab an enumerated value indicating the newly selected primary tab
-     */
-    public void setSelectedTab(Tab tab) {
-        enforceMainLooper();
-        mTabModel.setSelectedTab(tab);
-    }
-
-    /**
-     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
-     */
-    public void addTabScrollListener(TabScrollListener tabScrollListener) {
-        enforceMainLooper();
-        mTabModel.addTabScrollListener(tabScrollListener);
-    }
-
-    /**
-     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
-     */
-    public void removeTabScrollListener(TabScrollListener tabScrollListener) {
-        enforceMainLooper();
-        mTabModel.removeTabScrollListener(tabScrollListener);
-    }
-
-    /**
-     * Updates the scrolling state in the {@link UiDataModel} for this tab.
-     *
-     * @param tab an enumerated value indicating the tab reporting its vertical scroll position
-     * @param scrolledToTop {@code true} iff the vertical scroll position of the tab is at the top
-     */
-    public void setTabScrolledToTop(Tab tab, boolean scrolledToTop) {
-        enforceMainLooper();
-        mTabModel.setTabScrolledToTop(tab, scrolledToTop);
-    }
-
-    /**
-     * @return {@code true} iff the content in the selected tab is currently scrolled to the top
-     */
-    public boolean isSelectedTabScrolledToTop() {
-        enforceMainLooper();
-        return mTabModel.isTabScrolledToTop(getSelectedTab());
-    }
-
-    //
-    // Shortcut Ids
-    //
-
-    /**
-     * @param category which category of shortcut of which to get the id
-     * @param action the desired action to perform
-     * @return the id of the shortcut
-     */
-    public String getShortcutId(@StringRes int category, @StringRes int action) {
-        if (category == R.string.category_stopwatch) {
-            return mContext.getString(category);
-        }
-        return mContext.getString(category) + "_" + mContext.getString(action);
-    }
-
-    //
-    // Timed Callbacks
-    //
-
-    /**
-     * @param runnable to be called every minute
-     * @param offset an offset applied to the minute to control when the callback occurs
-     */
-    public void addMinuteCallback(Runnable runnable, long offset) {
-        enforceMainLooper();
-        mPeriodicCallbackModel.addMinuteCallback(runnable, offset);
-    }
-
-    /**
-     * @param runnable to be called every quarter-hour
-     * @param offset an offset applied to the quarter-hour to control when the callback occurs
-     */
-    public void addQuarterHourCallback(Runnable runnable, long offset) {
-        enforceMainLooper();
-        mPeriodicCallbackModel.addQuarterHourCallback(runnable, offset);
-    }
-
-    /**
-     * @param runnable to be called every hour
-     * @param offset an offset applied to the hour to control when the callback occurs
-     */
-    public void addHourCallback(Runnable runnable, long offset) {
-        enforceMainLooper();
-        mPeriodicCallbackModel.addHourCallback(runnable, offset);
-    }
-
-    /**
-     * @param runnable to be called every midnight
-     * @param offset an offset applied to the midnight to control when the callback occurs
-     */
-    public void addMidnightCallback(Runnable runnable, long offset) {
-        enforceMainLooper();
-        mPeriodicCallbackModel.addMidnightCallback(runnable, offset);
-    }
-
-    /**
-     * @param runnable to no longer be called periodically
-     */
-    public void removePeriodicCallback(Runnable runnable) {
-        enforceMainLooper();
-        mPeriodicCallbackModel.removePeriodicCallback(runnable);
-    }
-}
diff --git a/src/com/android/deskclock/uidata/UiDataModel.kt b/src/com/android/deskclock/uidata/UiDataModel.kt
new file mode 100644
index 0000000..c1561d7
--- /dev/null
+++ b/src/com/android/deskclock/uidata/UiDataModel.kt
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context
+import android.content.SharedPreferences
+import android.graphics.Typeface
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.ClockFragment
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.stopwatch.StopwatchFragment
+import com.android.deskclock.timer.TimerFragment
+
+/**
+ * All application-wide user interface data is accessible through this singleton.
+ */
+class UiDataModel private constructor() {
+    /** Identifies each of the primary tabs within the application.  */
+    enum class Tab(
+        fragmentClass: Class<*>,
+        @DrawableRes val iconResId: Int,
+        @StringRes val labelResId: Int
+    ) {
+        ALARMS(AlarmClockFragment::class.java, R.drawable.ic_tab_alarm, R.string.menu_alarm),
+        CLOCKS(ClockFragment::class.java, R.drawable.ic_tab_clock, R.string.menu_clock),
+        TIMERS(TimerFragment::class.java, R.drawable.ic_tab_timer, R.string.menu_timer),
+        STOPWATCH(StopwatchFragment::class.java,
+                R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);
+
+        val fragmentClassName: String = fragmentClass.name
+    }
+
+    private var mContext: Context? = null
+
+    /** The model from which tab data are fetched.  */
+    private lateinit var mTabModel: TabModel
+
+    /** The model from which formatted strings are fetched.  */
+    private lateinit var mFormattedStringModel: FormattedStringModel
+
+    /** The model from which timed callbacks originate.  */
+    private lateinit var mPeriodicCallbackModel: PeriodicCallbackModel
+
+    /**
+     * The context may be set precisely once during the application life.
+     */
+    fun init(context: Context, prefs: SharedPreferences) {
+        if (mContext !== context) {
+            mContext = context.applicationContext
+
+            mPeriodicCallbackModel = PeriodicCallbackModel(mContext!!)
+            mFormattedStringModel = FormattedStringModel(mContext!!)
+            mTabModel = TabModel(prefs)
+        }
+    }
+
+    /**
+     * To display the alarm clock in this font, use the character [R.string.clock_emoji].
+     *
+     * @return a special font containing a glyph that draws an alarm clock
+     */
+    val alarmIconTypeface: Typeface
+        get() = Typeface.createFromAsset(mContext!!.assets, "fonts/clock.ttf")
+
+    //
+    // Formatted Strings
+    //
+
+    /**
+     * 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 `value` formatted as a String in the current locale
+     * @throws IllegalArgumentException if `value` is negative
+     */
+    fun getFormattedNumber(value: Int): String {
+        Utils.enforceMainLooper()
+        return mFormattedStringModel.getFormattedNumber(value)
+    }
+
+    /**
+     * 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 `value` formatted as a String in the current locale and padded to the
+     * requested `length`
+     * @throws IllegalArgumentException if `value` is negative
+     */
+    fun getFormattedNumber(value: Int, length: Int): String {
+        Utils.enforceMainLooper()
+        return mFormattedStringModel.getFormattedNumber(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 `value` is `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
+     * `negative` is `true` the return value will contain a minus sign and a total
+     * length of `length + 1`.
+     * @return the `value` formatted as a String in the current locale and padded to the
+     * requested `length`
+     * @throws IllegalArgumentException if `value` is negative
+     */
+    fun getFormattedNumber(negative: Boolean, value: Int, length: Int): String {
+        Utils.enforceMainLooper()
+        return mFormattedStringModel.getFormattedNumber(negative, value, length)
+    }
+
+    /**
+     * @param calendarDay any of the following values
+     *
+     *  * [Calendar.SUNDAY]
+     *  * [Calendar.MONDAY]
+     *  * [Calendar.TUESDAY]
+     *  * [Calendar.WEDNESDAY]
+     *  * [Calendar.THURSDAY]
+     *  * [Calendar.FRIDAY]
+     *  * [Calendar.SATURDAY]
+     *
+     * @return single-character version of weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
+     */
+    fun getShortWeekday(calendarDay: Int): String? {
+        Utils.enforceMainLooper()
+        return mFormattedStringModel.getShortWeekday(calendarDay)
+    }
+
+    /**
+     * @param calendarDay any of the following values
+     *
+     *  * [Calendar.SUNDAY]
+     *  * [Calendar.MONDAY]
+     *  * [Calendar.TUESDAY]
+     *  * [Calendar.WEDNESDAY]
+     *  * [Calendar.THURSDAY]
+     *  * [Calendar.FRIDAY]
+     *  * [Calendar.SATURDAY]
+     *
+     * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
+     */
+    fun getLongWeekday(calendarDay: Int): String? {
+        Utils.enforceMainLooper()
+        return mFormattedStringModel.getLongWeekday(calendarDay)
+    }
+
+    //
+    // Animations
+    //
+
+    /**
+     * @return the duration in milliseconds of short animations
+     */
+    val shortAnimationDuration: Long
+        get() {
+            Utils.enforceMainLooper()
+            return mContext!!.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
+        }
+
+    /**
+     * @return the duration in milliseconds of long animations
+     */
+    val longAnimationDuration: Long
+        get() {
+            Utils.enforceMainLooper()
+            return mContext!!.resources.getInteger(android.R.integer.config_longAnimTime).toLong()
+        }
+
+    //
+    // Tabs
+    //
+
+    /**
+     * @param tabListener to be notified when the selected tab changes
+     */
+    fun addTabListener(tabListener: TabListener) {
+        Utils.enforceMainLooper()
+        mTabModel.addTabListener(tabListener)
+    }
+
+    /**
+     * @param tabListener to no longer be notified when the selected tab changes
+     */
+    fun removeTabListener(tabListener: TabListener) {
+        Utils.enforceMainLooper()
+        mTabModel.removeTabListener(tabListener)
+    }
+
+    /**
+     * @return the number of tabs
+     */
+    val tabCount: Int
+        get() {
+            Utils.enforceMainLooper()
+            return mTabModel.tabCount
+        }
+
+    /**
+     * @param ordinal the ordinal of the tab
+     * @return the tab at the given `ordinal`
+     */
+    fun getTab(ordinal: Int): Tab {
+        Utils.enforceMainLooper()
+        return mTabModel.getTab(ordinal)
+    }
+
+    /**
+     * @param position the position of the tab in the user interface
+     * @return the tab at the given `ordinal`
+     */
+    fun getTabAt(position: Int): Tab {
+        Utils.enforceMainLooper()
+        return mTabModel.getTabAt(position)
+    }
+
+    var selectedTab: Tab
+        /**
+         * @return an enumerated value indicating the currently selected primary tab
+         */
+        get() {
+            Utils.enforceMainLooper()
+            return mTabModel.selectedTab
+        }
+        /**
+         * @param tab an enumerated value indicating the newly selected primary tab
+         */
+        set(tab) {
+            Utils.enforceMainLooper()
+            mTabModel.setSelectedTab(tab)
+        }
+
+    /**
+     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+     */
+    fun addTabScrollListener(tabScrollListener: TabScrollListener) {
+        Utils.enforceMainLooper()
+        mTabModel.addTabScrollListener(tabScrollListener)
+    }
+
+    /**
+     * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+     */
+    fun removeTabScrollListener(tabScrollListener: TabScrollListener) {
+        Utils.enforceMainLooper()
+        mTabModel.removeTabScrollListener(tabScrollListener)
+    }
+
+    /**
+     * Updates the scrolling state in the [UiDataModel] for this tab.
+     *
+     * @param tab an enumerated value indicating the tab reporting its vertical scroll position
+     * @param scrolledToTop `true` iff the vertical scroll position of the tab is at the top
+     */
+    fun setTabScrolledToTop(tab: Tab, scrolledToTop: Boolean) {
+        Utils.enforceMainLooper()
+        mTabModel.setTabScrolledToTop(tab, scrolledToTop)
+    }
+
+    /**
+     * @return `true` iff the content in the selected tab is currently scrolled to the top
+     */
+    val isSelectedTabScrolledToTop: Boolean
+        get() {
+            Utils.enforceMainLooper()
+            return mTabModel.isTabScrolledToTop(selectedTab)
+        }
+
+    //
+    // Shortcut Ids
+    //
+
+    /**
+     * @param category which category of shortcut of which to get the id
+     * @param action the desired action to perform
+     * @return the id of the shortcut
+     */
+    fun getShortcutId(@StringRes category: Int, @StringRes action: Int): String {
+        return if (category == R.string.category_stopwatch) {
+            mContext!!.getString(category)
+        } else {
+            mContext!!.getString(category) + "_" + mContext!!.getString(action)
+        }
+    }
+
+    //
+    // Timed Callbacks
+    //
+
+    /**
+     * @param runnable to be called every minute
+     * @param offset an offset applied to the minute to control when the callback occurs
+     */
+    fun addMinuteCallback(runnable: Runnable, offset: Long) {
+        Utils.enforceMainLooper()
+        mPeriodicCallbackModel.addMinuteCallback(runnable, offset)
+    }
+
+    /**
+     * @param runnable to be called every quarter-hour
+     */
+    fun addQuarterHourCallback(runnable: Runnable) {
+        Utils.enforceMainLooper()
+        mPeriodicCallbackModel.addQuarterHourCallback(runnable)
+    }
+
+    /**
+     * @param runnable to be called every hour
+     */
+    fun addHourCallback(runnable: Runnable) {
+        Utils.enforceMainLooper()
+        mPeriodicCallbackModel.addHourCallback(runnable)
+    }
+
+    /**
+     * @param runnable to be called every midnight
+     */
+    fun addMidnightCallback(runnable: Runnable) {
+        Utils.enforceMainLooper()
+        mPeriodicCallbackModel.addMidnightCallback(runnable)
+    }
+
+    /**
+     * @param runnable to no longer be called periodically
+     */
+    fun removePeriodicCallback(runnable: Runnable) {
+        Utils.enforceMainLooper()
+        mPeriodicCallbackModel.removePeriodicCallback(runnable)
+    }
+
+    companion object {
+        /** The single instance of this data model that exists for the life of the application.  */
+        val sUiDataModel = UiDataModel()
+
+        @get:JvmStatic
+        val uiDataModel
+            get() = sUiDataModel
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/AutoSizingTextClock.java b/src/com/android/deskclock/widget/AutoSizingTextClock.java
deleted file mode 100644
index 1a70da9..0000000
--- a/src/com/android/deskclock/widget/AutoSizingTextClock.java
+++ /dev/null
@@ -1,78 +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.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/AutoSizingTextClock.kt b/src/com/android/deskclock/widget/AutoSizingTextClock.kt
new file mode 100644
index 0000000..c332762
--- /dev/null
+++ b/src/com/android/deskclock/widget/AutoSizingTextClock.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.
+ */
+class AutoSizingTextClock @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : TextClock(context, attrs, defStyleAttr) {
+    private val mTextSizeHelper: TextSizeHelper? = TextSizeHelper(this)
+
+    private var mSuppressLayout = false
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        mTextSizeHelper!!.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+    }
+
+    override fun onTextChanged(
+        text: CharSequence,
+        start: Int,
+        lengthBefore: Int,
+        lengthAfter: Int
+    ) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter)
+        if (mTextSizeHelper != null) {
+            if (lengthBefore != lengthAfter) {
+                mSuppressLayout = false
+            }
+            mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter)
+        } else {
+            requestLayout()
+        }
+    }
+
+    override fun setText(text: CharSequence, type: BufferType) {
+        mSuppressLayout = true
+        super.setText(text, type)
+        mSuppressLayout = false
+    }
+
+    override fun 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
deleted file mode 100644
index 7e41997..0000000
--- a/src/com/android/deskclock/widget/AutoSizingTextView.java
+++ /dev/null
@@ -1,65 +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.widget;
-
-import android.content.Context;
-import androidx.appcompat.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/AutoSizingTextView.kt b/src/com/android/deskclock/widget/AutoSizingTextView.kt
new file mode 100644
index 0000000..d393542
--- /dev/null
+++ b/src/com/android/deskclock/widget/AutoSizingTextView.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.appcompat.widget.AppCompatTextView
+
+/**
+ * A TextView which automatically re-sizes its text to fit within its boundaries.
+ */
+class AutoSizingTextView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = android.R.attr.textViewStyle
+) : AppCompatTextView(context, attrs, defStyleAttr) {
+    private val mTextSizeHelper: TextSizeHelper? = TextSizeHelper(this)
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        mTextSizeHelper!!.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+    }
+
+    override fun onTextChanged(
+        text: CharSequence?,
+        start: Int,
+        lengthBefore: Int,
+        lengthAfter: Int
+    ) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter)
+        if (mTextSizeHelper != null) {
+            mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter)
+        } else {
+            requestLayout()
+        }
+    }
+
+    override fun 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
deleted file mode 100644
index b3a4675..0000000
--- a/src/com/android/deskclock/widget/CircleView.java
+++ /dev/null
@@ -1,341 +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.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.util.AttributeSet;
-import android.util.Property;
-import android.view.Gravity;
-import android.view.View;
-
-import com.android.deskclock.R;
-
-/**
- * A {@link View} that draws primitive circles.
- */
-public class CircleView extends View {
-
-    /**
-     * A Property wrapper around the fillColor functionality handled by the
-     * {@link #setFillColor(int)} and {@link #getFillColor()} methods.
-     */
-    public final static Property<CircleView, Integer> FILL_COLOR =
-            new Property<CircleView, Integer>(Integer.class, "fillColor") {
-        @Override
-        public Integer get(CircleView view) {
-            return view.getFillColor();
-        }
-
-        @Override
-        public void set(CircleView view, Integer value) {
-            view.setFillColor(value);
-        }
-    };
-
-    /**
-     * A Property wrapper around the radius functionality handled by the
-     * {@link #setRadius(float)} and {@link #getRadius()} methods.
-     */
-    public final static Property<CircleView, Float> RADIUS =
-            new Property<CircleView, Float>(Float.class, "radius") {
-        @Override
-        public Float get(CircleView view) {
-            return view.getRadius();
-        }
-
-        @Override
-        public void set(CircleView view, Float value) {
-            view.setRadius(value);
-        }
-    };
-
-    /**
-     * The {@link Paint} used to draw the circle.
-     */
-    private final Paint mCirclePaint = new Paint();
-
-    private int mGravity;
-    private float mCenterX;
-    private float mCenterY;
-    private float mRadius;
-
-    public CircleView(Context context) {
-        this(context, null /* attrs */);
-    }
-
-    public CircleView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0 /* defStyleAttr */);
-    }
-
-    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-
-        final TypedArray a = context.obtainStyledAttributes(
-                attrs, R.styleable.CircleView, defStyleAttr, 0 /* defStyleRes */);
-
-        mGravity = a.getInt(R.styleable.CircleView_android_gravity, Gravity.NO_GRAVITY);
-        mCenterX = a.getDimension(R.styleable.CircleView_centerX, 0.0f);
-        mCenterY = a.getDimension(R.styleable.CircleView_centerY, 0.0f);
-        mRadius = a.getDimension(R.styleable.CircleView_radius, 0.0f);
-
-        mCirclePaint.setColor(a.getColor(R.styleable.CircleView_fillColor, Color.WHITE));
-
-        a.recycle();
-    }
-
-    @Override
-    public void onRtlPropertiesChanged(int layoutDirection) {
-        super.onRtlPropertiesChanged(layoutDirection);
-
-        if (mGravity != Gravity.NO_GRAVITY) {
-            applyGravity(mGravity, layoutDirection);
-        }
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-
-        if (mGravity != Gravity.NO_GRAVITY) {
-            applyGravity(mGravity, getLayoutDirection());
-        }
-    }
-
-    @Override
-    protected void onDraw(Canvas canvas) {
-        super.onDraw(canvas);
-
-        // draw the circle, duh
-        canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);
-    }
-
-    @Override
-    public boolean hasOverlappingRendering() {
-        // only if we have a background, which we shouldn't...
-        return getBackground() != null;
-    }
-
-    /**
-     * @return the current {@link Gravity} used to align/size the circle
-     */
-    public final int getGravity() {
-        return mGravity;
-    }
-
-    /**
-     * Describes how to align/size the circle relative to the view's bounds. Defaults to
-     * {@link Gravity#NO_GRAVITY}.
-     * <p/>
-     * Note: using {@link #setCenterX(float)}, {@link #setCenterY(float)}, or
-     * {@link #setRadius(float)} will automatically clear any conflicting gravity bits.
-     *
-     * @param gravity the {@link Gravity} flags to use
-     * @return this object, allowing calls to methods in this class to be chained
-     * @see R.styleable#CircleView_android_gravity
-     */
-    public CircleView setGravity(int gravity) {
-        if (mGravity != gravity) {
-            mGravity = gravity;
-
-            if (gravity != Gravity.NO_GRAVITY && isLayoutDirectionResolved()) {
-                applyGravity(gravity, getLayoutDirection());
-            }
-        }
-        return this;
-    }
-
-    /**
-     * @return the ARGB color used to fill the circle
-     */
-    public final int getFillColor() {
-        return mCirclePaint.getColor();
-    }
-
-    /**
-     * Sets the ARGB color used to fill the circle and invalidates only the affected area.
-     *
-     * @param color the ARGB color to use
-     * @return this object, allowing calls to methods in this class to be chained
-     * @see R.styleable#CircleView_fillColor
-     */
-    public CircleView setFillColor(int color) {
-        if (mCirclePaint.getColor() != color) {
-            mCirclePaint.setColor(color);
-
-            // invalidate the current area
-            invalidate(mCenterX, mCenterY, mRadius);
-        }
-        return this;
-    }
-
-    /**
-     * 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
-     * @return this object, allowing calls to methods in this class to be chained
-     * @see R.styleable#CircleView_centerX
-     */
-    public CircleView setCenterX(float centerX) {
-        final float oldCenterX = mCenterX;
-        if (oldCenterX != centerX) {
-            mCenterX = centerX;
-
-            // invalidate the old/new areas
-            invalidate(oldCenterX, mCenterY, mRadius);
-            invalidate(centerX, mCenterY, mRadius);
-        }
-
-        // clear the horizontal gravity flags
-        mGravity &= ~Gravity.HORIZONTAL_GRAVITY_MASK;
-
-        return this;
-    }
-
-    /**
-     * 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
-     * @return this object, allowing calls to methods in this class to be chained
-     * @see R.styleable#CircleView_centerY
-     */
-    public CircleView setCenterY(float centerY) {
-        final float oldCenterY = mCenterY;
-        if (oldCenterY != centerY) {
-            mCenterY = centerY;
-
-            // invalidate the old/new areas
-            invalidate(mCenterX, oldCenterY, mRadius);
-            invalidate(mCenterX, centerY, mRadius);
-        }
-
-        // clear the vertical gravity flags
-        mGravity &= ~Gravity.VERTICAL_GRAVITY_MASK;
-
-        return this;
-    }
-
-    /**
-     * @return the radius of the circle
-     */
-    public final float getRadius() {
-        return mRadius;
-    }
-
-    /**
-     * Sets the radius of the circle and invalidates only the affected area.
-     *
-     * @param radius the radius to use
-     * @return this object, allowing calls to methods in this class to be chained
-     * @see R.styleable#CircleView_radius
-     */
-    public CircleView setRadius(float radius) {
-        final float oldRadius = mRadius;
-        if (oldRadius != radius) {
-            mRadius = radius;
-
-            // invalidate the old/new areas
-            invalidate(mCenterX, mCenterY, oldRadius);
-            if (radius > oldRadius) {
-                invalidate(mCenterX, mCenterY, radius);
-            }
-        }
-
-        // clear the fill gravity flags
-        if ((mGravity & Gravity.FILL_HORIZONTAL) == Gravity.FILL_HORIZONTAL) {
-            mGravity &= ~Gravity.FILL_HORIZONTAL;
-        }
-        if ((mGravity & Gravity.FILL_VERTICAL) == Gravity.FILL_VERTICAL) {
-            mGravity &= ~Gravity.FILL_VERTICAL;
-        }
-
-        return this;
-    }
-
-    /**
-     * Invalidates the rectangular area that circumscribes the circle defined by {@code centerX},
-     * {@code centerY}, and {@code radius}.
-     */
-    private void invalidate(float centerX, float centerY, float radius) {
-        invalidate((int) (centerX - radius - 0.5f), (int) (centerY - radius - 0.5f),
-                (int) (centerX + radius + 0.5f), (int) (centerY + radius + 0.5f));
-    }
-
-    /**
-     * Applies the specified {@code gravity} and {@code layoutDirection}, adjusting the alignment
-     * and size of the circle depending on the resolved {@link Gravity} flags. Also invalidates the
-     * affected area if necessary.
-     *
-     * @param gravity the {@link Gravity} the {@link Gravity} flags to use
-     * @param layoutDirection the layout direction used to resolve the absolute gravity
-     */
-    @SuppressLint("RtlHardcoded")
-    private void applyGravity(int gravity, int layoutDirection) {
-        final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
-
-        final float oldRadius = mRadius;
-        final float oldCenterX = mCenterX;
-        final float oldCenterY = mCenterY;
-
-        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
-            case Gravity.LEFT:
-                mCenterX = 0.0f;
-                break;
-            case Gravity.CENTER_HORIZONTAL:
-            case Gravity.FILL_HORIZONTAL:
-                mCenterX = getWidth() / 2.0f;
-                break;
-            case Gravity.RIGHT:
-                mCenterX = getWidth();
-                break;
-        }
-
-        switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
-            case Gravity.TOP:
-                mCenterY = 0.0f;
-                break;
-            case Gravity.CENTER_VERTICAL:
-            case Gravity.FILL_VERTICAL:
-                mCenterY = getHeight() / 2.0f;
-                break;
-            case Gravity.BOTTOM:
-                mCenterY = getHeight();
-                break;
-        }
-
-        switch (absoluteGravity & Gravity.FILL) {
-            case Gravity.FILL:
-                mRadius = Math.min(getWidth(), getHeight()) / 2.0f;
-                break;
-            case Gravity.FILL_HORIZONTAL:
-                mRadius = getWidth() / 2.0f;
-                break;
-            case Gravity.FILL_VERTICAL:
-                mRadius = getHeight() / 2.0f;
-                break;
-        }
-
-        if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != mRadius) {
-            invalidate(oldCenterX, oldCenterY, oldRadius);
-            invalidate(mCenterX, mCenterY, mRadius);
-        }
-    }
-}
diff --git a/src/com/android/deskclock/widget/CircleView.kt b/src/com/android/deskclock/widget/CircleView.kt
new file mode 100644
index 0000000..062a284
--- /dev/null
+++ b/src/com/android/deskclock/widget/CircleView.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.util.Property
+import android.view.Gravity
+import android.view.View
+
+import com.android.deskclock.R
+
+import kotlin.math.min
+
+/**
+ * A [View] that draws primitive circles.
+ */
+class CircleView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+    /** The [Paint] used to draw the circle. */
+    private val mCirclePaint = Paint()
+
+    /** the current [Gravity] used to align/size the circle */
+    var gravity: Int
+        private set
+
+    private var mCenterX: Float
+    private var mCenterY: Float
+
+    /** the radius of the circle */
+    var radius: Float
+        private set
+
+    init {
+        val a = context.obtainStyledAttributes(attrs, R.styleable.CircleView, defStyleAttr, 0)
+
+        gravity = a.getInt(R.styleable.CircleView_android_gravity, Gravity.NO_GRAVITY)
+        mCenterX = a.getDimension(R.styleable.CircleView_centerX, 0.0f)
+        mCenterY = a.getDimension(R.styleable.CircleView_centerY, 0.0f)
+        radius = a.getDimension(R.styleable.CircleView_radius, 0.0f)
+
+        mCirclePaint.color = a.getColor(R.styleable.CircleView_fillColor, Color.WHITE)
+
+        a.recycle()
+    }
+
+    override fun onRtlPropertiesChanged(layoutDirection: Int) {
+        super.onRtlPropertiesChanged(layoutDirection)
+
+        if (gravity != Gravity.NO_GRAVITY) {
+            applyGravity(gravity, layoutDirection)
+        }
+    }
+
+    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+        super.onLayout(changed, left, top, right, bottom)
+
+        if (gravity != Gravity.NO_GRAVITY) {
+            applyGravity(gravity, layoutDirection)
+        }
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        // draw the circle, duh
+        canvas.drawCircle(mCenterX, mCenterY, radius, mCirclePaint)
+    }
+
+    override fun hasOverlappingRendering(): Boolean {
+        // only if we have a background, which we shouldn't...
+        return background != null
+    }
+
+    /**
+     * Describes how to align/size the circle relative to the view's bounds. Defaults to
+     * [Gravity.NO_GRAVITY].
+     *
+     * Note: using [.setCenterX], [.setCenterY], or
+     * [.setRadius] will automatically clear any conflicting gravity bits.
+     *
+     * @param gravity the [Gravity] flags to use
+     * @return this object, allowing calls to methods in this class to be chained
+     * @see R.styleable.CircleView_android_gravity
+     */
+    fun setGravity(gravity: Int): CircleView {
+        if (this.gravity != gravity) {
+            this.gravity = gravity
+
+            if (gravity != Gravity.NO_GRAVITY && isLayoutDirectionResolved) {
+                applyGravity(gravity, layoutDirection)
+            }
+        }
+        return this
+    }
+
+    /**
+     * @return the ARGB color used to fill the circle
+     */
+    val fillColor: Int
+        get() = mCirclePaint.color
+
+    /**
+     * Sets the ARGB color used to fill the circle and invalidates only the affected area.
+     *
+     * @param color the ARGB color to use
+     * @return this object, allowing calls to methods in this class to be chained
+     * @see R.styleable.CircleView_fillColor
+     */
+    fun setFillColor(color: Int): CircleView {
+        if (mCirclePaint.color != color) {
+            mCirclePaint.color = color
+
+            // invalidate the current area
+            invalidate(mCenterX, mCenterY, radius)
+        }
+        return this
+    }
+
+    /**
+     * 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
+     * @return this object, allowing calls to methods in this class to be chained
+     * @see R.styleable.CircleView_centerX
+     */
+    fun setCenterX(centerX: Float): CircleView {
+        val oldCenterX = mCenterX
+        if (oldCenterX != centerX) {
+            mCenterX = centerX
+
+            // invalidate the old/new areas
+            invalidate(oldCenterX, mCenterY, radius)
+            invalidate(centerX, mCenterY, radius)
+        }
+
+        // clear the horizontal gravity flags
+        gravity = gravity and Gravity.HORIZONTAL_GRAVITY_MASK.inv()
+
+        return this
+    }
+
+    /**
+     * 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
+     * @return this object, allowing calls to methods in this class to be chained
+     * @see R.styleable.CircleView_centerY
+     */
+    fun setCenterY(centerY: Float): CircleView {
+        val oldCenterY = mCenterY
+        if (oldCenterY != centerY) {
+            mCenterY = centerY
+
+            // invalidate the old/new areas
+            invalidate(mCenterX, oldCenterY, radius)
+            invalidate(mCenterX, centerY, radius)
+        }
+
+        // clear the vertical gravity flags
+        gravity = gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()
+
+        return this
+    }
+
+    /**
+     * Sets the radius of the circle and invalidates only the affected area.
+     *
+     * @param radius the radius to use
+     * @return this object, allowing calls to methods in this class to be chained
+     * @see R.styleable.CircleView_radius
+     */
+    fun setRadius(radius: Float): CircleView {
+        val oldRadius = this.radius
+        if (oldRadius != radius) {
+            this.radius = radius
+
+            // invalidate the old/new areas
+            invalidate(mCenterX, mCenterY, oldRadius)
+            if (radius > oldRadius) {
+                invalidate(mCenterX, mCenterY, radius)
+            }
+        }
+
+        // clear the fill gravity flags
+        if (gravity and Gravity.FILL_HORIZONTAL == Gravity.FILL_HORIZONTAL) {
+            gravity = gravity and Gravity.FILL_HORIZONTAL.inv()
+        }
+        if (gravity and Gravity.FILL_VERTICAL == Gravity.FILL_VERTICAL) {
+            gravity = gravity and Gravity.FILL_VERTICAL.inv()
+        }
+
+        return this
+    }
+
+    /**
+     * Invalidates the rectangular area that circumscribes the circle defined by `centerX`,
+     * `centerY`, and `radius`.
+     */
+    private fun invalidate(centerX: Float, centerY: Float, radius: Float) {
+        invalidate((centerX - radius - 0.5f).toInt(), (centerY - radius - 0.5f).toInt(),
+                (centerX + radius + 0.5f).toInt(), (centerY + radius + 0.5f).toInt())
+    }
+
+    /**
+     * Applies the specified `gravity` and `layoutDirection`, adjusting the alignment
+     * and size of the circle depending on the resolved [Gravity] flags. Also invalidates the
+     * affected area if necessary.
+     *
+     * @param gravity the [Gravity] the [Gravity] flags to use
+     * @param layoutDirection the layout direction used to resolve the absolute gravity
+     */
+    @SuppressLint("RtlHardcoded")
+    private fun applyGravity(gravity: Int, layoutDirection: Int) {
+        val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
+
+        val oldRadius = radius
+        val oldCenterX = mCenterX
+        val oldCenterY = mCenterY
+
+        when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
+            Gravity.LEFT -> mCenterX = 0.0f
+            Gravity.CENTER_HORIZONTAL, Gravity.FILL_HORIZONTAL -> mCenterX = width / 2.0f
+            Gravity.RIGHT -> mCenterX = width.toFloat()
+        }
+
+        when (absoluteGravity and Gravity.VERTICAL_GRAVITY_MASK) {
+            Gravity.TOP -> mCenterY = 0.0f
+            Gravity.CENTER_VERTICAL, Gravity.FILL_VERTICAL -> mCenterY = height / 2.0f
+            Gravity.BOTTOM -> mCenterY = height.toFloat()
+        }
+
+        when (absoluteGravity and Gravity.FILL) {
+            Gravity.FILL -> radius = min(width, height) / 2.0f
+            Gravity.FILL_HORIZONTAL -> radius = width / 2.0f
+            Gravity.FILL_VERTICAL -> radius = height / 2.0f
+        }
+
+        if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != radius) {
+            invalidate(oldCenterX, oldCenterY, oldRadius)
+            invalidate(mCenterX, mCenterY, radius)
+        }
+    }
+
+    companion object {
+        /**
+         * A Property wrapper around the fillColor functionality handled by the
+         * [.setFillColor] and [.getFillColor] methods.
+         */
+        @JvmField
+        val FILL_COLOR: Property<CircleView, Int> =
+                object : Property<CircleView, Int>(Int::class.java, "fillColor") {
+                    override fun get(view: CircleView): Int {
+                        return view.fillColor
+                    }
+
+                    override fun set(view: CircleView, value: Int) {
+                        view.setFillColor(value)
+                    }
+                }
+
+        /**
+         * A Property wrapper around the radius functionality handled by the
+         * [.setRadius] and [.getRadius] methods.
+         */
+        @JvmField
+        val RADIUS: Property<CircleView, Float> =
+                object : Property<CircleView, Float>(Float::class.java, "radius") {
+                    override fun get(view: CircleView): Float {
+                        return view.radius
+                    }
+
+                    override fun set(view: CircleView, value: Float) {
+                        view.setRadius(value)
+                    }
+                }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/EllipsizeLayout.java b/src/com/android/deskclock/widget/EllipsizeLayout.java
deleted file mode 100644
index 6c5dd33..0000000
--- a/src/com/android/deskclock/widget/EllipsizeLayout.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package com.android.deskclock.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * When this layout is in the Horizontal orientation and one and only one child is a TextView with a
- * non-null android:ellipsize, this layout will reduce android:maxWidth of that TextView to ensure
- * the siblings are not truncated. This class is useful when that ellipsize-text-view "starts"
- * before other children of this view group. This layout has no effect if:
- * <ul>
- *     <li>the orientation is not horizontal</li>
- *     <li>any child has weights.</li>
- *     <li>more than one child has a non-null android:ellipsize.</li>
- * </ul>
- *
- * <p>The purpose of this horizontal-linear-layout is to ensure that when the sum of widths of the
- * children are greater than this parent, the maximum width of the ellipsize-text-view, is reduced
- * so that no siblings are truncated.</p>
- *
- * <p>For example: Given Text1 has android:ellipsize="end" and Text2 has android:ellipsize="none",
- * as Text1 and/or Text2 grow in width, both will consume more width until Text2 hits the end
- * margin, then Text1 will cease to grow and instead shrink to accommodate any further growth in
- * Text2.</p>
- * <ul>
- * <li>|[text1]|[text2]              |</li>
- * <li>|[text1 text1]|[text2 text2]  |</li>
- * <li>|[text...]|[text2 text2 text2]|</li>
- * </ul>
- */
-public class EllipsizeLayout extends LinearLayout {
-
-    @SuppressWarnings("unused")
-    public EllipsizeLayout(Context context) {
-        this(context, null);
-    }
-
-    public EllipsizeLayout(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    /**
-     * This override only acts when the LinearLayout is in the Horizontal orientation and is in it's
-     * final measurement pass(MeasureSpec.EXACTLY). In this case only, this class
-     * <ul>
-     *     <li>Identifies the one TextView child with the non-null android:ellipsize.</li>
-     *     <li>Re-measures the needed width of all children (by calling measureChildWithMargins with
-     *     the width measure specification to MeasureSpec.UNSPECIFIED.)</li>
-     *     <li>Sums the children's widths.</li>
-     *     <li>Whenever the sum of the children's widths is greater than this parent was allocated,
-     *     the maximum width of the one TextView child with the non-null android:ellipsize is
-     *     reduced.</li>
-     * </ul>
-     *
-     * @param widthMeasureSpec horizontal space requirements as imposed by the parent
-     * @param heightMeasureSpec vertical space requirements as imposed by the parent
-     */
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        if (getOrientation() == HORIZONTAL
-                && (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY)) {
-            int totalLength = 0;
-            // If any of the constraints of this class are exceeded, outOfSpec becomes true
-            // and the no alterations are made to the ellipsize-text-view.
-            boolean outOfSpec = false;
-            TextView ellipsizeView = null;
-            final int count = getChildCount();
-            final int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
-            final int queryWidthMeasureSpec = MeasureSpec.
-                    makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
-
-            for (int ii = 0; ii < count && !outOfSpec; ++ii) {
-                final View child = getChildAt(ii);
-                if (child != null && child.getVisibility() != GONE) {
-                    // Identify the ellipsize view
-                    if (child instanceof TextView) {
-                        final TextView tv = (TextView) child;
-                        if (tv.getEllipsize() != null) {
-                            if (ellipsizeView == null) {
-                                ellipsizeView = tv;
-                                // Clear the maximum width on ellipsizeView before measurement
-                                ellipsizeView.setMaxWidth(Integer.MAX_VALUE);
-                            } else {
-                                // TODO: support multiple android:ellipsize
-                                outOfSpec = true;
-                            }
-                        }
-                    }
-                    // Ask the child to measure itself
-                    measureChildWithMargins(child, queryWidthMeasureSpec, 0, heightMeasureSpec, 0);
-
-                    // Get the layout parameters to check for a weighted width and to add the
-                    // child's margins to the total length.
-                    final LinearLayout.LayoutParams layoutParams =
-                            (LinearLayout.LayoutParams) child.getLayoutParams();
-                    if (layoutParams != null) {
-                        outOfSpec |= (layoutParams.weight > 0f);
-                        totalLength += child.getMeasuredWidth()
-                                + layoutParams.leftMargin + layoutParams.rightMargin;
-                    } else {
-                        outOfSpec = true;
-                    }
-                }
-            }
-            // Last constraint test
-            outOfSpec |= (ellipsizeView == null) || (totalLength == 0);
-
-            if (!outOfSpec && totalLength > parentWidth) {
-                int maxWidth = ellipsizeView.getMeasuredWidth() - (totalLength - parentWidth);
-                // TODO: Respect android:minWidth (easy with @TargetApi(16))
-                final int minWidth = 0;
-                if (maxWidth < minWidth) {
-                    maxWidth = minWidth;
-                }
-                ellipsizeView.setMaxWidth(maxWidth);
-            }
-        }
-
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-    }
-}
diff --git a/src/com/android/deskclock/widget/EllipsizeLayout.kt b/src/com/android/deskclock/widget/EllipsizeLayout.kt
new file mode 100644
index 0000000..3d31477
--- /dev/null
+++ b/src/com/android/deskclock/widget/EllipsizeLayout.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+
+/**
+ * When this layout is in the Horizontal orientation and one and only one child is a TextView with a
+ * non-null android:ellipsize, this layout will reduce android:maxWidth of that TextView to ensure
+ * the siblings are not truncated. This class is useful when that ellipsize-text-view "starts"
+ * before other children of this view group. This layout has no effect if:
+ * <ul>
+ *     <li>the orientation is not horizontal</li>
+ *     <li>any child has weights.</li>
+ *     <li>more than one child has a non-null android:ellipsize.</li>
+ * </ul>
+ *
+ * The purpose of this horizontal-linear-layout is to ensure that when the sum of widths of the
+ * children are greater than this parent, the maximum width of the ellipsize-text-view, is reduced
+ * so that no siblings are truncated.
+ *
+ *
+ * For example: Given Text1 has android:ellipsize="end" and Text2 has android:ellipsize="none",
+ * as Text1 and/or Text2 grow in width, both will consume more width until Text2 hits the end
+ * margin, then Text1 will cease to grow and instead shrink to accommodate any further growth in
+ * Text2.
+ * <ul>
+ * <li>|[text1]|[text2]              |</li>
+ * <li>|[text1 text1]|[text2 text2]  |</li>
+ * <li>|[text... ]|[text2 text2 text2]|</li>
+ * </ul>
+ */
+class EllipsizeLayout @JvmOverloads constructor(
+    context: Context?,
+    attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+    /**
+     * This override only acts when the LinearLayout is in the Horizontal orientation and is in it's
+     * final measurement pass(MeasureSpec.EXACTLY). In this case only, this class
+     *
+     *  * Identifies the one TextView child with the non-null android:ellipsize.
+     *  * Re-measures the needed width of all children (by calling measureChildWithMargins with
+     * the width measure specification to MeasureSpec.UNSPECIFIED.)
+     *  * Sums the children's widths.
+     *  * Whenever the sum of the children's widths is greater than this parent was allocated,
+     * the maximum width of the one TextView child with the non-null android:ellipsize is
+     * reduced.
+     *
+     *
+     * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+     * @param heightMeasureSpec vertical space requirements as imposed by the parent
+     */
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        if (orientation == HORIZONTAL &&
+                MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+            var totalLength = 0
+            // If any of the constraints of this class are exceeded, outOfSpec becomes true
+            // and the no alterations are made to the ellipsize-text-view.
+            var outOfSpec = false
+            var ellipsizeView: TextView? = null
+            val count = childCount
+            val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
+            val queryWidthMeasureSpec =
+                    MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec),
+                            MeasureSpec.UNSPECIFIED)
+
+            var ii = 0
+            while (ii < count && !outOfSpec) {
+                val child = getChildAt(ii)
+                if (child != null && child.visibility != View.GONE) {
+                    // Identify the ellipsize view
+                    if (child is TextView) {
+                        val tv = child
+                        if (tv.ellipsize != null) {
+                            if (ellipsizeView == null) {
+                                ellipsizeView = tv
+                                // Clear the maximum width on ellipsizeView before measurement
+                                ellipsizeView.maxWidth = Int.MAX_VALUE
+                            } else {
+                                // TODO: support multiple android:ellipsize
+                                outOfSpec = true
+                            }
+                        }
+                    }
+                    // Ask the child to measure itself
+                    measureChildWithMargins(child, queryWidthMeasureSpec, 0, heightMeasureSpec, 0)
+
+                    // Get the layout parameters to check for a weighted width and to add the
+                    // child's margins to the total length.
+                    val layoutParams = child.layoutParams as LayoutParams?
+                    if (layoutParams != null) {
+                        outOfSpec = outOfSpec or (layoutParams.weight > 0f)
+                        totalLength += (child.measuredWidth +
+                                layoutParams.leftMargin + layoutParams.rightMargin)
+                    } else {
+                        outOfSpec = true
+                    }
+                }
+                ++ii
+            }
+            // Last constraint test
+            outOfSpec = outOfSpec or (ellipsizeView == null || totalLength == 0)
+
+            if (!outOfSpec && totalLength > parentWidth) {
+                var maxWidth = ellipsizeView!!.measuredWidth - (totalLength - parentWidth)
+                // TODO: Respect android:minWidth (easy with @TargetApi(16))
+                val minWidth = 0
+                if (maxWidth < minWidth) {
+                    maxWidth = minWidth
+                }
+                ellipsizeView.maxWidth = maxWidth
+            }
+        }
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/EmptyViewController.java b/src/com/android/deskclock/widget/EmptyViewController.java
deleted file mode 100644
index 4a21999..0000000
--- a/src/com/android/deskclock/widget/EmptyViewController.java
+++ /dev/null
@@ -1,82 +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.transition.Fade;
-import android.transition.Transition;
-import android.transition.TransitionManager;
-import android.transition.TransitionSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.Utils;
-
-/**
- * Controller that displays empty view and handles animation appropriately.
- */
-public final class EmptyViewController {
-
-    private static final int ANIMATION_DURATION = 300;
-    private static final boolean USE_TRANSITION_FRAMEWORK = Utils.isLOrLater();
-
-    private final Transition mEmptyViewTransition;
-    private final ViewGroup mMainLayout;
-    private final View mContentView;
-    private final View mEmptyView;
-    private boolean mIsEmpty;
-
-    /**
-     * Constructor of the controller.
-     *
-     * @param contentView  The view that should be displayed when empty view is hidden.
-     * @param emptyView The view that should be displayed when main view is empty.
-     */
-    public EmptyViewController(ViewGroup mainLayout, View contentView, View emptyView) {
-        mMainLayout = mainLayout;
-        mContentView = contentView;
-        mEmptyView = emptyView;
-        if (USE_TRANSITION_FRAMEWORK) {
-            mEmptyViewTransition = new TransitionSet()
-                    .setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
-                    .addTarget(contentView)
-                    .addTarget(emptyView)
-                    .addTransition(new Fade(Fade.OUT))
-                    .addTransition(new Fade(Fade.IN))
-                    .setDuration(ANIMATION_DURATION);
-        } else {
-            mEmptyViewTransition = null;
-        }
-    }
-
-    /**
-     * Sets the state for the controller. If it's empty, it will display the empty view.
-     *
-     * @param isEmpty Whether or not the controller should transition into empty state.
-     */
-    public void setEmpty(boolean isEmpty) {
-        if (mIsEmpty == isEmpty) {
-            return;
-        }
-        mIsEmpty = isEmpty;
-        // State changed, perform transition.
-        if (USE_TRANSITION_FRAMEWORK) {
-            TransitionManager.beginDelayedTransition(mMainLayout, mEmptyViewTransition);
-        }
-        mEmptyView.setVisibility(mIsEmpty ? View.VISIBLE : View.GONE);
-        mContentView.setVisibility(mIsEmpty ? View.GONE : View.VISIBLE);
-    }
-}
diff --git a/src/com/android/deskclock/widget/EmptyViewController.kt b/src/com/android/deskclock/widget/EmptyViewController.kt
new file mode 100644
index 0000000..288cc8a
--- /dev/null
+++ b/src/com/android/deskclock/widget/EmptyViewController.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.transition.Fade
+import android.transition.Transition
+import android.transition.TransitionManager
+import android.transition.TransitionSet
+import android.view.View
+import android.view.ViewGroup
+
+import com.android.deskclock.Utils
+
+/**
+ * Controller that displays empty view and handles animation appropriately.
+ *
+ * @param contentView The view that should be displayed when empty view is hidden.
+ * @param emptyView The view that should be displayed when main view is empty.
+ */
+class EmptyViewController(
+    private val mMainLayout: ViewGroup,
+    private val mContentView: View,
+    private val mEmptyView: View
+) {
+    private var mEmptyViewTransition: Transition? = null
+    private var mIsEmpty = false
+
+    init {
+        mEmptyViewTransition = if (USE_TRANSITION_FRAMEWORK) {
+            TransitionSet()
+                    .setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
+                    .addTarget(mContentView)
+                    .addTarget(mEmptyView)
+                    .addTransition(Fade(Fade.OUT))
+                    .addTransition(Fade(Fade.IN))
+                    .setDuration(ANIMATION_DURATION.toLong())
+        } else {
+            null
+        }
+    }
+
+    /**
+     * Sets the state for the controller. If it's empty, it will display the empty view.
+     *
+     * @param isEmpty Whether or not the controller should transition into empty state.
+     */
+    fun setEmpty(isEmpty: Boolean) {
+        if (mIsEmpty == isEmpty) {
+            return
+        }
+        mIsEmpty = isEmpty
+        // State changed, perform transition.
+        if (USE_TRANSITION_FRAMEWORK) {
+            TransitionManager.beginDelayedTransition(mMainLayout, mEmptyViewTransition)
+        }
+        mEmptyView.visibility = if (mIsEmpty) View.VISIBLE else View.GONE
+        mContentView.visibility = if (mIsEmpty) View.GONE else View.VISIBLE
+    }
+
+    companion object {
+        private const val ANIMATION_DURATION = 300
+        private val USE_TRANSITION_FRAMEWORK = Utils.isLOrLater
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextSizeHelper.java b/src/com/android/deskclock/widget/TextSizeHelper.java
deleted file mode 100644
index bf37f76..0000000
--- a/src/com/android/deskclock/widget/TextSizeHelper.java
+++ /dev/null
@@ -1,119 +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.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/TextSizeHelper.kt b/src/com/android/deskclock/widget/TextSizeHelper.kt
new file mode 100644
index 0000000..6ca966f
--- /dev/null
+++ b/src/com/android/deskclock/widget/TextSizeHelper.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.MeasureSpec
+import android.widget.TextView
+
+/**
+ * A TextView which automatically re-sizes its text to fit within its boundaries.
+ */
+class TextSizeHelper(private val mTextView: TextView) {
+
+    // Text paint used for measuring.
+    private val mMeasurePaint = TextPaint()
+
+    // The maximum size the text is allowed to be (in pixels).
+    private val mMaxTextSize: Float = mTextView.textSize
+
+    // The maximum width the text is allowed to be (in pixels).
+    private var mWidthConstraint = Int.MAX_VALUE
+
+    // The maximum height the text is allowed to be (in pixels).
+    private var mHeightConstraint = Int.MAX_VALUE
+
+    // When {@code true} calls to {@link #requestLayout()} should be ignored.
+    private var mIgnoreRequestLayout = false
+
+    fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        var widthConstraint = Int.MAX_VALUE
+        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+            widthConstraint = (MeasureSpec.getSize(widthMeasureSpec) -
+                    mTextView.compoundPaddingLeft - mTextView.compoundPaddingRight)
+        }
+
+        var heightConstraint = Int.MAX_VALUE
+        if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+            heightConstraint = (MeasureSpec.getSize(heightMeasureSpec) -
+                    mTextView.compoundPaddingTop - mTextView.compoundPaddingBottom)
+        }
+
+        if (mTextView.isLayoutRequested ||
+                mWidthConstraint != widthConstraint ||
+                mHeightConstraint != heightConstraint) {
+            mWidthConstraint = widthConstraint
+            mHeightConstraint = heightConstraint
+            adjustTextSize()
+        }
+    }
+
+    fun onTextChanged(lengthBefore: Int, lengthAfter: Int) {
+        // 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()
+        }
+    }
+
+    fun shouldIgnoreRequestLayout(): Boolean {
+        return mIgnoreRequestLayout
+    }
+
+    private fun adjustTextSize() {
+        val text = mTextView.text
+        var textSize = mMaxTextSize
+        if (text.isNotEmpty() &&
+                (mWidthConstraint < Int.MAX_VALUE || mHeightConstraint < Int.MAX_VALUE)) {
+            mMeasurePaint.set(mTextView.paint)
+
+            var minTextSize = 1f
+            var maxTextSize = mMaxTextSize
+            while (maxTextSize >= minTextSize) {
+                val midTextSize = Math.round((maxTextSize + minTextSize) / 2f).toFloat()
+                mMeasurePaint.textSize = midTextSize
+
+                val width = Layout.getDesiredWidth(text, mMeasurePaint)
+                val height = mMeasurePaint.getFontMetricsInt(null).toFloat()
+                if (width > mWidthConstraint || height > mHeightConstraint) {
+                    maxTextSize = midTextSize - 1f
+                } else {
+                    textSize = midTextSize
+                    minTextSize = midTextSize + 1f
+                }
+            }
+        }
+
+        if (mTextView.textSize != 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
deleted file mode 100644
index 3e93124..0000000
--- a/src/com/android/deskclock/widget/TextTime.java
+++ /dev/null
@@ -1,176 +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.widget;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-import androidx.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
- * format specifiers. {@link android.widget.TextClock} doesn't support a non-ticking clock.
- */
-public class TextTime extends TextView {
-
-    /** 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;
-    private CharSequence mFormat;
-
-    private boolean mAttached;
-
-    private int mHour;
-    private int mMinute;
-
-    private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
-        @Override
-        public void onChange(boolean selfChange) {
-            chooseFormat();
-            updateTime();
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            chooseFormat();
-            updateTime();
-        }
-    };
-
-    @SuppressWarnings("UnusedDeclaration")
-    public TextTime(Context context) {
-        this(context, null);
-    }
-
-    @SuppressWarnings("UnusedDeclaration")
-    public TextTime(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public TextTime(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */, false));
-        setFormat24Hour(Utils.get24ModeFormat(false));
-
-        chooseFormat();
-    }
-
-    @SuppressWarnings("UnusedDeclaration")
-    public CharSequence getFormat12Hour() {
-        return mFormat12;
-    }
-
-    @SuppressWarnings("UnusedDeclaration")
-    public void setFormat12Hour(CharSequence format) {
-        mFormat12 = format;
-
-        chooseFormat();
-        updateTime();
-    }
-
-    @SuppressWarnings("UnusedDeclaration")
-    public CharSequence getFormat24Hour() {
-        return mFormat24;
-    }
-
-    @SuppressWarnings("UnusedDeclaration")
-    public void setFormat24Hour(CharSequence format) {
-        mFormat24 = format;
-
-        chooseFormat();
-        updateTime();
-    }
-
-    private void chooseFormat() {
-        final boolean format24Requested = DataModel.getDataModel().is24HourFormat();
-        if (format24Requested) {
-            mFormat = mFormat24 == null ? DEFAULT_FORMAT_24_HOUR : mFormat24;
-        } else {
-            mFormat = mFormat12 == null ? DEFAULT_FORMAT_12_HOUR : mFormat12;
-        }
-    }
-
-    @Override
-    protected void onAttachedToWindow() {
-        super.onAttachedToWindow();
-        if (!mAttached) {
-            mAttached = true;
-            registerObserver();
-            updateTime();
-        }
-    }
-
-    @Override
-    protected void onDetachedFromWindow() {
-        super.onDetachedFromWindow();
-        if (mAttached) {
-            unregisterObserver();
-            mAttached = false;
-        }
-    }
-
-    private void registerObserver() {
-        final ContentResolver resolver = getContext().getContentResolver();
-        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
-    }
-
-    private void unregisterObserver() {
-        final ContentResolver resolver = getContext().getContentResolver();
-        resolver.unregisterContentObserver(mFormatChangeObserver);
-    }
-
-    public void setTime(int hour, int minute) {
-        mHour = hour;
-        mMinute = minute;
-        updateTime();
-    }
-
-    private void updateTime() {
-        // 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
-        setContentDescription(text.toString());
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextTime.kt b/src/com/android/deskclock/widget/TextTime.kt
new file mode 100644
index 0000000..8f2be6c
--- /dev/null
+++ b/src/com/android/deskclock/widget/TextTime.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.text.format.DateFormat
+import android.util.AttributeSet
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+import java.util.Calendar
+import java.util.TimeZone
+
+/**
+ * Based on [android.widget.TextClock], This widget displays a constant time of day using
+ * format specifiers. [android.widget.TextClock] doesn't support a non-ticking clock.
+ */
+class TextTime @JvmOverloads constructor(
+    context: Context?,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : TextView(context, attrs, defStyle) {
+    private var mFormat12: CharSequence? = Utils.get12ModeFormat(0.3f, false)
+    private var mFormat24: CharSequence? = Utils.get24ModeFormat(false)
+    private var mFormat: CharSequence? = null
+
+    private var mAttached = false
+
+    private var mHour = 0
+    private var mMinute = 0
+
+    private val mFormatChangeObserver: ContentObserver =
+            object : ContentObserver(Handler(Looper.myLooper()!!)) {
+        override fun onChange(selfChange: Boolean) {
+            chooseFormat()
+            updateTime()
+        }
+
+        override fun onChange(selfChange: Boolean, uri: Uri?) {
+            chooseFormat()
+            updateTime()
+        }
+    }
+
+    var format12Hour: CharSequence?
+        get() = mFormat12
+        set(format) {
+            mFormat12 = format
+            chooseFormat()
+            updateTime()
+        }
+
+    var format24Hour: CharSequence?
+        get() = mFormat24
+        set(format) {
+            mFormat24 = format
+            chooseFormat()
+            updateTime()
+        }
+
+    init {
+        chooseFormat()
+    }
+
+    private fun chooseFormat() {
+        val format24Requested: Boolean = DataModel.dataModel.is24HourFormat()
+        mFormat = if (format24Requested) {
+            mFormat24 ?: DEFAULT_FORMAT_24_HOUR
+        } else {
+            mFormat12 ?: DEFAULT_FORMAT_12_HOUR
+        }
+    }
+
+    override fun onAttachedToWindow() {
+        super.onAttachedToWindow()
+        if (!mAttached) {
+            mAttached = true
+            registerObserver()
+            updateTime()
+        }
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        if (mAttached) {
+            unregisterObserver()
+            mAttached = false
+        }
+    }
+
+    private fun registerObserver() {
+        val resolver = context.contentResolver
+        resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver)
+    }
+
+    private fun unregisterObserver() {
+        val resolver = context.contentResolver
+        resolver.unregisterContentObserver(mFormatChangeObserver)
+    }
+
+    fun setTime(hour: Int, minute: Int) {
+        mHour = hour
+        mMinute = minute
+        updateTime()
+    }
+
+    private fun updateTime() {
+        // Format the time relative to UTC to ensure hour and minute are not adjusted for DST.
+        val calendar: Calendar = DataModel.dataModel.calendar
+        calendar.timeZone = UTC
+        calendar[Calendar.HOUR_OF_DAY] = mHour
+        calendar[Calendar.MINUTE] = mMinute
+        val text = DateFormat.format(mFormat, calendar)
+        setText(text)
+        // Strip away the spans from text so talkback is not confused
+        contentDescription = text.toString()
+    }
+
+    companion object {
+        /** UTC does not have DST rules and will not alter the [.mHour] and [.mMinute].  */
+        private val UTC = TimeZone.getTimeZone("UTC")
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        val DEFAULT_FORMAT_12_HOUR: CharSequence = "h:mm a"
+
+        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+        val DEFAULT_FORMAT_24_HOUR: CharSequence = "H:mm"
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/selector/AlarmSelection.java b/src/com/android/deskclock/widget/selector/AlarmSelection.java
deleted file mode 100644
index 0cddc1d..0000000
--- a/src/com/android/deskclock/widget/selector/AlarmSelection.java
+++ /dev/null
@@ -1,40 +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.selector;
-
-import com.android.deskclock.provider.Alarm;
-
-public class AlarmSelection {
-    private final String mLabel;
-    private final Alarm mAlarm;
-
-    /**
-     * Created a new selectable item with a visual label and an id.
-     * id corresponds to the Alarm id
-     */
-    public AlarmSelection(String label, Alarm alarm) {
-        mLabel = label;
-        mAlarm = alarm;
-    }
-
-    public String getLabel() {
-        return mLabel;
-    }
-
-    public Alarm getAlarm() {
-        return mAlarm;
-    }
-}
diff --git a/src/com/android/deskclock/data/CityListener.java b/src/com/android/deskclock/widget/selector/AlarmSelection.kt
similarity index 64%
copy from src/com/android/deskclock/data/CityListener.java
copy to src/com/android/deskclock/widget/selector/AlarmSelection.kt
index 91f66b3..bee3285 100644
--- a/src/com/android/deskclock/data/CityListener.java
+++ b/src/com/android/deskclock/widget/selector/AlarmSelection.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.data;
+package com.android.deskclock.widget.selector
 
-import java.util.List;
+import com.android.deskclock.provider.Alarm
 
 /**
- * The interface through which interested parties are notified of changes to the world cities list.
+ * Created a new selectable item with a visual label and an id.
+ * id corresponds to the Alarm id
  */
-public interface CityListener {
-    void citiesChanged(List<City> oldCities, List<City> newCities);
-}
\ No newline at end of file
+class AlarmSelection(val label: String, val alarm: Alarm)
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java
deleted file mode 100644
index 1361e72..0000000
--- a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java
+++ /dev/null
@@ -1,77 +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.selector;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-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;
-
-import java.util.Calendar;
-import java.util.List;
-
-public class AlarmSelectionAdapter extends ArrayAdapter<AlarmSelection> {
-
-    public AlarmSelectionAdapter(Context context, int id, List<AlarmSelection> alarms) {
-        super(context, id, alarms);
-    }
-
-    @Override
-    public @NonNull View getView(int position, @Nullable View convertView,
-            @NonNull ViewGroup parent) {
-        final Context context = getContext();
-        View row = convertView;
-        if (row == null) {
-            final LayoutInflater inflater = LayoutInflater.from(context);
-            row = inflater.inflate(R.layout.alarm_row, parent, false);
-        }
-
-        final AlarmSelection selection = getItem(position);
-        final Alarm alarm = selection.getAlarm();
-
-        final TextTime alarmTime = (TextTime) row.findViewById(R.id.digital_clock);
-        alarmTime.setTime(alarm.hour, alarm.minutes);
-
-        final TextView alarmLabel = (TextView) row.findViewById(R.id.label);
-        alarmLabel.setText(alarm.label);
-
-        // find days when alarm is firing
-        final String daysOfWeek;
-        if (!alarm.daysOfWeek.isRepeating()) {
-            daysOfWeek = Alarm.isTomorrow(alarm, Calendar.getInstance()) ?
-                    context.getResources().getString(R.string.alarm_tomorrow) :
-                    context.getResources().getString(R.string.alarm_today);
-        } else {
-            final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
-            daysOfWeek = alarm.daysOfWeek.toString(context, weekdayOrder);
-        }
-
-        final TextView daysOfWeekView = (TextView) row.findViewById(R.id.daysOfWeek);
-        daysOfWeekView.setText(daysOfWeek);
-
-        return row;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.kt b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.kt
new file mode 100644
index 0000000..8858d64
--- /dev/null
+++ b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.selector
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+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
+
+import java.util.Calendar
+
+class AlarmSelectionAdapter(
+    context: Context,
+    id: Int,
+    alarms: List<AlarmSelection>
+) : ArrayAdapter<AlarmSelection?>(context, id, alarms) {
+    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+        val context = context
+        var row = convertView
+        if (row == null) {
+            val inflater = LayoutInflater.from(context)
+            row = inflater.inflate(R.layout.alarm_row, parent, false)
+        }
+
+        val selection = getItem(position)
+        val alarm = selection?.alarm
+
+        val alarmTime = row!!.findViewById<View>(R.id.digital_clock) as TextTime
+        alarmTime.setTime(alarm!!.hour, alarm.minutes)
+
+        val alarmLabel = row.findViewById<View>(R.id.label) as TextView
+        alarmLabel.text = alarm.label
+
+        // find days when alarm is firing
+        val daysOfWeek: String
+        daysOfWeek = if (!alarm.daysOfWeek.isRepeating) {
+            if (Alarm.isTomorrow(alarm, Calendar.getInstance())) {
+                context.resources.getString(R.string.alarm_tomorrow)
+            } else {
+                context.resources.getString(R.string.alarm_today)
+            }
+        } else {
+            val weekdayOrder: Weekdays.Order = DataModel.dataModel.weekdayOrder
+            alarm.daysOfWeek.toString(context, weekdayOrder)
+        }
+
+        val daysOfWeekView = row.findViewById<View>(R.id.daysOfWeek) as TextView
+        daysOfWeekView.text = daysOfWeek
+
+        return row
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/SnackbarManager.java b/src/com/android/deskclock/widget/toast/SnackbarManager.java
deleted file mode 100644
index 95d91b2..0000000
--- a/src/com/android/deskclock/widget/toast/SnackbarManager.java
+++ /dev/null
@@ -1,44 +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.toast;
-
-import com.google.android.material.snackbar.Snackbar;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Manages visibility of Snackbar and allow preemptive dismiss of current displayed Snackbar.
- */
-public final class SnackbarManager {
-
-    private static WeakReference<Snackbar> sSnackbar = null;
-
-    private SnackbarManager() {}
-
-    public static void show(Snackbar snackbar) {
-        sSnackbar = new WeakReference<>(snackbar);
-        snackbar.show();
-    }
-
-    public static void dismiss() {
-        final Snackbar snackbar = sSnackbar == null ? null : sSnackbar.get();
-        if (snackbar != null) {
-            snackbar.dismiss();
-            sSnackbar = null;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/SnackbarManager.kt b/src/com/android/deskclock/widget/toast/SnackbarManager.kt
new file mode 100644
index 0000000..0d242f2
--- /dev/null
+++ b/src/com/android/deskclock/widget/toast/SnackbarManager.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.toast
+
+import com.google.android.material.snackbar.Snackbar
+
+import java.lang.ref.WeakReference
+
+/**
+ * Manages visibility of Snackbar and allow preemptive dismiss of current displayed Snackbar.
+ */
+object SnackbarManager {
+    private var sSnackbar: WeakReference<Snackbar>? = null
+
+    @JvmStatic
+    fun show(snackbar: Snackbar) {
+        sSnackbar = WeakReference<Snackbar>(snackbar)
+        snackbar.show()
+    }
+
+    @JvmStatic
+    fun dismiss() {
+        val snackbar: Snackbar? = sSnackbar?.get()
+        if (snackbar != null) {
+            snackbar.dismiss()
+            sSnackbar = null
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.java b/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.java
deleted file mode 100644
index 84cbcff..0000000
--- a/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.java
+++ /dev/null
@@ -1,64 +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.widget.toast;
-
-import android.content.Context;
-import androidx.annotation.Keep;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import com.google.android.material.snackbar.Snackbar;
-import android.util.AttributeSet;
-import android.view.View;
-
-/**
- * Custom {@link CoordinatorLayout.Behavior} that slides with the {@link Snackbar}.
- */
-@Keep
-public final class SnackbarSlidingBehavior extends CoordinatorLayout.Behavior<View> {
-
-    public SnackbarSlidingBehavior(Context context, AttributeSet attrs) {
-    }
-
-    @Override
-    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
-        return dependency instanceof Snackbar.SnackbarLayout;
-    }
-
-    @Override
-    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
-        updateTranslationY(parent, child);
-        return false;
-    }
-
-    @Override
-    public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
-        updateTranslationY(parent, child);
-    }
-
-    @Override
-    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
-        updateTranslationY(parent, child);
-        return false;
-    }
-
-    private void updateTranslationY(CoordinatorLayout parent, View child) {
-        float translationY = 0f;
-        for (View dependency : parent.getDependencies(child)) {
-            translationY = Math.min(translationY, dependency.getY() - child.getBottom());
-        }
-        child.setTranslationY(translationY);
-    }
-}
diff --git a/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.kt b/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.kt
new file mode 100644
index 0000000..4ebfc7c
--- /dev/null
+++ b/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.toast
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.annotation.Keep
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+
+import com.google.android.material.snackbar.Snackbar
+
+import kotlin.math.min
+
+/**
+ * Custom [CoordinatorLayout.Behavior] that slides with the [Snackbar].
+ */
+@Keep
+class SnackbarSlidingBehavior(
+    context: Context?,
+    attrs: AttributeSet?
+) : CoordinatorLayout.Behavior<View?>() {
+    override fun layoutDependsOn(
+        parent: CoordinatorLayout,
+        child: View,
+        dependency: View
+    ): Boolean {
+        return dependency is Snackbar.SnackbarLayout
+    }
+
+    override fun onDependentViewChanged(
+        parent: CoordinatorLayout,
+        child: View,
+        dependency: View
+    ): Boolean {
+        updateTranslationY(parent, child)
+        return false
+    }
+
+    override fun onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) {
+        updateTranslationY(parent, child)
+    }
+
+    override fun onLayoutChild(
+        parent: CoordinatorLayout,
+        child: View,
+        layoutDirection: Int
+    ): Boolean {
+        updateTranslationY(parent, child)
+        return false
+    }
+
+    private fun updateTranslationY(parent: CoordinatorLayout, child: View) {
+        var translationY = 0f
+        for (dependency in parent.getDependencies(child)) {
+            translationY = min(translationY, dependency.y - child.bottom)
+        }
+        child.translationY = translationY
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/ToastManager.java b/src/com/android/deskclock/widget/toast/ToastManager.java
deleted file mode 100644
index 09be715..0000000
--- a/src/com/android/deskclock/widget/toast/ToastManager.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.toast;
-
-import android.widget.Toast;
-
-public final class ToastManager {
-
-    private static Toast sToast = null;
-
-    private ToastManager() {
-
-    }
-
-    public static void setToast(Toast toast) {
-        if (sToast != null)
-            sToast.cancel();
-        sToast = toast;
-    }
-
-    public static void cancelToast() {
-        if (sToast != null)
-            sToast.cancel();
-        sToast = null;
-    }
-
-}
diff --git a/src/com/android/deskclock/ringtone/SystemRingtoneHolder.java b/src/com/android/deskclock/widget/toast/ToastManager.kt
similarity index 60%
rename from src/com/android/deskclock/ringtone/SystemRingtoneHolder.java
rename to src/com/android/deskclock/widget/toast/ToastManager.kt
index ff531ff..49ef9b7 100644
--- a/src/com/android/deskclock/ringtone/SystemRingtoneHolder.java
+++ b/src/com/android/deskclock/widget/toast/ToastManager.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,18 +14,22 @@
  * limitations under the License.
  */
 
-package com.android.deskclock.ringtone;
+package com.android.deskclock.widget.toast
 
-import android.net.Uri;
+import android.widget.Toast
 
-final class SystemRingtoneHolder extends RingtoneHolder {
+object ToastManager {
+    private var sToast: Toast? = null
 
-    SystemRingtoneHolder(Uri uri, String name) {
-        super(uri, name);
+    @JvmStatic
+    fun setToast(toast: Toast) {
+        sToast?.cancel()
+        sToast = toast
     }
 
-    @Override
-    public int getItemViewType() {
-        return RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND;
+    @JvmStatic
+    fun cancelToast() {
+        sToast?.cancel()
+        sToast = null
     }
 }
\ No newline at end of file
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.java b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
deleted file mode 100644
index d8954d0..0000000
--- a/src/com/android/deskclock/worldclock/CitySelectionActivity.java
+++ /dev/null
@@ -1,650 +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.worldclock;
-
-import android.content.Context;
-import android.os.Bundle;
-import androidx.appcompat.widget.SearchView;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.util.ArraySet;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.ListView;
-import android.widget.SectionIndexer;
-import android.widget.TextView;
-
-import com.android.deskclock.BaseActivity;
-import com.android.deskclock.DropShadowController;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-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;
-import com.android.deskclock.data.DataModel;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.TimeZone;
-
-import static android.view.Menu.NONE;
-
-/**
- * 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>
- * </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.
-     */
-    private ListView mCitiesList;
-
-    /**
-     * The adapter that presents all of the selected and unselected cities.
-     */
-    private CityAdapter mCitiesAdapter;
-
-    /**
-     * Manages all action bar menu display and click handling.
-     */
-    private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
-
-    /**
-     * Menu item controller for search view.
-     */
-    private SearchMenuItemController mSearchMenuItemController;
-
-    /**
-     * The controller that shows the drop shadow when content is not scrolled to the top.
-     */
-    private DropShadowController mDropShadowController;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.cities_activity);
-        mSearchMenuItemController =
-                new SearchMenuItemController(getSupportActionBar().getThemedContext(),
-                        new SearchView.OnQueryTextListener() {
-                            @Override
-                            public boolean onQueryTextSubmit(String query) {
-                                return false;
-                            }
-
-                            @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)
-                .addMenuItemController(new SortOrderMenuItemController())
-                .addMenuItemController(new SettingsMenuItemController(this))
-                .addMenuItemController(MenuItemControllerFactory.getInstance()
-                        .buildMenuItemControllers(this));
-        mCitiesList = (ListView) findViewById(R.id.cities_list);
-        mCitiesList.setAdapter(mCitiesAdapter);
-
-        updateFastScrolling();
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle bundle) {
-        super.onSaveInstanceState(bundle);
-        mSearchMenuItemController.saveInstance(bundle);
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-
-        // Recompute the contents of the adapter before displaying on screen.
-        mCitiesAdapter.refresh();
-
-        final View dropShadow = findViewById(R.id.drop_shadow);
-        mDropShadowController = new DropShadowController(dropShadow, mCitiesList);
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-
-        mDropShadowController.stop();
-
-        // Save the selected cities.
-        DataModel.getDataModel().setSelectedCities(mCitiesAdapter.getSelectedCities());
-    }
-
-    @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);
-    }
-
-    /**
-     * Fast scrolling is only enabled while no filtering is happening.
-     */
-    private void updateFastScrolling() {
-        final boolean enabled = !mCitiesAdapter.isFiltering();
-        mCitiesList.setFastScrollAlwaysVisible(enabled);
-        mCitiesList.setFastScrollEnabled(enabled);
-    }
-
-    /**
-     * This adapter presents data in 2 possible modes. If selected cities exist the format is:
-     * <p/>
-     * <pre>
-     * Selected Cities
-     *   City 1 (alphabetically first)
-     *   City 2 (alphabetically second)
-     *   ...
-     * A City A1 (alphabetically first starting with A)
-     *   City A2 (alphabetically second starting with A)
-     *   ...
-     * B City B1 (alphabetically first starting with B)
-     *   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)
-     *   ...
-     * B City B1 (alphabetically first starting with B)
-     *   City B2 (alphabetically second starting with B)
-     *   ...
-     * </pre>
-     */
-    private static final class CityAdapter extends BaseAdapter implements View.OnClickListener,
-            CompoundButton.OnCheckedChangeListener, SectionIndexer {
-
-        /**
-         * 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.
-         */
-        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.
-         */
-        private final String mPattern12;
-
-        /**
-         * The 24-hour time pattern for the current locale.
-         */
-        private final String mPattern24;
-
-        /**
-         * {@code true} time should honor {@link #mPattern24}; {@link #mPattern12} otherwise.
-         */
-        private boolean mIs24HoursMode;
-
-        /**
-         * 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.
-         */
-        private List<City> mFilteredCities = Collections.emptyList();
-
-        /**
-         * 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.
-         */
-        private int mOriginalUserSelectionCount;
-
-        /**
-         * The precomputed section headers.
-         */
-        private String[] mSectionHeaders;
-
-        /**
-         * The corresponding location of each precomputed section header.
-         */
-        private Integer[] mSectionHeaderPositions;
-
-        /**
-         * Menu item controller for search. Search query is maintained here.
-         */
-        private final SearchMenuItemController mSearchMenuItemController;
-
-        public CityAdapter(Context context, SearchMenuItemController searchMenuItemController) {
-            mContext = context;
-            mSearchMenuItemController = searchMenuItemController;
-            mInflater = LayoutInflater.from(context);
-
-            mCalendar = Calendar.getInstance();
-            mCalendar.setTimeInMillis(System.currentTimeMillis());
-
-            final Locale locale = Locale.getDefault();
-            mPattern24 = DateFormat.getBestDateTimePattern(locale, "Hm");
-
-            String pattern12 = DateFormat.getBestDateTimePattern(locale, "hma");
-            if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
-                // There's an RTL layout bug that causes jank when fast-scrolling through
-                // the list in 12-hour mode in an RTL locale. We can work around this by
-                // ensuring the strings are the same length by using "hh" instead of "h".
-                pattern12 = pattern12.replaceAll("h", "hh");
-            }
-            mPattern12 = pattern12;
-        }
-
-        @Override
-        public int getCount() {
-            final int headerCount = hasHeader() ? 1 : 0;
-            return headerCount + mFilteredCities.size();
-        }
-
-        @Override
-        public City getItem(int position) {
-            if (hasHeader()) {
-                final int itemViewType = getItemViewType(position);
-                switch (itemViewType) {
-                    case VIEW_TYPE_SELECTED_CITIES_HEADER:
-                        return null;
-                    case VIEW_TYPE_CITY:
-                        return mFilteredCities.get(position - 1);
-                }
-                throw new IllegalStateException("unexpected item view type: " + itemViewType);
-            }
-
-            return mFilteredCities.get(position);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
-        }
-
-        @Override
-        public View getView(int position, View view, ViewGroup parent) {
-            final int itemViewType = getItemViewType(position);
-            switch (itemViewType) {
-                case VIEW_TYPE_SELECTED_CITIES_HEADER:
-                    if (view == null) {
-                        view = mInflater.inflate(R.layout.city_list_header, parent, false);
-                    }
-                    return view;
-
-                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.
-                    if (view == null) {
-                        view = mInflater.inflate(R.layout.city_list_item, parent, false);
-                        final TextView index = (TextView) view.findViewById(R.id.index);
-                        final TextView name = (TextView) view.findViewById(R.id.city_name);
-                        final TextView time = (TextView) view.findViewById(R.id.city_time);
-                        final CheckBox selected = (CheckBox) view.findViewById(R.id.city_onoff);
-                        view.setTag(new CityItemHolder(index, name, time, selected));
-                    }
-
-                    // Bind data into the child views.
-                    final CityItemHolder holder = (CityItemHolder) view.getTag();
-                    holder.selected.setTag(city);
-                    holder.selected.setChecked(mUserSelectedCities.contains(city));
-                    holder.selected.setContentDescription(city.getName());
-                    holder.selected.setOnCheckedChangeListener(this);
-                    holder.name.setText(city.getName(), TextView.BufferType.SPANNABLE);
-                    holder.time.setText(getTimeCharSequence(timeZone));
-
-                    final boolean showIndex = getShowIndex(position);
-                    holder.index.setVisibility(showIndex ? View.VISIBLE : View.INVISIBLE);
-                    if (showIndex) {
-                        switch (getCitySort()) {
-                            case NAME:
-                                holder.index.setText(city.getIndexString());
-                                holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
-                                break;
-
-                            case UTC_OFFSET:
-                                holder.index.setText(Utils.getGMTHourOffset(timeZone, false));
-                                holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
-                                break;
-                        }
-                    }
-
-                    // skip checkbox and other animations
-                    view.jumpDrawablesToCurrentState();
-                    view.setOnClickListener(this);
-                    return view;
-            }
-
-            throw new IllegalStateException("unexpected item view type: " + itemViewType);
-        }
-
-        @Override
-        public int getViewTypeCount() {
-            return 2;
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            return hasHeader() && position == 0 ? VIEW_TYPE_SELECTED_CITIES_HEADER : VIEW_TYPE_CITY;
-        }
-
-        @Override
-        public void onCheckedChanged(CompoundButton b, boolean checked) {
-            final City city = (City) b.getTag();
-            if (checked) {
-                mUserSelectedCities.add(city);
-                b.announceForAccessibility(mContext.getString(R.string.city_checked,
-                        city.getName()));
-            } else {
-                mUserSelectedCities.remove(city);
-                b.announceForAccessibility(mContext.getString(R.string.city_unchecked,
-                        city.getName()));
-            }
-        }
-
-        @Override
-        public void onClick(View v) {
-            final CheckBox b = (CheckBox) v.findViewById(R.id.city_onoff);
-            b.setChecked(!b.isChecked());
-        }
-
-        @Override
-        public Object[] getSections() {
-            if (mSectionHeaders == null) {
-                // Make an educated guess at the expected number of sections.
-                final int approximateSectionCount = getCount() / 5;
-                final List<String> sections = new ArrayList<>(approximateSectionCount);
-                final List<Integer> positions = new ArrayList<>(approximateSectionCount);
-
-                // Add a section for the "Selected Cities" header if it exists.
-                if (hasHeader()) {
-                    sections.add("+");
-                    positions.add(0);
-                }
-
-                for (int position = 0; position < getCount(); position++) {
-                    // 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());
-                                break;
-                            case UTC_OFFSET:
-                                final TimeZone timezone = city.getTimeZone();
-                                sections.add(Utils.getGMTHourOffset(timezone, Utils.isPreL()));
-                                break;
-                        }
-                        positions.add(position);
-                    }
-                }
-
-                mSectionHeaders = sections.toArray(new String[sections.size()]);
-                mSectionHeaderPositions = positions.toArray(new Integer[positions.size()]);
-            }
-            return mSectionHeaders;
-        }
-
-        @Override
-        public int getPositionForSection(int sectionIndex) {
-            return getSections().length == 0 ? 0 : mSectionHeaderPositions[sectionIndex];
-        }
-
-        @Override
-        public int getSectionForPosition(int position) {
-            if (getSections().length == 0) {
-                return 0;
-            }
-
-            for (int i = 0; i < mSectionHeaderPositions.length - 2; i++) {
-                if (position < mSectionHeaderPositions[i]) continue;
-                if (position >= mSectionHeaderPositions[i + 1]) continue;
-
-                return i;
-            }
-
-            return mSectionHeaderPositions.length - 1;
-        }
-
-        /**
-         * Clear the section headers to force them to be recomputed if they are now stale.
-         */
-        private void clearSectionHeaders() {
-            mSectionHeaders = null;
-            mSectionHeaderPositions = null;
-        }
-
-        /**
-         * Rebuilds all internal data structures from scratch.
-         */
-        private void refresh() {
-            // Update the 12/24 hour mode.
-            mIs24HoursMode = DateFormat.is24HourFormat(mContext);
-
-            // Refresh the user selections.
-            final List<City> selected = DataModel.getDataModel().getSelectedCities();
-            mUserSelectedCities.clear();
-            mUserSelectedCities.addAll(selected);
-            mOriginalUserSelectionCount = selected.size();
-
-            // Recompute section headers.
-            clearSectionHeaders();
-
-            // Recompute filtered cities.
-            filter(mSearchMenuItemController.getQueryText());
-        }
-
-        /**
-         * Filter the cities using the given {@code queryText}.
-         */
-        private void filter(String queryText) {
-            mSearchMenuItemController.setQueryText(queryText);
-            final String query = City.removeSpecialCharacters(queryText.toUpperCase());
-
-            // Compute the filtered list of cities.
-            final List<City> filteredCities;
-            if (TextUtils.isEmpty(query)) {
-                filteredCities = DataModel.getDataModel().getAllCities();
-            } else {
-                final List<City> unselected = DataModel.getDataModel().getUnselectedCities();
-                filteredCities = new ArrayList<>(unselected.size());
-                for (City city : unselected) {
-                    if (city.matches(query)) {
-                        filteredCities.add(city);
-                    }
-                }
-            }
-
-            // Swap in the filtered list of cities and notify of the data change.
-            mFilteredCities = filteredCities;
-            notifyDataSetChanged();
-        }
-
-        private boolean isFiltering() {
-            return !TextUtils.isEmpty(mSearchMenuItemController.getQueryText().trim());
-        }
-
-        private Collection<City> getSelectedCities() {
-            return mUserSelectedCities;
-        }
-
-        private boolean hasHeader() {
-            return !isFiltering() && mOriginalUserSelectionCount > 0;
-        }
-
-        private DataModel.CitySort getCitySort() {
-            return DataModel.getDataModel().getCitySort();
-        }
-
-        private Comparator<City> getCitySortComparator() {
-            return DataModel.getDataModel().getCityIndexComparator();
-        }
-
-        private CharSequence getTimeCharSequence(TimeZone timeZone) {
-            mCalendar.setTimeZone(timeZone);
-            return DateFormat.format(mIs24HoursMode ? mPattern24 : mPattern12, mCalendar);
-        }
-
-        private boolean getShowIndex(int position) {
-            // Indexes are never displayed on filtered cities.
-            if (isFiltering()) {
-                return false;
-            }
-
-            if (hasHeader()) {
-                // None of the original user selections should show their index.
-                if (position <= mOriginalUserSelectionCount) {
-                    return false;
-                }
-
-                // The first item after the original user selections must always show its index.
-                if (position == mOriginalUserSelectionCount + 1) {
-                    return true;
-                }
-            } else {
-                // None of the original user selections should show their index.
-                if (position < mOriginalUserSelectionCount) {
-                    return false;
-                }
-
-                // The first item after the original user selections must always show its index.
-                if (position == mOriginalUserSelectionCount) {
-                    return true;
-                }
-            }
-
-            // Otherwise compare the city with its predecessor to test if it is a header.
-            final City priorCity = getItem(position - 1);
-            final City city = getItem(position);
-            return getCitySortComparator().compare(priorCity, city) != 0;
-        }
-
-        /**
-         * Cache the child views of each city item view.
-         */
-        private static final class CityItemHolder {
-
-            private final TextView index;
-            private final TextView name;
-            private final TextView time;
-            private final CheckBox selected;
-
-            public CityItemHolder(TextView index, TextView name, TextView time, CheckBox selected) {
-                this.index = index;
-                this.name = name;
-                this.time = time;
-                this.selected = selected;
-            }
-        }
-    }
-
-    private final class SortOrderMenuItemController implements MenuItemController {
-
-        private static final int SORT_MENU_RES_ID = R.id.menu_item_sort;
-
-        @Override
-        public int getId() {
-            return SORT_MENU_RES_ID;
-        }
-
-        @Override
-        public void onCreateOptionsItem(Menu menu) {
-            menu.add(NONE, R.id.menu_item_sort, NONE, R.string.menu_item_sort_by_gmt_offset)
-                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-        }
-
-        @Override
-        public void onPrepareOptionsItem(MenuItem item) {
-            item.setTitle(DataModel.getDataModel().getCitySort() == DataModel.CitySort.NAME
-                    ? R.string.menu_item_sort_by_gmt_offset : R.string.menu_item_sort_by_name);
-        }
-
-        @Override
-        public boolean onOptionsItemSelected(MenuItem item) {
-            // Save the new sort order.
-            DataModel.getDataModel().toggleCitySort();
-
-            // Section headers are influenced by sort order and must be cleared.
-            mCitiesAdapter.clearSectionHeaders();
-
-            // Honor the new sort order in the adapter.
-            mCitiesAdapter.filter(mSearchMenuItemController.getQueryText());
-            return true;
-        }
-    }
-}
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.kt b/src/com/android/deskclock/worldclock/CitySelectionActivity.kt
new file mode 100644
index 0000000..998faa5
--- /dev/null
+++ b/src/com/android/deskclock/worldclock/CitySelectionActivity.kt
@@ -0,0 +1,593 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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
+
+import android.content.Context
+import android.os.Bundle
+import androidx.appcompat.widget.SearchView
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.util.ArraySet
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.ListView
+import android.widget.SectionIndexer
+import android.widget.TextView
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.DropShadowController
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+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
+import com.android.deskclock.data.DataModel
+
+import java.util.ArrayList
+import java.util.Calendar
+import java.util.Comparator
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * This activity allows the user to alter the cities selected for display.
+ *
+ * Note, it is possible for two instances of this Activity to exist simultaneously:
+ * <ul>
+ * <li>Clock Tab-> Tap Floating Action Button</li>
+ * <li>Digital Widget -> Tap any city clock</li>
+ * </ul>
+ *
+ * As a result, [.onResume] conservatively refreshes itself from the backing
+ * [DataModel] which may have changed since this activity was last displayed.
+ */
+class CitySelectionActivity : BaseActivity() {
+    /**
+     * The list of all selected and unselected cities, indexed and possibly filtered.
+     */
+    private lateinit var mCitiesList: ListView
+
+    /**
+     * The adapter that presents all of the selected and unselected cities.
+     */
+    private lateinit var mCitiesAdapter: CityAdapter
+
+    /**
+     * Manages all action bar menu display and click handling.
+     */
+    private val mOptionsMenuManager = OptionsMenuManager()
+
+    /**
+     * Menu item controller for search view.
+     */
+    private lateinit var mSearchMenuItemController: SearchMenuItemController
+
+    /**
+     * The controller that shows the drop shadow when content is not scrolled to the top.
+     */
+    private lateinit var mDropShadowController: DropShadowController
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(R.layout.cities_activity)
+        mSearchMenuItemController = SearchMenuItemController(
+                getSupportActionBar()!!.getThemedContext(),
+                object : SearchView.OnQueryTextListener {
+                    override fun onQueryTextSubmit(query: String?): Boolean {
+                        return false
+                    }
+
+                    override fun onQueryTextChange(query: String): Boolean {
+                        mCitiesAdapter.filter(query)
+                        updateFastScrolling()
+                        return true
+                    }
+                }, savedInstanceState)
+        mCitiesAdapter = CityAdapter(this, mSearchMenuItemController)
+        mOptionsMenuManager.addMenuItemController(NavUpMenuItemController(this))
+                .addMenuItemController(mSearchMenuItemController)
+                .addMenuItemController(SortOrderMenuItemController())
+                .addMenuItemController(SettingsMenuItemController(this))
+                .addMenuItemController(*MenuItemControllerFactory.buildMenuItemControllers(this))
+        mCitiesList = findViewById(R.id.cities_list) as ListView
+        mCitiesList.adapter = mCitiesAdapter
+
+        updateFastScrolling()
+    }
+
+    override fun onSaveInstanceState(bundle: Bundle) {
+        super.onSaveInstanceState(bundle)
+        mSearchMenuItemController.saveInstance(bundle)
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        // Recompute the contents of the adapter before displaying on screen.
+        mCitiesAdapter.refresh()
+
+        val dropShadow: View = findViewById(R.id.drop_shadow)
+        mDropShadowController = DropShadowController(dropShadow, mCitiesList)
+    }
+
+    override fun onPause() {
+        super.onPause()
+
+        mDropShadowController.stop()
+
+        // Save the selected cities.
+        DataModel.dataModel.selectedCities = mCitiesAdapter.selectedCities
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        mOptionsMenuManager.onCreateOptionsMenu(menu)
+        return true
+    }
+
+    override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+        mOptionsMenuManager.onPrepareOptionsMenu(menu)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return (mOptionsMenuManager.onOptionsItemSelected(item) ||
+                super.onOptionsItemSelected(item))
+    }
+
+    /**
+     * Fast scrolling is only enabled while no filtering is happening.
+     */
+    private fun updateFastScrolling() {
+        val enabled: Boolean = !mCitiesAdapter.isFiltering
+        mCitiesList.isFastScrollAlwaysVisible = enabled
+        mCitiesList.isFastScrollEnabled = enabled
+    }
+
+    /**
+     * This adapter presents data in 2 possible modes. If selected cities exist the format is:
+     *
+     * <pre>
+     * Selected Cities
+     * City 1 (alphabetically first)
+     * City 2 (alphabetically second)
+     * ...
+     * A City A1 (alphabetically first starting with A)
+     * City A2 (alphabetically second starting with A)
+     * ...
+     * B City B1 (alphabetically first starting with B)
+     * City B2 (alphabetically second starting with B)
+     * ...
+     * </pre>
+     *
+     * If selected cities do not exist, that section is removed and all that remains is:
+     *
+     * <pre>
+     * A City A1 (alphabetically first starting with A)
+     * City A2 (alphabetically second starting with A)
+     * ...
+     * B City B1 (alphabetically first starting with B)
+     * City B2 (alphabetically second starting with B)
+     * ...
+     * </pre>
+     */
+    private class CityAdapter(
+        private val mContext: Context,
+        /** Menu item controller for search. Search query is maintained here. */
+        private val mSearchMenuItemController: SearchMenuItemController
+    ) : BaseAdapter(), View.OnClickListener,
+            CompoundButton.OnCheckedChangeListener, SectionIndexer {
+        private val mInflater: LayoutInflater = LayoutInflater.from(mContext)
+
+        /**
+         * The 12-hour time pattern for the current locale.
+         */
+        private val mPattern12: String
+
+        /**
+         * The 24-hour time pattern for the current locale.
+         */
+        private val mPattern24: String
+
+        /**
+         * `true` time should honor [.mPattern24]; [.mPattern12] otherwise.
+         */
+        private var mIs24HoursMode = false
+
+        /**
+         * A calendar used to format time in a particular timezone.
+         */
+        private val mCalendar: Calendar = Calendar.getInstance()
+
+        /**
+         * The list of cities which may be filtered by a search term.
+         */
+        private var mFilteredCities: List<City> = emptyList()
+
+        /**
+         * A mutable set of cities currently selected by the user.
+         */
+        private val mUserSelectedCities: MutableSet<City> = ArraySet()
+
+        /**
+         * The number of user selections at the top of the adapter to avoid indexing.
+         */
+        private var mOriginalUserSelectionCount = 0
+
+        /**
+         * The precomputed section headers.
+         */
+        private var mSectionHeaders: Array<String>? = null
+
+        /**
+         * The corresponding location of each precomputed section header.
+         */
+        private var mSectionHeaderPositions: Array<Int>? = null
+
+        init {
+            mCalendar.timeInMillis = System.currentTimeMillis()
+
+            val locale = Locale.getDefault()
+            mPattern24 = DateFormat.getBestDateTimePattern(locale, "Hm")
+
+            var pattern12 = DateFormat.getBestDateTimePattern(locale, "hma")
+            if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
+                // There's an RTL layout bug that causes jank when fast-scrolling through
+                // the list in 12-hour mode in an RTL locale. We can work around this by
+                // ensuring the strings are the same length by using "hh" instead of "h".
+                pattern12 = pattern12.replace("h".toRegex(), "hh")
+            }
+            mPattern12 = pattern12
+        }
+
+        override fun getCount(): Int {
+            val headerCount = if (hasHeader()) 1 else 0
+            return headerCount + mFilteredCities.size
+        }
+
+        override fun getItem(position: Int): City? {
+            if (hasHeader()) {
+                val itemViewType = getItemViewType(position)
+                when (itemViewType) {
+                    VIEW_TYPE_SELECTED_CITIES_HEADER -> return null
+                    VIEW_TYPE_CITY -> return mFilteredCities[position - 1]
+                }
+                throw IllegalStateException("unexpected item view type: $itemViewType")
+            }
+
+            return mFilteredCities[position]
+        }
+
+        override fun getItemId(position: Int): Long {
+            return position.toLong()
+        }
+
+        override fun getView(position: Int, view: View?, parent: ViewGroup): View {
+            var variableView = view
+            val itemViewType = getItemViewType(position)
+            when (itemViewType) {
+                VIEW_TYPE_SELECTED_CITIES_HEADER -> {
+                    return variableView
+                            ?: mInflater.inflate(R.layout.city_list_header, parent, false)
+                }
+                VIEW_TYPE_CITY -> {
+                    val city = getItem(position)
+                            ?: throw IllegalStateException("The desired city does not exist")
+                    val timeZone: TimeZone = city.timeZone
+
+                    // Inflate a new view if necessary.
+                    if (variableView == null) {
+                        variableView = mInflater.inflate(R.layout.city_list_item, parent, false)
+                        val index = variableView.findViewById<View>(R.id.index) as TextView
+                        val name = variableView.findViewById<View>(R.id.city_name) as TextView
+                        val time = variableView.findViewById<View>(R.id.city_time) as TextView
+                        val selected = variableView.findViewById<View>(R.id.city_onoff) as CheckBox
+                        variableView.tag = CityItemHolder(index, name, time, selected)
+                    }
+
+                    // Bind data into the child views.
+                    val holder = variableView!!.tag as CityItemHolder
+                    holder.selected.tag = city
+                    holder.selected.isChecked = mUserSelectedCities.contains(city)
+                    holder.selected.contentDescription = city.name
+                    holder.selected.setOnCheckedChangeListener(this)
+                    holder.name.setText(city.name, TextView.BufferType.SPANNABLE)
+                    holder.time.text = getTimeCharSequence(timeZone)
+
+                    val showIndex = getShowIndex(position)
+                    holder.index.visibility = if (showIndex) View.VISIBLE else View.INVISIBLE
+                    if (showIndex) {
+                        when (citySort) {
+                            DataModel.CitySort.NAME -> {
+                                holder.index.setText(city.indexString)
+                                holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f)
+                            }
+                            DataModel.CitySort.UTC_OFFSET -> {
+                                holder.index.text = Utils.getGMTHourOffset(timeZone, false)
+                                holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
+                            }
+                        }
+                    }
+
+                    // skip checkbox and other animations
+                    variableView.jumpDrawablesToCurrentState()
+                    variableView.setOnClickListener(this)
+                    return variableView
+                }
+                else -> throw IllegalStateException("unexpected item view type: $itemViewType")
+            }
+        }
+
+        override fun getViewTypeCount(): Int {
+            return 2
+        }
+
+        override fun getItemViewType(position: Int): Int {
+            return if (hasHeader() && position == 0) {
+                VIEW_TYPE_SELECTED_CITIES_HEADER
+            } else {
+                VIEW_TYPE_CITY
+            }
+        }
+
+        override fun onCheckedChanged(b: CompoundButton, checked: Boolean) {
+            val city = b.tag as City
+            if (checked) {
+                mUserSelectedCities.add(city)
+                b.announceForAccessibility(mContext.getString(R.string.city_checked,
+                        city.name))
+            } else {
+                mUserSelectedCities.remove(city)
+                b.announceForAccessibility(mContext.getString(R.string.city_unchecked,
+                        city.name))
+            }
+        }
+
+        override fun onClick(v: View) {
+            val b = v.findViewById<View>(R.id.city_onoff) as CheckBox
+            b.isChecked = !b.isChecked
+        }
+
+        override fun getSections(): Array<String>? {
+            if (mSectionHeaders == null) {
+                // Make an educated guess at the expected number of sections.
+                val approximateSectionCount = count / 5
+                val sections: MutableList<String> = ArrayList(approximateSectionCount)
+                val positions: MutableList<Int> = ArrayList(approximateSectionCount)
+
+                // Add a section for the "Selected Cities" header if it exists.
+                if (hasHeader()) {
+                    sections.add("+")
+                    positions.add(0)
+                }
+
+                for (position in 0 until count) {
+                    // Add a section if this position should show the section index.
+                    if (getShowIndex(position)) {
+                        val city = getItem(position)
+                                ?: throw IllegalStateException("The desired city does not exist")
+                        when (citySort) {
+                            DataModel.CitySort.NAME -> sections.add(city.indexString.orEmpty())
+                            DataModel.CitySort.UTC_OFFSET -> {
+                                val timezone: TimeZone = city.timeZone
+                                sections.add(Utils.getGMTHourOffset(timezone, Utils.isPreL))
+                            }
+                        }
+                        positions.add(position)
+                    }
+                }
+
+                mSectionHeaders = sections.toTypedArray()
+                mSectionHeaderPositions = positions.toTypedArray()
+            }
+            return mSectionHeaders
+        }
+
+        override fun getPositionForSection(sectionIndex: Int): Int {
+            return if (sections!!.isEmpty()) 0 else mSectionHeaderPositions!![sectionIndex]
+        }
+
+        override fun getSectionForPosition(position: Int): Int {
+            if (sections!!.isEmpty()) {
+                return 0
+            }
+
+            for (i in 0 until mSectionHeaderPositions!!.size - 2) {
+                if (position < mSectionHeaderPositions!![i]) continue
+                if (position >= mSectionHeaderPositions!![i + 1]) continue
+                return i
+            }
+
+            return mSectionHeaderPositions!!.size - 1
+        }
+
+        /**
+         * Clear the section headers to force them to be recomputed if they are now stale.
+         */
+        fun clearSectionHeaders() {
+            mSectionHeaders = null
+            mSectionHeaderPositions = null
+        }
+
+        /**
+         * Rebuilds all internal data structures from scratch.
+         */
+        fun refresh() {
+            // Update the 12/24 hour mode.
+            mIs24HoursMode = DateFormat.is24HourFormat(mContext)
+
+            // Refresh the user selections.
+            val selected = DataModel.dataModel.selectedCities as List<City>
+            mUserSelectedCities.clear()
+            mUserSelectedCities.addAll(selected)
+            mOriginalUserSelectionCount = selected.size
+
+            // Recompute section headers.
+            clearSectionHeaders()
+
+            // Recompute filtered cities.
+            filter(mSearchMenuItemController.queryText)
+        }
+
+        /**
+         * Filter the cities using the given `queryText`.
+         */
+        fun filter(queryText: String) {
+            mSearchMenuItemController.queryText = queryText
+            val query = City.removeSpecialCharacters(queryText.toUpperCase())
+
+            // Compute the filtered list of cities.
+            val filteredCities = if (TextUtils.isEmpty(query)) {
+                DataModel.dataModel.allCities
+            } else {
+                val unselected: List<City> = DataModel.dataModel.unselectedCities
+                val queriedCities: MutableList<City> = ArrayList(unselected.size)
+                for (city in unselected) {
+                    if (city.matches(query)) {
+                        queriedCities.add(city)
+                    }
+                }
+                queriedCities
+            }
+
+            // Swap in the filtered list of cities and notify of the data change.
+            mFilteredCities = filteredCities
+            notifyDataSetChanged()
+        }
+
+        val isFiltering: Boolean
+            get() = !TextUtils.isEmpty(mSearchMenuItemController.queryText.trim({ it <= ' ' }))
+
+        val selectedCities: Collection<City>
+            get() = mUserSelectedCities
+
+        private fun hasHeader(): Boolean {
+            return !isFiltering && mOriginalUserSelectionCount > 0
+        }
+
+        private val citySort: DataModel.CitySort
+            get() = DataModel.dataModel.citySort
+
+        private val citySortComparator: Comparator<City>
+            get() = DataModel.dataModel.cityIndexComparator
+
+        private fun getTimeCharSequence(timeZone: TimeZone): CharSequence {
+            mCalendar.timeZone = timeZone
+            return DateFormat.format(if (mIs24HoursMode) mPattern24 else mPattern12, mCalendar)
+        }
+
+        private fun getShowIndex(position: Int): Boolean {
+            // Indexes are never displayed on filtered cities.
+            if (isFiltering) {
+                return false
+            }
+
+            if (hasHeader()) {
+                // None of the original user selections should show their index.
+                if (position <= mOriginalUserSelectionCount) {
+                    return false
+                }
+
+                // The first item after the original user selections must always show its index.
+                if (position == mOriginalUserSelectionCount + 1) {
+                    return true
+                }
+            } else {
+                // None of the original user selections should show their index.
+                if (position < mOriginalUserSelectionCount) {
+                    return false
+                }
+
+                // The first item after the original user selections must always show its index.
+                if (position == mOriginalUserSelectionCount) {
+                    return true
+                }
+            }
+
+            // Otherwise compare the city with its predecessor to test if it is a header.
+            val priorCity = getItem(position - 1)
+            val city = getItem(position)
+            return citySortComparator.compare(priorCity, city) != 0
+        }
+
+        /**
+         * Cache the child views of each city item view.
+         */
+        private class CityItemHolder(
+            val index: TextView,
+            val name: TextView,
+            val time: TextView,
+            val selected: CheckBox
+        )
+
+        companion object {
+            /**
+             * The type of the single optional "Selected Cities" header entry.
+             */
+            private const val VIEW_TYPE_SELECTED_CITIES_HEADER = 0
+
+            /**
+             * The type of each city entry.
+             */
+            private const val VIEW_TYPE_CITY = 1
+        }
+    }
+
+    private inner class SortOrderMenuItemController : MenuItemController {
+        private val SORT_MENU_RES_ID = R.id.menu_item_sort
+
+        override val id: Int
+            get() = SORT_MENU_RES_ID
+
+        override fun onCreateOptionsItem(menu: Menu) {
+            menu.add(Menu.NONE, R.id.menu_item_sort, Menu.NONE,
+                    R.string.menu_item_sort_by_gmt_offset)
+                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+        }
+
+        override fun onPrepareOptionsItem(item: MenuItem) {
+            item.setTitle(if (DataModel.dataModel.citySort == DataModel.CitySort.NAME) {
+                R.string.menu_item_sort_by_gmt_offset
+            } else {
+                R.string.menu_item_sort_by_name
+            })
+        }
+
+        override fun onOptionsItemSelected(item: MenuItem): Boolean {
+            // Save the new sort order.
+            DataModel.dataModel.toggleCitySort()
+
+            // Section headers are influenced by sort order and must be cleared.
+            mCitiesAdapter.clearSectionHeaders()
+
+            // Honor the new sort order in the adapter.
+            mCitiesAdapter.filter(mSearchMenuItemController.queryText)
+            return true
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 0000000..47c1e22
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,22 @@
+package {
+    // http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "DeskClockTests",
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "junit",
+        "androidx.test.core",
+        "androidx.test.runner",
+        "androidx.test.rules",
+    ],
+    // Include all test java files.
+    srcs: ["src/**/*.java"],
+    platform_apis: true,
+    instrumentation_for: "DeskClock",
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..64cc6e2
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.deskclock.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+             android:targetPackage="com.android.deskclock"
+             android:label="Tests for DeskClock application."/>
+</manifest>
diff --git a/tests/src/com/android/deskclock/ringtone/RingtonePickerActivityTest.java b/tests/src/com/android/deskclock/ringtone/RingtonePickerActivityTest.java
new file mode 100644
index 0000000..7574e9c
--- /dev/null
+++ b/tests/src/com/android/deskclock/ringtone/RingtonePickerActivityTest.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.ItemAdapter.ItemHolder;
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.provider.Alarm;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Iterator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Exercise the user interface that adjusts the selected ringtone.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class RingtonePickerActivityTest {
+
+    private RingtonePickerActivity activity;
+    private RecyclerView ringtoneList;
+    private ItemAdapter<ItemHolder<Uri>> ringtoneAdapter;
+
+    public static final Uri ALERT = Uri.parse("content://settings/system/alarm_alert");
+    public static final Uri CUSTOM_RINGTONE_1 = Uri.parse("content://media/external/audio/one.ogg");
+
+    @Rule
+    public ActivityTestRule<RingtonePickerActivity> rule =
+            new ActivityTestRule<>(RingtonePickerActivity.class, true, false);
+
+    @Before
+    @After
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        prefs.edit().clear().commit();
+    }
+
+    @Test
+    public void validateDefaultState_TimerRingtonePicker() {
+        createTimerRingtonePickerActivity();
+
+        final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.items;
+
+        if (systemRingtoneHolders == null) {
+            return;
+        }
+
+        final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+        final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+        final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+        assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+        final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+        final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+        final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+        Runnable assertRunnable = () -> {
+            assertEquals("Silent", silentHolder.getName());
+            assertEquals("Timer Expired", defaultHolder.getName());
+            assertEquals(DataModel.getDataModel().getDefaultTimerRingtoneUri(),
+                defaultHolder.getUri());
+            // Verify initial selection.
+            assertEquals(
+                DataModel.getDataModel().getTimerRingtoneUri(),
+                DataModel.getDataModel().getDefaultTimerRingtoneUri());
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+    }
+
+    @Test
+    public void validateDefaultState_AlarmRingtonePicker() {
+        createAlarmRingtonePickerActivity(ALERT);
+
+        final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.items;
+        final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+        final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+        final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+        assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+        final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+        final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+        final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+        Runnable assertRunnable = () -> {
+            assertEquals("Silent", silentHolder.getName());
+            assertEquals("Default alarm sound", defaultHolder.getName());
+            assertEquals(DataModel.getDataModel().getDefaultAlarmRingtoneUri(),
+                    defaultHolder.getUri());
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+    }
+
+    @Test
+    public void validateDefaultState_TimerRingtonePicker_WithCustomRingtones() {
+        Runnable customRingtoneRunnable = () -> {
+            DataModel.getDataModel().addCustomRingtone(CUSTOM_RINGTONE_1, "CustomSound");
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(customRingtoneRunnable);
+        createTimerRingtonePickerActivity();
+
+        final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.items;
+        final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+        final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+        final CustomRingtoneHolder customRingtoneHolder = (CustomRingtoneHolder) itemsIter.next();
+        assertEquals("CustomSound", customRingtoneHolder.getName());
+        assertEquals(CUSTOM_RINGTONE_1, customRingtoneHolder.getUri());
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND,
+                customRingtoneHolder.getItemViewType());
+
+        final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+        assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+        final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+        final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+        final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+        Runnable assertRunnable = () -> {
+            assertEquals("Silent", silentHolder.getName());
+            assertEquals("Timer Expired", defaultHolder.getName());
+            assertEquals(DataModel.getDataModel().getDefaultTimerRingtoneUri(),
+                    defaultHolder.getUri());
+            // Verify initial selection.
+            assertEquals(
+                    DataModel.getDataModel().getTimerRingtoneUri(),
+                    DataModel.getDataModel().getDefaultTimerRingtoneUri());
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+
+        Runnable removeCustomRingtoneRunnable = () -> {
+            DataModel.getDataModel().removeCustomRingtone(CUSTOM_RINGTONE_1);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(removeCustomRingtoneRunnable);
+    }
+
+    @Test
+    public void validateDefaultState_AlarmRingtonePicker_WithCustomRingtones() {
+        Runnable customRingtoneRunnable = () -> {
+            DataModel.getDataModel().addCustomRingtone(CUSTOM_RINGTONE_1, "CustomSound");
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(customRingtoneRunnable);
+        createAlarmRingtonePickerActivity(ALERT);
+
+        final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.items;
+        final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+        final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+        final CustomRingtoneHolder customRingtoneHolder = (CustomRingtoneHolder) itemsIter.next();
+        assertEquals("CustomSound", customRingtoneHolder.getName());
+        assertEquals(CUSTOM_RINGTONE_1, customRingtoneHolder.getUri());
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND,
+                customRingtoneHolder.getItemViewType());
+
+        final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+        assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+        final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+        assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+        assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+        final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+        final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+        assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+        Runnable assertRunnable = () -> {
+            assertEquals("Silent", silentHolder.getName());
+            assertEquals("Default alarm sound", defaultHolder.getName());
+            assertEquals(DataModel.getDataModel().getDefaultAlarmRingtoneUri(),
+                    defaultHolder.getUri());
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+
+        Runnable removeCustomRingtoneRunnable = () -> {
+            DataModel.getDataModel().removeCustomRingtone(CUSTOM_RINGTONE_1);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(removeCustomRingtoneRunnable);
+    }
+
+    private void createTimerRingtonePickerActivity() {
+        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final Intent newIntent = new Intent();
+
+        Runnable createIntentRunnable = () -> {
+            final Intent intent = RingtonePickerActivity.createTimerRingtonePickerIntent(context);
+            newIntent.fillIn(intent, 0);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(createIntentRunnable);
+
+        createRingtonePickerActivity(newIntent);
+    }
+
+    private void createAlarmRingtonePickerActivity(Uri ringtone) {
+        final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final Intent newIntent = new Intent();
+
+        Runnable createIntentRunnable = () -> {
+            // Use the custom ringtone in some alarms.
+            final Alarm alarm = new Alarm(1, 1);
+            alarm.enabled = true;
+            alarm.vibrate = true;
+            alarm.alert = ringtone;
+            alarm.deleteAfterUse = true;
+
+            final Intent intent =
+                    RingtonePickerActivity.createAlarmRingtonePickerIntent(context, alarm);
+            newIntent.fillIn(intent, 0);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(createIntentRunnable);
+
+        createRingtonePickerActivity(newIntent);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void createRingtonePickerActivity(Intent intent) {
+        activity = rule.launchActivity(intent);
+        ringtoneList = activity.findViewById(R.id.ringtone_content);
+        ringtoneAdapter = (ItemAdapter<ItemHolder<Uri>>) ringtoneList.getAdapter();
+    }
+}
diff --git a/tests/src/com/android/deskclock/timer/ExpiredTimersActivityTest.java b/tests/src/com/android/deskclock/timer/ExpiredTimersActivityTest.java
new file mode 100644
index 0000000..1fd7b0f
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/ExpiredTimersActivityTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Intent;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertSame;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class ExpiredTimersActivityTest {
+
+    @Rule
+    public ActivityTestRule<ExpiredTimersActivity> rule =
+            new ActivityTestRule<>(ExpiredTimersActivity.class, true, false);
+
+    @Test
+    public void configurationChanges_DoNotResetFiringTimer() {
+        // Construct an ExpiredTimersActivity to display the firing timer.
+        final Context context = ApplicationProvider.getApplicationContext();
+        final Intent intent = new Intent()
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+        final ExpiredTimersActivity activity = rule.launchActivity(intent);
+
+        Runnable fireTimerRunnable = () -> {
+            // Create a firing timer.
+            final DataModel dm = DataModel.getDataModel();
+            Timer timer = dm.addTimer(60000L, "", false);
+            dm.startTimer(timer);
+            dm.expireTimer(null, dm.getTimer(timer.getId()));
+            timer = dm.getTimer(timer.getId());
+
+            // Make the ExpiredTimersActivity believe it has been displayed to the user.
+            activity.getWindow().getCallback().onWindowFocusChanged(true);
+
+            // Simulate a configuration change by recreating the activity.
+            activity.recreate();
+
+            // Verify that the recreation did not alter the firing timer.
+            assertSame(timer, dm.getTimer(timer.getId()));
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(fireTimerRunnable);
+    }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerFragmentTest.java b/tests/src/com/android/deskclock/timer/TimerFragmentTest.java
new file mode 100644
index 0000000..9728db6
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerFragmentTest.java
@@ -0,0 +1,718 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Intent;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+import com.android.deskclock.widget.MockFabContainer;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerFragmentTest {
+
+    private static int LIGHT;
+    private static int DARK;
+    private static int TOP;
+    private static int BOTTOM;
+
+    private static final int GONE = 0;
+
+    private TimerFragment fragment;
+    private View timersView;
+    private View timerSetupView;
+    private ViewPager viewPager;
+    private TimerPagerAdapter adapter;
+
+    private ImageView fab;
+    private Button leftButton;
+    private Button rightButton;
+
+    @Rule
+    public ActivityTestRule<DeskClock> rule = new ActivityTestRule<>(DeskClock.class, true);
+
+    @BeforeClass
+    public static void staticSetUp() {
+        LIGHT = R.drawable.ic_swipe_circle_light;
+        DARK = R.drawable.ic_swipe_circle_dark;
+        TOP = R.drawable.ic_swipe_circle_top;
+        BOTTOM = R.drawable.ic_swipe_circle_bottom;
+    }
+
+    private void setUpSingleTimer() {
+        Runnable addTimerRunnable = () -> {
+            DataModel.getDataModel().addTimer(60000L, null, false);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+        setUpFragment();
+    }
+
+    private void setUpTwoTimers() {
+        Runnable addTimerRunnable = () -> {
+            DataModel.getDataModel().addTimer(60000L, null, false);
+            DataModel.getDataModel().addTimer(90000L, null, false);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+        setUpFragment();
+    }
+
+    private void setUpFragment() {
+        Runnable setUpFragmentRunnable = () -> {
+            ViewPager deskClockPager =
+                    (ViewPager) rule.getActivity().findViewById(R.id.desk_clock_pager);
+            PagerAdapter tabPagerAdapter = (PagerAdapter) deskClockPager.getAdapter();
+            fragment = (TimerFragment) tabPagerAdapter.instantiateItem(deskClockPager, 2);
+            fragment.onStart();
+            fragment.selectTab();
+            final MockFabContainer fabContainer =
+                    new MockFabContainer(fragment, ApplicationProvider.getApplicationContext());
+            fragment.setFabContainer(fabContainer);
+
+            final View view = fragment.getView();
+            assertNotNull(view);
+
+            timersView = view.findViewById(R.id.timer_view);
+            timerSetupView = view.findViewById(R.id.timer_setup);
+            viewPager = view.findViewById(R.id.vertical_view_pager);
+            adapter = (TimerPagerAdapter) viewPager.getAdapter();
+
+            fab = fabContainer.getFab();
+            leftButton = fabContainer.getLeftButton();
+            rightButton = fabContainer.getRightButton();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(setUpFragmentRunnable);
+    }
+
+    @After
+    public void tearDown() {
+        clearTimers();
+        fragment = null;
+        fab = null;
+        timerSetupView = null;
+        timersView = null;
+        adapter = null;
+        viewPager = null;
+        leftButton = null;
+        rightButton = null;
+    }
+
+    private void clearTimers() {
+        Runnable clearTimersRunnable = () -> {
+            final List<Timer> timers = new ArrayList<>(DataModel.getDataModel().getTimers());
+            for (Timer timer : timers) {
+                DataModel.getDataModel().removeTimer(timer);
+            }
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(clearTimersRunnable);
+    }
+
+    @Test
+    public void initialStateNoTimers() {
+        setUpFragment();
+        assertEquals(View.VISIBLE, timerSetupView.getVisibility());
+        assertEquals(View.GONE, timersView.getVisibility());
+        assertAdapter(0);
+    }
+
+    @Test
+    public void initialStateOneTimer() {
+        setUpSingleTimer();
+        assertEquals(View.VISIBLE, timersView.getVisibility());
+        assertEquals(View.GONE, timerSetupView.getVisibility());
+        assertAdapter(1);
+    }
+
+    @Test
+    public void initialStateTwoTimers() {
+        setUpTwoTimers();
+        assertEquals(View.VISIBLE, timersView.getVisibility());
+        assertEquals(View.GONE, timerSetupView.getVisibility());
+        assertAdapter(2);
+    }
+
+    @Test
+    public void timeClick_startsTimer() {
+        setUpSingleTimer();
+
+        setCurrentItem(0);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+        final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 0);
+    }
+
+    @Test
+    public void timeClick_startsSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+        final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void timeClick_pausesTimer() {
+        setUpSingleTimer();
+
+        setCurrentItem(0);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+        final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.PAUSED, 0);
+    }
+
+    @Test
+    public void timeClick_pausesSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+        final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.PAUSED, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void timeClick_restartsTimer() {
+        setUpSingleTimer();
+
+        setCurrentItem(0);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+        final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.PAUSED, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 0);
+    }
+
+    @Test
+    public void timeClick_restartsSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+        final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.PAUSED, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(timeText);
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void fabClick_startsTimer() {
+        setUpSingleTimer();
+
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 0);
+    }
+
+    @Test
+    public void fabClick_startsSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void fabClick_pausesTimer() {
+        setUpSingleTimer();
+
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 0);
+        clickFab();
+        assertStateEquals(Timer.State.PAUSED, 0);
+    }
+
+    @Test
+    public void fabClick_pausesSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.PAUSED, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void fabClick_restartsTimer() {
+        setUpSingleTimer();
+
+        setCurrentItem(0);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 0);
+        clickFab();
+        assertStateEquals(Timer.State.PAUSED, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 0);
+    }
+
+    @Test
+    public void fabClick_restartsSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.PAUSED, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void fabClick_resetsTimer() {
+        setUpSingleTimer();
+
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 0);
+        final Context context = fab.getContext();
+        Runnable expireTimerRunnable = () -> {
+            DataModel.getDataModel().expireTimer(null, DataModel.getDataModel().getTimers().get(0));
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(expireTimerRunnable);
+        clickFab();
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void fabClick_resetsSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        final Context context = fab.getContext();
+        Runnable expireTimerRunnable = () -> {
+            DataModel.getDataModel().expireTimer(null, DataModel.getDataModel().getTimers().get(1));
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(expireTimerRunnable);
+        clickFab();
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void clickAdd_addsOneMinuteToTimer() {
+        setUpSingleTimer();
+
+        setCurrentItem(0);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+        final Button addMinute = timerItem.findViewById(R.id.reset_add);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 0);
+        Runnable getTimersRunnable = () -> {
+            long remainingTime1 = DataModel.getDataModel().getTimers().get(0).getRemainingTime();
+            addMinute.performClick();
+            long remainingTime2 = DataModel.getDataModel().getTimers().get(0).getRemainingTime();
+            assertSame(Timer.State.RUNNING, DataModel.getDataModel().getTimers().get(0).getState());
+            long expectedSeconds =
+                    TimeUnit.MILLISECONDS.toSeconds(remainingTime1 + DateUtils.MINUTE_IN_MILLIS);
+            long observedSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime2);
+            assertEquals(expectedSeconds, observedSeconds);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(getTimersRunnable);
+    }
+
+    @Test
+    public void clickAdd_addsOneMinuteToSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+        final Button addMinute = timerItem.findViewById(R.id.reset_add);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        Runnable getTimersRunnable = () -> {
+            long remainingTime1 = DataModel.getDataModel().getTimers().get(1).getRemainingTime();
+            addMinute.performClick();
+            long remainingTime2 = DataModel.getDataModel().getTimers().get(1).getRemainingTime();
+            assertSame(Timer.State.RUNNING, DataModel.getDataModel().getTimers().get(1).getState());
+            assertSame(Timer.State.RESET, DataModel.getDataModel().getTimers().get(0).getState());
+            long expectedSeconds =
+                    TimeUnit.MILLISECONDS.toSeconds(remainingTime1 + DateUtils.MINUTE_IN_MILLIS);
+            long observedSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime2);
+            assertEquals(expectedSeconds, observedSeconds);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(getTimersRunnable);
+    }
+
+    @Test
+    public void clickReset_resetsTimer() {
+        setUpSingleTimer();
+
+        setCurrentItem(0);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+        final Button reset = timerItem.findViewById(R.id.reset_add);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 0);
+        clickFab();
+        assertStateEquals(Timer.State.PAUSED, 0);
+        clickView(reset);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void clickReset_resetsSecondTimer() {
+        setUpTwoTimers();
+
+        setCurrentItem(1);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+        final Button reset = timerItem.findViewById(R.id.reset_add);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.RUNNING, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickFab();
+        assertStateEquals(Timer.State.PAUSED, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(reset);
+        assertStateEquals(Timer.State.RESET, 1);
+        assertStateEquals(Timer.State.RESET, 0);
+    }
+
+    @Test
+    public void labelClick_opensLabel() {
+        setUpSingleTimer();
+
+        setCurrentItem(0);
+        final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+        final TextView label = timerItem.findViewById(R.id.timer_label);
+        assertStateEquals(Timer.State.RESET, 0);
+        clickView(label);
+    }
+
+    //
+    // 3 Indicators
+    //
+
+    @Test
+    public void verify3Indicators0Pages() {
+        assertIndicatorsEquals(0, 3, 0, GONE, GONE, GONE);
+    }
+
+    @Test
+    public void verify3Indicators1Page() {
+        assertIndicatorsEquals(0, 3, 1, GONE, GONE, GONE);
+    }
+
+    @Test
+    public void verify3Indicators2Pages() {
+        assertIndicatorsEquals(0, 3, 2, LIGHT, DARK, GONE);
+        assertIndicatorsEquals(1, 3, 2, DARK, LIGHT, GONE);
+    }
+
+    @Test
+    public void verify3Indicators3Pages() {
+        assertIndicatorsEquals(0, 3, 3, LIGHT, DARK, DARK);
+        assertIndicatorsEquals(1, 3, 3, DARK, LIGHT, DARK);
+        assertIndicatorsEquals(2, 3, 3, DARK, DARK, LIGHT);
+    }
+
+    @Test
+    public void verify3Indicators4Pages() {
+        assertIndicatorsEquals(0, 3, 4, LIGHT, DARK, BOTTOM);
+        assertIndicatorsEquals(1, 3, 4, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(2, 3, 4, TOP, LIGHT, DARK);
+        assertIndicatorsEquals(3, 3, 4, TOP, DARK, LIGHT);
+    }
+
+    @Test
+    public void verify3Indicators5Pages() {
+        assertIndicatorsEquals(0, 3, 5, LIGHT, DARK, BOTTOM);
+        assertIndicatorsEquals(1, 3, 5, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(2, 3, 5, TOP, LIGHT, BOTTOM);
+        assertIndicatorsEquals(3, 3, 5, TOP, LIGHT, DARK);
+        assertIndicatorsEquals(4, 3, 5, TOP, DARK, LIGHT);
+    }
+
+    @Test
+    public void verify3Indicators6Pages() {
+        assertIndicatorsEquals(0, 3, 6, LIGHT, DARK, BOTTOM);
+        assertIndicatorsEquals(1, 3, 6, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(2, 3, 6, TOP, LIGHT, BOTTOM);
+        assertIndicatorsEquals(3, 3, 6, TOP, LIGHT, BOTTOM);
+        assertIndicatorsEquals(4, 3, 6, TOP, LIGHT, DARK);
+        assertIndicatorsEquals(5, 3, 6, TOP, DARK, LIGHT);
+    }
+
+    @Test
+    public void verify3Indicators7Pages() {
+        assertIndicatorsEquals(0, 3, 7, LIGHT, DARK, BOTTOM);
+        assertIndicatorsEquals(1, 3, 7, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(2, 3, 7, TOP, LIGHT, BOTTOM);
+        assertIndicatorsEquals(3, 3, 7, TOP, LIGHT, BOTTOM);
+        assertIndicatorsEquals(4, 3, 7, TOP, LIGHT, BOTTOM);
+        assertIndicatorsEquals(5, 3, 7, TOP, LIGHT, DARK);
+        assertIndicatorsEquals(6, 3, 7, TOP, DARK, LIGHT);
+    }
+
+    //
+    // 4 Indicators
+    //
+
+    @Test
+    public void verify4Indicators0Pages() {
+        assertIndicatorsEquals(0, 4, 0, GONE, GONE, GONE, GONE);
+    }
+
+    @Test
+    public void verify4Indicators1Page() {
+        assertIndicatorsEquals(0, 4, 1, GONE, GONE, GONE, GONE);
+    }
+
+    @Test
+    public void verify4Indicators2Pages() {
+        assertIndicatorsEquals(0, 4, 2, LIGHT, DARK, GONE, GONE);
+        assertIndicatorsEquals(1, 4, 2, DARK, LIGHT, GONE, GONE);
+    }
+
+    @Test
+    public void verify4Indicators3Pages() {
+        assertIndicatorsEquals(0, 4, 3, LIGHT, DARK, DARK, GONE);
+        assertIndicatorsEquals(1, 4, 3, DARK, LIGHT, DARK, GONE);
+        assertIndicatorsEquals(2, 4, 3, DARK, DARK, LIGHT, GONE);
+    }
+
+    @Test
+    public void verify4Indicators4Pages() {
+        assertIndicatorsEquals(0, 4, 4, LIGHT, DARK, DARK, DARK);
+        assertIndicatorsEquals(1, 4, 4, DARK, LIGHT, DARK, DARK);
+        assertIndicatorsEquals(2, 4, 4, DARK, DARK, LIGHT, DARK);
+        assertIndicatorsEquals(3, 4, 4, DARK, DARK, DARK, LIGHT);
+    }
+
+    @Test
+    public void verify4Indicators5Pages() {
+        assertIndicatorsEquals(0, 4, 5, LIGHT, DARK, DARK, BOTTOM);
+        assertIndicatorsEquals(1, 4, 5, DARK, LIGHT, DARK, BOTTOM);
+        assertIndicatorsEquals(2, 4, 5, DARK, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(3, 4, 5, TOP, DARK, LIGHT, DARK);
+        assertIndicatorsEquals(4, 4, 5, TOP, DARK, DARK, LIGHT);
+    }
+
+    @Test
+    public void verify4Indicators6Pages() {
+        assertIndicatorsEquals(0, 4, 6, LIGHT, DARK, DARK, BOTTOM);
+        assertIndicatorsEquals(1, 4, 6, DARK, LIGHT, DARK, BOTTOM);
+        assertIndicatorsEquals(2, 4, 6, DARK, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(3, 4, 6, TOP, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(4, 4, 6, TOP, DARK, LIGHT, DARK);
+        assertIndicatorsEquals(5, 4, 6, TOP, DARK, DARK, LIGHT);
+    }
+
+    @Test
+    public void verify4Indicators7Pages() {
+        assertIndicatorsEquals(0, 4, 7, LIGHT, DARK, DARK, BOTTOM);
+        assertIndicatorsEquals(1, 4, 7, DARK, LIGHT, DARK, BOTTOM);
+        assertIndicatorsEquals(2, 4, 7, DARK, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(3, 4, 7, TOP, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(4, 4, 7, TOP, DARK, LIGHT, BOTTOM);
+        assertIndicatorsEquals(5, 4, 7, TOP, DARK, LIGHT, DARK);
+        assertIndicatorsEquals(6, 4, 7, TOP, DARK, DARK, LIGHT);
+    }
+
+    @Test
+    public void showTimerSetupView_fromIntent() {
+        setUpSingleTimer();
+
+        assertEquals(View.VISIBLE, timersView.getVisibility());
+        assertEquals(View.GONE, timerSetupView.getVisibility());
+
+        final Intent intent = TimerFragment.createTimerSetupIntent(fragment.getContext());
+        rule.getActivity().setIntent(intent);
+        restartFragment();
+
+        assertEquals(View.GONE, timersView.getVisibility());
+        assertEquals(View.VISIBLE, timerSetupView.getVisibility());
+    }
+
+    @Test
+    public void showTimerSetupView_usesLabel_fromIntent() {
+        setUpSingleTimer();
+
+        assertEquals(View.VISIBLE, timersView.getVisibility());
+        assertEquals(View.GONE, timerSetupView.getVisibility());
+
+        final Intent intent = TimerFragment.createTimerSetupIntent(fragment.getContext());
+        rule.getActivity().setIntent(intent);
+        restartFragment();
+
+        assertEquals(View.GONE, timersView.getVisibility());
+        assertEquals(View.VISIBLE, timerSetupView.getVisibility());
+        clickView(timerSetupView.findViewById(R.id.timer_setup_digit_3));
+
+        clickFab();
+    }
+
+    @Test
+    public void showTimer_fromIntent() {
+        setUpTwoTimers();
+
+        assertEquals(View.VISIBLE, timersView.getVisibility());
+        assertEquals(View.GONE, timerSetupView.getVisibility());
+        assertEquals(0, viewPager.getCurrentItem());
+
+        final Intent intent =
+                new Intent(ApplicationProvider.getApplicationContext(), TimerService.class)
+                        .setAction(TimerService.ACTION_SHOW_TIMER)
+                        .putExtra(TimerService.EXTRA_TIMER_ID, 0);
+        rule.getActivity().setIntent(intent);
+        restartFragment();
+
+        assertEquals(View.VISIBLE, timersView.getVisibility());
+        assertEquals(View.GONE, timerSetupView.getVisibility());
+        assertEquals(1, viewPager.getCurrentItem());
+    }
+
+    private void assertIndicatorsEquals(
+            int page, int indicatorCount, int pageCount, int... expected) {
+        int[] actual = TimerFragment.computePageIndicatorStates(page, indicatorCount, pageCount);
+        if (!Arrays.equals(expected, actual)) {
+            final String expectedString = Arrays.toString(expected);
+            final String actualString = Arrays.toString(actual);
+            fail(String.format("Expected %s, found %s", expectedString, actualString));
+        }
+    }
+
+    private void assertStateEquals(Timer.State expectedState, int index) {
+        Runnable timerRunnable = () -> {
+            final Timer.State actualState =
+                    DataModel.getDataModel().getTimers().get(index).getState();
+            assertSame(expectedState, actualState);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(timerRunnable);
+    }
+
+    private void assertAdapter(int count) {
+        Runnable assertRunnable = () -> {
+            assertEquals(count, adapter.getCount());
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+    }
+
+    private void restartFragment() {
+        Runnable onStartRunnable = () -> {
+            fragment.onStart();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(onStartRunnable);
+    }
+
+    private void setCurrentItem(int position) {
+        Runnable setCurrentItemRunnable = () -> {
+            viewPager.setCurrentItem(position);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(setCurrentItemRunnable);
+    }
+
+    private void clickView(View view) {
+        Runnable clickRunnable = () -> {
+            view.performClick();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(clickRunnable);
+    }
+
+    private void clickFab() {
+        Runnable clickRunnable = () -> {
+            fab.performClick();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(clickRunnable);
+    }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerItemFragmentTest.java b/tests/src/com/android/deskclock/timer/TimerItemFragmentTest.java
new file mode 100644
index 0000000..1152b91
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerItemFragmentTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.View;
+import android.view.ViewGroup;
+
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Exercise the user interface that shows current timers.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerItemFragmentTest {
+
+    @Rule
+    public ActivityTestRule<DeskClock> rule = new ActivityTestRule<>(DeskClock.class, true);
+
+    @Test
+    public void ensureTimerIsHeldSuccessfully_whenOneTimerIsRunning() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final TimerFragment timerFragment = new TimerFragment();
+        rule.getActivity().getSupportFragmentManager()
+                .beginTransaction().add(timerFragment, null).commit();
+        Runnable selectTabRunnable = () -> {
+            timerFragment.selectTab();
+            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+
+            // Get the view held by the TimerFragment
+            final View view = timerFragment.getView();
+            assertNotNull(view);
+
+            // Get the TimerPagerAdapter associated with this view
+            ViewPager viewPager = (ViewPager) view.findViewById(R.id.vertical_view_pager);
+            TimerPagerAdapter adapter = (TimerPagerAdapter) viewPager.getAdapter();
+            ViewGroup viewGroup = view.findViewById(R.id.timer_view);
+
+            // Retrieve the TimerItemFragment from the adapter
+            TimerItemFragment timerItemFragment =
+                    (TimerItemFragment) adapter.instantiateItem(viewGroup, 0);
+
+            // Assert that the correct timer is set
+            assertEquals(timerItemFragment.getTimer(), timer);
+            DataModel.getDataModel().removeTimer(timer);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(selectTabRunnable);
+    }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerServiceTest.java b/tests/src/com/android/deskclock/timer/TimerServiceTest.java
new file mode 100644
index 0000000..985fd0d
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerServiceTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.ComponentName;
+import android.content.Intent;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.app.Service.START_NOT_STICKY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerServiceTest {
+
+    private TimerService timerService;
+    private DataModel dataModel;
+
+    @Before
+    public void setUp() {
+        dataModel = DataModel.getDataModel();
+        timerService = new TimerService();
+        timerService.onCreate();
+    }
+
+    @After
+    public void tearDown() {
+        clearTimers();
+        dataModel = null;
+        timerService = null;
+    }
+
+    private void clearTimers() {
+        Runnable clearTimersRunnable = () -> {
+            final List<Timer> timers = new ArrayList<>(DataModel.getDataModel().getTimers());
+            for (Timer timer : timers) {
+                DataModel.getDataModel().removeTimer(timer);
+            }
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(clearTimersRunnable);
+    }
+
+    @Test
+    public void verifyIntentsHonored_whileTimersFire() {
+        Runnable testRunnable = () -> {
+            Timer timer1 = dataModel.addTimer(60000L, null, false);
+            Timer timer2 = dataModel.addTimer(60000L, null, false);
+            dataModel.startTimer(timer1);
+            dataModel.startTimer(timer2);
+            timer1 = dataModel.getTimer(timer1.getId());
+            timer2 = dataModel.getTimer(timer2.getId());
+
+            // Expire the first timer.
+            dataModel.expireTimer(null, timer1);
+
+            // Have TimerService honor the Intent.
+            assertEquals(START_NOT_STICKY,
+                    timerService.onStartCommand(getTimerServiceIntent(), 0, 0));
+
+            // Expire the second timer.
+            dataModel.expireTimer(null, timer2);
+
+            // Have TimerService honor the Intent which updates the firing timers.
+            assertEquals(START_NOT_STICKY,
+                    timerService.onStartCommand(getTimerServiceIntent(), 0, 1));
+
+            // Reset timer 1.
+            dataModel.resetTimer(dataModel.getTimer(timer1.getId()));
+
+            // Have TimerService honor the Intent which updates the firing timers.
+            assertEquals(START_NOT_STICKY,
+                    timerService.onStartCommand(getTimerServiceIntent(), 0, 2));
+
+            // Remove timer 2.
+            dataModel.removeTimer(dataModel.getTimer(timer2.getId()));
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunnable);
+    }
+
+    @Test
+    public void verifyIntentsHonored_ifNoTimersAreExpired() {
+        Runnable testRunnable = () -> {
+            Timer timer = dataModel.addTimer(60000L, null, false);
+            dataModel.startTimer(timer);
+            timer = dataModel.getTimer(timer.getId());
+
+            // Expire the timer.
+            dataModel.expireTimer(null, timer);
+
+            final Intent timerServiceIntent = getTimerServiceIntent();
+
+            // Reset the timer before TimerService starts.
+            dataModel.resetTimer(dataModel.getTimer(timer.getId()));
+
+            // Have TimerService honor the Intent.
+            assertEquals(START_NOT_STICKY, timerService.onStartCommand(timerServiceIntent, 0, 0));
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunnable);
+    }
+
+    private Intent getTimerServiceIntent() {
+        Intent serviceIntent = new Intent(ApplicationProvider.getApplicationContext(),
+                TimerService.class);
+
+        final ComponentName component = serviceIntent.getComponent();
+        assertNotNull(component);
+        assertEquals(TimerService.class.getName(), component.getClassName());
+
+        return serviceIntent;
+    }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerSetupViewTest.java b/tests/src/com/android/deskclock/timer/TimerSetupViewTest.java
new file mode 100644
index 0000000..ab0e61e
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerSetupViewTest.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.IdRes;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+import com.android.deskclock.widget.MockFabContainer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+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;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Exercise the user interface that collects new timer lengths.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerSetupViewTest {
+
+    private MockFabContainer fabContainer;
+    private TimerSetupView timerSetupView;
+
+    private TextView timeView;
+    private View deleteView;
+
+    private Locale defaultLocale;
+
+    @Rule
+    public ActivityTestRule<DeskClock> rule = new ActivityTestRule<>(DeskClock.class, true);
+
+    @Before
+    public void setUp() {
+        defaultLocale = Locale.getDefault();
+        Locale.setDefault(new Locale("en", "US"));
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        final TimerFragment fragment = new TimerFragment();
+        rule.getActivity().getSupportFragmentManager()
+                .beginTransaction().add(fragment, null).commit();
+        Runnable selectTabRunnable = () -> {
+            fragment.selectTab();
+            fabContainer = new MockFabContainer(fragment, context);
+            fragment.setFabContainer(fabContainer);
+
+            // Fetch the child views the tests will manipulate.
+            final View view = fragment.getView();
+            assertNotNull(view);
+
+            timerSetupView = view.findViewById(R.id.timer_setup);
+            assertNotNull(timerSetupView);
+            assertEquals(VISIBLE, timerSetupView.getVisibility());
+
+            timeView = timerSetupView.findViewById(R.id.timer_setup_time);
+            timeView.setActivated(true);
+            deleteView = timerSetupView.findViewById(R.id.timer_setup_delete);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(selectTabRunnable);
+    }
+
+    @After
+    public void tearDown() {
+        fabContainer = null;
+        timerSetupView = null;
+
+        timeView = null;
+        deleteView = null;
+
+        Locale.setDefault(defaultLocale);
+    }
+
+    private void validateDefaultState() {
+        assertIsReset();
+    }
+
+    @Test
+    public void validateDefaultState_TimersExist() {
+        Runnable addTimerRunnable = () -> {
+            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+            validateDefaultState();
+            DataModel.getDataModel().removeTimer(timer);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+    }
+
+    @Test
+    public void validateDefaultState_NoTimersExist() {
+        Runnable runnable = () -> {
+            validateDefaultState();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+
+    private void type0InDefaultState() {
+        performClick(R.id.timer_setup_digit_0);
+        assertIsReset();
+    }
+
+    @Test
+    public void type0InDefaultState_TimersExist() {
+        Runnable addTimerRunnable = () -> {
+            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+            type0InDefaultState();
+            DataModel.getDataModel().removeTimer(timer);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+    }
+
+    @Test
+    public void type0InDefaultState_NoTimersExist() {
+        Runnable runnable = () -> {
+            type0InDefaultState();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+
+    private void fillDisplayThenDeleteAll() {
+        assertIsReset();
+        // Fill the display.
+        performClick(R.id.timer_setup_digit_1);
+        assertHasValue(0, 0, 1);
+        performClick(R.id.timer_setup_digit_2);
+        assertHasValue(0, 0, 12);
+        performClick(R.id.timer_setup_digit_3);
+        assertHasValue(0, 1, 23);
+        performClick(R.id.timer_setup_digit_4);
+        assertHasValue(0, 12, 34);
+        performClick(R.id.timer_setup_digit_5);
+        assertHasValue(1, 23, 45);
+        performClick(R.id.timer_setup_digit_6);
+        assertHasValue(12, 34, 56);
+
+        // Typing another character is ignored.
+        performClick(R.id.timer_setup_digit_7);
+        assertHasValue(12, 34, 56);
+        performClick(R.id.timer_setup_digit_8);
+        assertHasValue(12, 34, 56);
+
+        // Delete everything in the display.
+        performClick(R.id.timer_setup_delete);
+        assertHasValue(1, 23, 45);
+        performClick(R.id.timer_setup_delete);
+        assertHasValue(0, 12, 34);
+        performClick(R.id.timer_setup_delete);
+        assertHasValue(0, 1, 23);
+        performClick(R.id.timer_setup_delete);
+        assertHasValue(0, 0, 12);
+        performClick(R.id.timer_setup_delete);
+        assertHasValue(0, 0, 1);
+        performClick(R.id.timer_setup_delete);
+        assertIsReset();
+    }
+
+    @Test
+    public void fillDisplayThenDeleteAll_TimersExist() {
+        Runnable addTimerRunnable = () -> {
+            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+            fillDisplayThenDeleteAll();
+            DataModel.getDataModel().removeTimer(timer);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+    }
+
+    @Test
+    public void fillDisplayThenDeleteAll_NoTimersExist() {
+        Runnable runnable = () -> {
+            fillDisplayThenDeleteAll();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+
+    private void fillDisplayWith9s() {
+        performClick(R.id.timer_setup_digit_9);
+        performClick(R.id.timer_setup_digit_9);
+        performClick(R.id.timer_setup_digit_9);
+        performClick(R.id.timer_setup_digit_9);
+        performClick(R.id.timer_setup_digit_9);
+        performClick(R.id.timer_setup_digit_9);
+        assertHasValue(99, 99, 99);
+    }
+
+    @Test
+    public void fillDisplayWith9s_TimersExist() {
+        Runnable addTimerRunnable = () -> {
+            Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+            fillDisplayWith9s();
+            DataModel.getDataModel().removeTimer(timer);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+    }
+
+    @Test
+    public void fillDisplayWith9s_NoTimersExist() {
+        Runnable runnable = () -> {
+            fillDisplayWith9s();
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+    }
+
+    private void assertIsReset() {
+        assertStateEquals(0, 0, 0);
+        assertFalse(timerSetupView.hasValidInput());
+        assertEquals(0, timerSetupView.getTimeInMillis());
+
+        assertTrue(TextUtils.equals("00h 00m 00s", timeView.getText()));
+        assertTrue(TextUtils.equals("0 hours, 0 minutes, 0 seconds",
+                timeView.getContentDescription()));
+
+        assertFalse(deleteView.isEnabled());
+        assertTrue(TextUtils.equals("Delete", deleteView.getContentDescription()));
+
+        final View fab = fabContainer.getFab();
+        final TextView leftButton = fabContainer.getLeftButton();
+        final TextView rightButton = fabContainer.getRightButton();
+
+        if (DataModel.getDataModel().getTimers().isEmpty()) {
+            assertEquals(INVISIBLE, leftButton.getVisibility());
+        } else {
+            assertEquals(VISIBLE, leftButton.getVisibility());
+            assertTrue(TextUtils.equals("Cancel", leftButton.getText()));
+        }
+
+        assertNull(fab.getContentDescription());
+        assertEquals(INVISIBLE, fab.getVisibility());
+        assertEquals(INVISIBLE, rightButton.getVisibility());
+    }
+
+    private void assertHasValue(int hours, int minutes, int seconds) {
+        final long time =
+                hours * HOUR_IN_MILLIS + minutes * MINUTE_IN_MILLIS + seconds * SECOND_IN_MILLIS;
+        assertStateEquals(hours, minutes, seconds);
+        assertTrue(timerSetupView.hasValidInput());
+        assertEquals(time, timerSetupView.getTimeInMillis());
+
+        final String timeString =
+                String.format(Locale.US, "%02dh %02dm %02ds", hours, minutes, seconds);
+        assertTrue(TextUtils.equals(timeString, timeView.getText()));
+
+        assertTrue(deleteView.isEnabled());
+        assertTrue(TextUtils.equals("Delete " + seconds % 10, deleteView.getContentDescription()));
+
+        final View fab = fabContainer.getFab();
+        final TextView leftButton = fabContainer.getLeftButton();
+        final TextView rightButton = fabContainer.getRightButton();
+
+        if (DataModel.getDataModel().getTimers().isEmpty()) {
+            assertEquals(INVISIBLE, leftButton.getVisibility());
+        } else {
+            assertEquals(VISIBLE, leftButton.getVisibility());
+            assertTrue(TextUtils.equals("Cancel", leftButton.getText()));
+        }
+
+        assertEquals(VISIBLE, fab.getVisibility());
+        assertTrue(TextUtils.equals("Start", fab.getContentDescription()));
+        assertEquals(INVISIBLE, rightButton.getVisibility());
+    }
+
+    private void assertStateEquals(int hours, int minutes, int seconds) {
+        final int[] expected = {
+                seconds % 10, seconds / 10, minutes % 10, minutes / 10, hours % 10, hours / 10
+        };
+        final int[] actual = (int[]) timerSetupView.getState();
+        assertArrayEquals(expected, actual);
+    }
+
+    private void performClick(@IdRes int id) {
+        final View view = timerSetupView.findViewById(id);
+        assertNotNull(view);
+        assertTrue(view.performClick());
+    }
+}
diff --git a/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java b/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java
new file mode 100644
index 0000000..9893545
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class FormattedStringModelTest {
+
+    private FormattedStringModel model;
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        model = new FormattedStringModel(context);
+    }
+
+    @After
+    public void tearDown() {
+        model = null;
+    }
+
+    @Test
+    public void positiveFormattedNumberWithNoPadding() {
+        assertEquals("0", model.getFormattedNumber(0));
+        assertEquals("9", model.getFormattedNumber(9));
+        assertEquals("10", model.getFormattedNumber(10));
+        assertEquals("99", model.getFormattedNumber(99));
+        assertEquals("100", model.getFormattedNumber(100));
+    }
+
+    @Test
+    public void positiveFormattedNumber() {
+        assertEquals("0", model.getFormattedNumber(false, 0, 1));
+        assertEquals("00", model.getFormattedNumber(false, 0, 2));
+        assertEquals("000", model.getFormattedNumber(false, 0, 3));
+
+        assertEquals("9", model.getFormattedNumber(false, 9, 1));
+        assertEquals("09", model.getFormattedNumber(false, 9, 2));
+        assertEquals("009", model.getFormattedNumber(false, 9, 3));
+
+        assertEquals("90", model.getFormattedNumber(false, 90, 2));
+        assertEquals("090", model.getFormattedNumber(false, 90, 3));
+
+        assertEquals("999", model.getFormattedNumber(false, 999, 3));
+    }
+
+    @Test
+    public void negativeFormattedNumber() {
+        assertEquals("−0", model.getFormattedNumber(true, 0, 1));
+        assertEquals("−00", model.getFormattedNumber(true, 0, 2));
+        assertEquals("−000", model.getFormattedNumber(true, 0, 3));
+
+        assertEquals("−9", model.getFormattedNumber(true, 9, 1));
+        assertEquals("−09", model.getFormattedNumber(true, 9, 2));
+        assertEquals("−009", model.getFormattedNumber(true, 9, 3));
+
+        assertEquals("−90", model.getFormattedNumber(true, 90, 2));
+        assertEquals("−090", model.getFormattedNumber(true, 90, 3));
+
+        assertEquals("−999", model.getFormattedNumber(true, 999, 3));
+    }
+}
diff --git a/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java b/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java
new file mode 100644
index 0000000..3df7404
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.HOUR;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.MIDNIGHT;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.MINUTE;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.QUARTER_HOUR;
+
+import static java.util.Calendar.MILLISECOND;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class PeriodicCallbackModelTest {
+
+    @Test
+    public void getMinuteDelay() {
+        assertEquals(1, PeriodicCallbackModel.getDelay(56999, MINUTE, -3000));
+        assertEquals(60000, PeriodicCallbackModel.getDelay(57000, MINUTE, -3000));
+        assertEquals(59999, PeriodicCallbackModel.getDelay(57001, MINUTE, -3000));
+
+        assertEquals(1, PeriodicCallbackModel.getDelay(59999, MINUTE, 0));
+        assertEquals(60000, PeriodicCallbackModel.getDelay(60000, MINUTE, 0));
+        assertEquals(59999, PeriodicCallbackModel.getDelay(60001, MINUTE, 0));
+
+        assertEquals(3001, PeriodicCallbackModel.getDelay(59999, MINUTE, 3000));
+        assertEquals(3000, PeriodicCallbackModel.getDelay(60000, MINUTE, 3000));
+        assertEquals(1, PeriodicCallbackModel.getDelay(62999, MINUTE, 3000));
+        assertEquals(60000, PeriodicCallbackModel.getDelay(63000, MINUTE, 3000));
+        assertEquals(59999, PeriodicCallbackModel.getDelay(63001, MINUTE, 3000));
+    }
+
+    @Test
+    public void getQuarterHourDelay() {
+        assertEquals(1, PeriodicCallbackModel.getDelay(896999, QUARTER_HOUR, -3000));
+        assertEquals(900000, PeriodicCallbackModel.getDelay(897000, QUARTER_HOUR, -3000));
+        assertEquals(899999, PeriodicCallbackModel.getDelay(897001, QUARTER_HOUR, -3000));
+
+        assertEquals(1, PeriodicCallbackModel.getDelay(899999, QUARTER_HOUR, 0));
+        assertEquals(900000, PeriodicCallbackModel.getDelay(900000, QUARTER_HOUR, 0));
+        assertEquals(899999, PeriodicCallbackModel.getDelay(900001, QUARTER_HOUR, 0));
+
+        assertEquals(3001, PeriodicCallbackModel.getDelay(899999, QUARTER_HOUR, 3000));
+        assertEquals(3000, PeriodicCallbackModel.getDelay(900000, QUARTER_HOUR, 3000));
+        assertEquals(1, PeriodicCallbackModel.getDelay(902999, QUARTER_HOUR, 3000));
+        assertEquals(900000, PeriodicCallbackModel.getDelay(903000, QUARTER_HOUR, 3000));
+        assertEquals(899999, PeriodicCallbackModel.getDelay(903001, QUARTER_HOUR, 3000));
+    }
+
+    @Test
+    public void getHourDelay() {
+        assertEquals(1, PeriodicCallbackModel.getDelay(3596999, HOUR, -3000));
+        assertEquals(3600000, PeriodicCallbackModel.getDelay(3597000, HOUR, -3000));
+        assertEquals(3599999, PeriodicCallbackModel.getDelay(3597001, HOUR, -3000));
+
+        assertEquals(1, PeriodicCallbackModel.getDelay(3599999, HOUR, 0));
+        assertEquals(3600000, PeriodicCallbackModel.getDelay(3600000, HOUR, 0));
+        assertEquals(3599999, PeriodicCallbackModel.getDelay(3600001, HOUR, 0));
+
+        assertEquals(3001, PeriodicCallbackModel.getDelay(3599999, HOUR, 3000));
+        assertEquals(3000, PeriodicCallbackModel.getDelay(3600000, HOUR, 3000));
+        assertEquals(1, PeriodicCallbackModel.getDelay(3602999, HOUR, 3000));
+        assertEquals(3600000, PeriodicCallbackModel.getDelay(3603000, HOUR, 3000));
+        assertEquals(3599999, PeriodicCallbackModel.getDelay(3603001, HOUR, 3000));
+    }
+
+    @Test
+    public void getMidnightDelay() {
+        final Calendar c = Calendar.getInstance();
+        c.set(2016, 0, 20, 0, 0, 0);
+        c.set(MILLISECOND, 0);
+        final long now = c.getTimeInMillis();
+
+        assertEquals(1, PeriodicCallbackModel.getDelay(now - 3001, MIDNIGHT, -3000));
+        assertEquals(86400000, PeriodicCallbackModel.getDelay(now - 3000, MIDNIGHT, -3000));
+        assertEquals(86399999, PeriodicCallbackModel.getDelay(now - 2999, MIDNIGHT, -3000));
+
+        assertEquals(1, PeriodicCallbackModel.getDelay(now - 1, MIDNIGHT, 0));
+        assertEquals(86400000, PeriodicCallbackModel.getDelay(now, MIDNIGHT, 0));
+        assertEquals(86399999, PeriodicCallbackModel.getDelay(now + 1, MIDNIGHT, 0));
+
+        assertEquals(3001, PeriodicCallbackModel.getDelay(now - 1, MIDNIGHT, 3000));
+        assertEquals(3000, PeriodicCallbackModel.getDelay(now, MIDNIGHT, 3000));
+        assertEquals(1, PeriodicCallbackModel.getDelay(now + 2999, MIDNIGHT, 3000));
+        assertEquals(86400000, PeriodicCallbackModel.getDelay(now + 3000, MIDNIGHT, 3000));
+        assertEquals(86399999, PeriodicCallbackModel.getDelay(now + 3001, MIDNIGHT, 3000));
+    }
+}
diff --git a/tests/src/com/android/deskclock/uidata/TabModelTest.java b/tests/src/com/android/deskclock/uidata/TabModelTest.java
new file mode 100644
index 0000000..dbe9344
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/TabModelTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.Context;
+import android.preference.PreferenceManager;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+import static com.android.deskclock.uidata.UiDataModel.Tab.ALARMS;
+import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS;
+import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
+import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
+
+import static org.junit.Assert.assertSame;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TabModelTest {
+
+    private Locale defaultLocale;
+    private TabModel tabModel;
+
+    @Test
+    public void ltrTabLayoutIndex() {
+        setUpTabModel(new Locale("en", "US"));
+        assertSame(ALARMS, tabModel.getTabAt(0));
+        assertSame(CLOCKS, tabModel.getTabAt(1));
+        assertSame(TIMERS, tabModel.getTabAt(2));
+        assertSame(STOPWATCH, tabModel.getTabAt(3));
+        Locale.setDefault(defaultLocale);
+    }
+
+    @Test
+    public void rtlTabLayoutIndex() {
+        setUpTabModel(new Locale("ar", "EG"));
+        assertSame(STOPWATCH, tabModel.getTabAt(0));
+        assertSame(TIMERS, tabModel.getTabAt(1));
+        assertSame(CLOCKS, tabModel.getTabAt(2));
+        assertSame(ALARMS, tabModel.getTabAt(3));
+        Locale.setDefault(defaultLocale);
+    }
+
+    private void setUpTabModel(Locale testLocale) {
+        defaultLocale = Locale.getDefault();
+        Locale.setDefault(testLocale);
+        final Context context = ApplicationProvider.getApplicationContext();
+        tabModel = new TabModel(PreferenceManager.getDefaultSharedPreferences(context));
+    }
+}
diff --git a/tests/src/com/android/deskclock/widget/MockFabContainer.java b/tests/src/com/android/deskclock/widget/MockFabContainer.java
new file mode 100644
index 0000000..bdfbf7d
--- /dev/null
+++ b/tests/src/com/android/deskclock/widget/MockFabContainer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import com.android.deskclock.DeskClockFragment;
+import com.android.deskclock.FabContainer;
+
+/**
+ * DeskClock is the normal container for the fab and its left and right buttons. In order to test
+ * each tab in isolation, tests avoid inflating all of DeskClock and instead set this mock fab
+ * container into the DeskClockFragment under test. It mimics the behavior of a fab container,
+ * albeit without animation, so fragment tests can verify the state of the fab any time they like.
+ */
+public final class MockFabContainer implements FabContainer {
+
+    private final DeskClockFragment deskClockFragment;
+
+    private ImageView fab;
+    private Button leftButton;
+    private Button rightButton;
+
+    public MockFabContainer(DeskClockFragment fragment, Context context) {
+        deskClockFragment = fragment;
+        fab = new ImageView(context);
+        leftButton = new Button(context);
+        rightButton = new Button(context);
+
+        updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE);
+
+        fab.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                deskClockFragment.onFabClick(fab);
+            }
+        });
+        leftButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                deskClockFragment.onLeftButtonClick(leftButton);
+            }
+        });
+        rightButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                deskClockFragment.onRightButtonClick(rightButton);
+            }
+        });
+    }
+
+    @Override
+    public void updateFab(@UpdateFabFlag int updateType) {
+        if ((updateType & FabContainer.FAB_ANIMATION_MASK) != 0) {
+            deskClockFragment.onUpdateFab(fab);
+        }
+        if ((updateType & FabContainer.FAB_REQUEST_FOCUS_MASK) != 0) {
+            fab.requestFocus();
+        }
+        if ((updateType & FabContainer.BUTTONS_ANIMATION_MASK) != 0) {
+            deskClockFragment.onUpdateFabButtons(leftButton, rightButton);
+        }
+        if ((updateType & FabContainer.BUTTONS_DISABLE_MASK) != 0) {
+            leftButton.setClickable(false);
+            rightButton.setClickable(false);
+        }
+    }
+
+    public ImageView getFab() {
+        return fab;
+    }
+
+    public Button getLeftButton() {
+        return leftButton;
+    }
+
+    public Button getRightButton() {
+        return rightButton;
+    }
+}
diff --git a/tests/src/com/android/deskclock/worldclock/CitySelectionActivityTest.java b/tests/src/com/android/deskclock/worldclock/CitySelectionActivityTest.java
new file mode 100644
index 0000000..46e0030
--- /dev/null
+++ b/tests/src/com/android/deskclock/worldclock/CitySelectionActivityTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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;
+
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import androidx.appcompat.widget.SearchView;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.R;
+import com.android.deskclock.data.City;
+import com.android.deskclock.data.DataModel;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.Locale;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Exercise the user interface that adjusts the selected cities.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class CitySelectionActivityTest {
+
+    private CitySelectionActivity activity;
+    private ListView cities;
+    private ListAdapter citiesAdapter;
+    private SearchView searchView;
+    private Locale defaultLocale;
+
+    @Rule
+    public ActivityTestRule<CitySelectionActivity> rule =
+            new ActivityTestRule<>(CitySelectionActivity.class, true);
+
+    @Before
+    public void setUp() {
+        defaultLocale = Locale.getDefault();
+        Locale.setDefault(new Locale("en", "US"));
+        Runnable setUpRunnable = () -> {
+            final City city = DataModel.getDataModel().getAllCities().get(0);
+            DataModel.getDataModel().setSelectedCities(Collections.singletonList(city));
+            activity = rule.getActivity();
+            cities = activity.findViewById(R.id.cities_list);
+            citiesAdapter = (ListAdapter) cities.getAdapter();
+            searchView = activity.findViewById(R.id.menu_item_search);
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(setUpRunnable);
+    }
+
+    @After
+    public void tearDown() {
+        activity = null;
+        cities = null;
+        searchView = null;
+        Locale.setDefault(defaultLocale);
+    }
+
+    @Test
+    public void validateDefaultState() {
+        Runnable testRunable = () -> {
+            assertNotNull(searchView);
+            assertNotNull(cities);
+            assertNotNull(citiesAdapter);
+            assertEquals(340, citiesAdapter.getCount());
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunable);
+    }
+
+    @Test
+    public void searchCities() {
+        Runnable testRunable = () -> {
+            // Search for cities starting with Z.
+            searchView.setQuery("Z", true);
+            assertEquals(2, citiesAdapter.getCount());
+            assertItemContent(0, "Zagreb");
+            assertItemContent(1, "Zurich");
+
+            // Clear the filter query.
+            searchView.setQuery("", true);
+            assertEquals(340, citiesAdapter.getCount());
+        };
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunable);
+    }
+
+    private void assertItemContent(int index, String cityName) {
+        final City city = (City) citiesAdapter.getItem(index);
+        assertEquals(cityName, city.getName());
+    }
+}