Merge "Merge QQ3A.200605.002 into master"
diff --git a/Android.bp b/Android.bp
index 5f3dc5a..1795461 100644
--- a/Android.bp
+++ b/Android.bp
@@ -42,6 +42,14 @@
     ],
     exclude_srcs: [
         "src/**/alarmclock/*.java",
+        "src/**/deskclock/alarms/AlarmActivity.java",
+        "src/**/deskclock/alarms/AlarmKlaxon.java",
+        "src/**/deskclock/alarms/AlarmTimeClickHandler.java",
+        "src/**/deskclock/alarms/AlarmUpdateHandler.java",
+        "src/**/deskclock/alarms/ScrollHandler.java",
+        "src/**/deskclock/alarms/TimePickerDialogFragment.java",
+        "src/**/deskclock/actionbarmenu/*.java",
+        "src/**/deskclock/alarms/dataadapter/*.java",
         "src/**/deskclock/controller/*.java",
         "src/**/deskclock/events/*.java",
         "src/**/deskclock/provider/*.java",
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemController.kt b/src/com/android/deskclock/actionbarmenu/MenuItemController.kt
new file mode 100644
index 0000000..f1af2e0
--- /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.kt b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.kt
new file mode 100644
index 0000000..ff7e99a
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.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.actionbarmenu
+
+import android.app.Activity
+
+import kotlin.collections.ArrayList
+
+/**
+ * Factory that builds optional [MenuItemController] instances.
+ */
+// TODO(b/157255731) This class can be made an object once the Java code relying on it is Kotlin
+class MenuItemControllerFactory private constructor() {
+    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
+    }
+
+    companion object {
+        @JvmStatic
+        fun getInstance() = MenuItemControllerFactory()
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt
new file mode 100644
index 0000000..33d7878
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.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.actionbarmenu
+
+import android.app.Activity
+
+/**
+ * Provider for a [MenuItemController] instances.
+ */
+interface MenuItemProvider {
+    /**
+     * provides a [MenuItemController] that handles menu item.
+     */
+    fun provide(activity: Activity?): MenuItemController?
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt
new file mode 100644
index 0000000..7ddf11b
--- /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.kt b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.kt
new file mode 100644
index 0000000..079dea6
--- /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.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.kt b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.kt
new file mode 100644
index 0000000..d9c38d4
--- /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.kt b/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.kt
new file mode 100644
index 0000000..48e1364
--- /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.kt b/src/com/android/deskclock/alarms/AlarmActivity.kt
new file mode 100644
index 0000000..4a52a3c
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmActivity.kt
@@ -0,0 +1,652 @@
+/*
+ * 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.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 {
+    // TODO(b/157255731) Replace Handler with non-deprecated constructor call
+    private val mHandler: Handler = Handler()
+
+    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.getDataModel().alarmVolumeButtonBehavior
+
+        // TODO(b/157255731) Replace deprecated LayoutParams flags on Android versions above O
+        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)
+            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.getDataModel().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.kt b/src/com/android/deskclock/alarms/AlarmKlaxon.kt
new file mode 100644
index 0000000..cc3b6f9
--- /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.getDataModel().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/AlarmTimeClickHandler.kt b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt
new file mode 100644
index 0000000..975a895
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.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.provider.ClockContract.InstancesColumns
+import com.android.deskclock.ringtone.RingtonePickerActivity
+
+import java.util.Calendar
+
+/**
+ * Click handler for an alarm time item.
+ */
+// TODO(b/157255731) Replace deprecated Fragment related calls with AndroidX equivalent
+class AlarmTimeClickHandler(
+    private val mFragment: Fragment,
+    savedState: Bundle?,
+    private val mAlarmUpdateHandler: AlarmUpdateHandler,
+    private val mScrollHandler: ScrollHandler
+) {
+
+    private val mContext: Context = mFragment.getActivity().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.getDataModel().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.kt b/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt
new file mode 100644
index 0000000..5818add
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt
@@ -0,0 +1,207 @@
+/*
+ * 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/157255731) 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.kt b/src/com/android/deskclock/alarms/ScrollHandler.kt
new file mode 100644
index 0000000..ddcf5f4
--- /dev/null
+++ b/src/com/android/deskclock/alarms/ScrollHandler.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.alarms
+
+/**
+ * API that handles scrolling when an alarm item is expanded/collapsed.
+ */
+interface ScrollHandler {
+
+    /**
+     * Sets the stable id that view should be scrolled to. The view does not actually scroll yet.
+     */
+    fun setSmoothScrollStableId(stableId: Long)
+
+    /**
+     * Perform smooth scroll to position.
+     */
+    fun smoothScrollTo(position: Int)
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/TimePickerDialogFragment.kt b/src/com/android/deskclock/alarms/TimePickerDialogFragment.kt
new file mode 100644
index 0000000..f1daed3
--- /dev/null
+++ b/src/com/android/deskclock/alarms/TimePickerDialogFragment.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.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.os.Bundle
+import android.text.format.DateFormat
+import android.widget.TimePicker
+import androidx.appcompat.app.AlertDialog
+
+import com.android.deskclock.Utils
+
+import java.util.Calendar
+
+/**
+ * DialogFragment used to show TimePicker.
+ */
+// TODO(b/157255731) Replace deprecated Fragment related calls with AndroidX equivalent
+class TimePickerDialogFragment : DialogFragment() {
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val listener = getParentFragment() as OnTimeSetListener
+
+        val now = Calendar.getInstance()
+        val args: Bundle = if (getArguments() == null) Bundle.EMPTY else getArguments()
+        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 = getActivity()
+            TimePickerDialog(context, { _, hourOfDay, minuteOfHour ->
+                listener.onTimeSet(this@TimePickerDialogFragment, hourOfDay, minuteOfHour)
+            }, hour, minute, DateFormat.is24HourFormat(context))
+        } else {
+            val builder: AlertDialog.Builder = AlertDialog.Builder(getActivity())
+            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.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.kt b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.kt
new file mode 100644
index 0000000..2e19e6b
--- /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.kt b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.kt
new file mode 100644
index 0000000..33217b6
--- /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.getDataModel().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.kt b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.kt
new file mode 100644
index 0000000..8fa4ca4
--- /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.getDataModel().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.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
+        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.getDataModel().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.getDataModel().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