Merge "AOSP/DeskClock - Add Kotlin for AlarmAlertWakeLock, AlarmClockFragment"
diff --git a/Android.bp b/Android.bp
index 3c58462..ff51899 100644
--- a/Android.bp
+++ b/Android.bp
@@ -61,6 +61,8 @@
         "src/**/deskclock/widget/toast/*.java",
         "src/**/deskclock/widget/*.java",
         "src/**/deskclock/worldclock/*.java",
+        "src/**/deskclock/AlarmAlertWakeLock.java",
+        "src/**/deskclock/AlarmClockFragment.java",
     ],
     product_specific: true,
     static_libs: [
diff --git a/src/com/android/deskclock/AlarmAlertWakeLock.kt b/src/com/android/deskclock/AlarmAlertWakeLock.kt
new file mode 100644
index 0000000..ca97ed3
--- /dev/null
+++ b/src/com/android/deskclock/AlarmAlertWakeLock.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+
+/**
+ * Utility class to hold wake lock in app.
+ */
+object AlarmAlertWakeLock {
+    private const val TAG = "AlarmAlertWakeLock"
+
+    private var sCpuWakeLock: WakeLock? = null
+
+    @JvmStatic
+    fun createPartialWakeLock(context: Context): WakeLock {
+        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+        return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
+    }
+
+    @JvmStatic
+    fun acquireCpuWakeLock(context: Context) {
+        if (sCpuWakeLock != null) {
+            return
+        }
+
+        sCpuWakeLock = createPartialWakeLock(context)
+        sCpuWakeLock!!.acquire()
+    }
+
+    @JvmStatic
+    fun acquireScreenCpuWakeLock(context: Context) {
+        if (sCpuWakeLock != null) {
+            return
+        }
+        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+        sCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK
+                or PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.ON_AFTER_RELEASE, TAG)
+        sCpuWakeLock!!.acquire()
+    }
+
+    @JvmStatic
+    fun releaseCpuLock() {
+        if (sCpuWakeLock != null) {
+            sCpuWakeLock!!.release()
+            sCpuWakeLock = null
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmClockFragment.kt b/src/com/android/deskclock/AlarmClockFragment.kt
new file mode 100644
index 0000000..8e678a2
--- /dev/null
+++ b/src/com/android/deskclock/AlarmClockFragment.kt
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.LoaderManager.LoaderCallbacks
+import android.content.Context
+import android.content.Loader
+import android.database.Cursor
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.ItemAdapter.OnItemChangedListener
+import com.android.deskclock.alarms.AlarmTimeClickHandler
+import com.android.deskclock.alarms.AlarmUpdateHandler
+import com.android.deskclock.alarms.ScrollHandler
+import com.android.deskclock.alarms.TimePickerDialogFragment
+import com.android.deskclock.alarms.dataadapter.AlarmItemHolder
+import com.android.deskclock.alarms.dataadapter.CollapsedAlarmViewHolder
+import com.android.deskclock.alarms.dataadapter.ExpandedAlarmViewHolder
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.widget.EmptyViewController
+import com.android.deskclock.widget.toast.SnackbarManager
+import com.android.deskclock.widget.toast.ToastManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import kotlin.math.max
+
+/**
+ * A fragment that displays a list of alarm time and allows interaction with them.
+ */
+class AlarmClockFragment : DeskClockFragment(UiDataModel.Tab.ALARMS),
+        LoaderCallbacks<Cursor>, ScrollHandler, TimePickerDialogFragment.OnTimeSetListener {
+    // Updates "Today/Tomorrow" in the UI when midnight passes.
+    private val mMidnightUpdater: Runnable = MidnightRunnable()
+
+    // Views
+    private lateinit var mMainLayout: ViewGroup
+    private lateinit var mRecyclerView: RecyclerView
+
+    // Data
+    private var mCursorLoader: Loader<*>? = null
+    private var mScrollToAlarmId = Alarm.INVALID_ID
+    private var mExpandedAlarmId = Alarm.INVALID_ID
+    private var mCurrentUpdateToken: Long = 0
+
+    // Controllers
+    private lateinit var mItemAdapter: ItemAdapter<AlarmItemHolder>
+    private lateinit var mAlarmUpdateHandler: AlarmUpdateHandler
+    private lateinit var mEmptyViewController: EmptyViewController
+    private lateinit var mAlarmTimeClickHandler: AlarmTimeClickHandler
+    private lateinit var mLayoutManager: LinearLayoutManager
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+        mCursorLoader = loaderManager.initLoader(0, Bundle.EMPTY, this)
+        savedState?.let {
+            mExpandedAlarmId = it.getLong(KEY_EXPANDED_ID, Alarm.INVALID_ID)
+        }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedState: Bundle?
+    ): View? {
+        // Inflate the layout for this fragment
+        val v = inflater.inflate(R.layout.alarm_clock, container, false)
+        val context: Context = activity
+
+        mRecyclerView = v.findViewById<View>(R.id.alarms_recycler_view) as RecyclerView
+        mLayoutManager = object : LinearLayoutManager(context) {
+            override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
+                val extraSpace: Int = super.getExtraLayoutSpace(state)
+                return if (state.willRunPredictiveAnimations()) {
+                    max(getHeight(), extraSpace)
+                } else extraSpace
+            }
+        }
+        mRecyclerView.setLayoutManager(mLayoutManager)
+        mMainLayout = v.findViewById<View>(R.id.main) as ViewGroup
+        mAlarmUpdateHandler = AlarmUpdateHandler(context, this, mMainLayout)
+        val emptyView = v.findViewById<View>(R.id.alarms_empty_view) as TextView
+        val noAlarms: Drawable = Utils.getVectorDrawable(context, R.drawable.ic_noalarms)
+        emptyView.setCompoundDrawablesWithIntrinsicBounds(null, noAlarms, null, null)
+        mEmptyViewController = EmptyViewController(mMainLayout, mRecyclerView, emptyView)
+        mAlarmTimeClickHandler = AlarmTimeClickHandler(this, savedState, mAlarmUpdateHandler, this)
+
+        mItemAdapter = ItemAdapter()
+        mItemAdapter.setHasStableIds()
+        mItemAdapter.withViewTypes(CollapsedAlarmViewHolder.Factory(inflater),
+                null, CollapsedAlarmViewHolder.VIEW_TYPE)
+        mItemAdapter.withViewTypes(ExpandedAlarmViewHolder.Factory(context),
+                null, ExpandedAlarmViewHolder.VIEW_TYPE)
+        mItemAdapter.setOnItemChangedListener(object : OnItemChangedListener {
+            override fun onItemChanged(holder: ItemHolder<*>) {
+                if ((holder as AlarmItemHolder).isExpanded) {
+                    if (mExpandedAlarmId != holder.itemId) {
+                        // Collapse the prior expanded alarm.
+                        val aih = mItemAdapter.findItemById(mExpandedAlarmId)
+                        aih?.collapse()
+                        // Record the freshly expanded alarm.
+                        mExpandedAlarmId = holder.itemId
+                        val viewHolder: RecyclerView.ViewHolder? =
+                                mRecyclerView.findViewHolderForItemId(mExpandedAlarmId)
+                        viewHolder?.let {
+                            smoothScrollTo(viewHolder.getAdapterPosition())
+                        }
+                    }
+                } else if (mExpandedAlarmId == holder.itemId) {
+                    // The expanded alarm is now collapsed so update the tracking id.
+                    mExpandedAlarmId = Alarm.INVALID_ID
+                }
+            }
+
+            override fun onItemChanged(holder: ItemHolder<*>?, payload: Any) {
+                /* No additional work to do */
+            }
+        })
+        val scrollPositionWatcher = ScrollPositionWatcher()
+        mRecyclerView.addOnLayoutChangeListener(scrollPositionWatcher)
+        mRecyclerView.addOnScrollListener(scrollPositionWatcher)
+        mRecyclerView.setAdapter(mItemAdapter)
+        val itemAnimator = ItemAnimator()
+        itemAnimator.setChangeDuration(300L)
+        itemAnimator.setMoveDuration(300L)
+        mRecyclerView.setItemAnimator(itemAnimator)
+        return v
+    }
+
+    override fun onStart() {
+        super.onStart()
+
+        if (!isTabSelected) {
+            TimePickerDialogFragment.removeTimeEditDialog(fragmentManager)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+        // Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
+        // alarms when midnight passes.
+        UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
+
+        // Check if another app asked us to create a blank new alarm.
+        val intent = activity.intent ?: return
+
+        if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+            if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
+                // An external app asked us to create a blank alarm.
+                startCreatingAlarm()
+            }
+
+            // Remove the CREATE_NEW extra now that we've processed it.
+            intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA)
+        } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
+            UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+            val alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID)
+            if (alarmId != Alarm.INVALID_ID) {
+                setSmoothScrollStableId(alarmId)
+                if (mCursorLoader != null && mCursorLoader!!.isStarted) {
+                    // We need to force a reload here to make sure we have the latest view
+                    // of the data to scroll to.
+                    mCursorLoader!!.forceLoad()
+                }
+            }
+
+            // Remove the SCROLL_TO_ALARM extra now that we've processed it.
+            intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA)
+        }
+    }
+
+    override fun onPause() {
+        super.onPause()
+        UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
+
+        // When the user places the app in the background by pressing "home",
+        // dismiss the toast bar. However, since there is no way to determine if
+        // home was pressed, just dismiss any existing toast bar when restarting
+        // the app.
+        mAlarmUpdateHandler.hideUndoBar()
+    }
+
+    override fun smoothScrollTo(position: Int) {
+        mLayoutManager.scrollToPositionWithOffset(position, 0)
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        mAlarmTimeClickHandler.saveInstance(outState)
+        outState.putLong(KEY_EXPANDED_ID, mExpandedAlarmId)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        ToastManager.cancelToast()
+    }
+
+    fun setLabel(alarm: Alarm, label: String?) {
+        alarm.label = label
+        mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+    }
+
+    override fun onCreateLoader(id: Int, args: Bundle): Loader<Cursor> {
+        return Alarm.getAlarmsCursorLoader(activity)
+    }
+
+    override fun onLoadFinished(cursorLoader: Loader<Cursor>, data: Cursor) {
+        val itemHolders: MutableList<AlarmItemHolder> = ArrayList(data.count)
+        data.moveToFirst()
+        while (!data.isAfterLast) {
+            val alarm = Alarm(data)
+            val alarmInstance = if (alarm.canPreemptivelyDismiss()) {
+                AlarmInstance(data, joinedTable = true)
+            } else {
+                null
+            }
+            val itemHolder = AlarmItemHolder(alarm, alarmInstance, mAlarmTimeClickHandler)
+            itemHolders.add(itemHolder)
+            data.moveToNext()
+        }
+        setAdapterItems(itemHolders, SystemClock.elapsedRealtime())
+    }
+
+    /**
+     * Updates the adapters items, deferring the update until the current animation is finished or
+     * if no animation is running then the listener will be automatically be invoked immediately.
+     *
+     * @param items the new list of [AlarmItemHolder] to use
+     * @param updateToken a monotonically increasing value used to preserve ordering of deferred
+     * updates
+     */
+    private fun setAdapterItems(items: List<AlarmItemHolder>, updateToken: Long) {
+        if (updateToken < mCurrentUpdateToken) {
+            LogUtils.v("Ignoring adapter update: %d < %d", updateToken, mCurrentUpdateToken)
+            return
+        }
+
+        if (mRecyclerView.getItemAnimator()!!.isRunning()) {
+            // RecyclerView is currently animating -> defer update.
+            mRecyclerView.getItemAnimator()!!.isRunning(
+                    object : RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {
+                        override fun onAnimationsFinished() {
+                            setAdapterItems(items, updateToken)
+                        }
+                    })
+        } else if (mRecyclerView.isComputingLayout()) {
+            // RecyclerView is currently computing a layout -> defer update.
+            mRecyclerView.post(Runnable { setAdapterItems(items, updateToken) })
+        } else {
+            mCurrentUpdateToken = updateToken
+            mItemAdapter.setItems(items)
+
+            // Show or hide the empty view as appropriate.
+            val noAlarms = items.isEmpty()
+            mEmptyViewController.setEmpty(noAlarms)
+            if (noAlarms) {
+                // Ensure the drop shadow is hidden when no alarms exist.
+                setTabScrolledToTop(true)
+            }
+
+            // Expand the correct alarm.
+            if (mExpandedAlarmId != Alarm.INVALID_ID) {
+                val aih = mItemAdapter.findItemById(mExpandedAlarmId)
+                if (aih != null) {
+                    mAlarmTimeClickHandler.setSelectedAlarm(aih.item)
+                    aih.expand()
+                } else {
+                    mAlarmTimeClickHandler.setSelectedAlarm(null)
+                    mExpandedAlarmId = Alarm.INVALID_ID
+                }
+            }
+
+            // Scroll to the selected alarm.
+            if (mScrollToAlarmId != Alarm.INVALID_ID) {
+                scrollToAlarm(mScrollToAlarmId)
+                setSmoothScrollStableId(Alarm.INVALID_ID)
+            }
+        }
+    }
+
+    /**
+     * @param alarmId identifies the alarm to be displayed
+     */
+    private fun scrollToAlarm(alarmId: Long) {
+        val alarmCount = mItemAdapter.itemCount
+        var alarmPosition = -1
+        for (i in 0 until alarmCount) {
+            val id = mItemAdapter.getItemId(i)
+            if (id == alarmId) {
+                alarmPosition = i
+                break
+            }
+        }
+
+        if (alarmPosition >= 0) {
+            mItemAdapter.findItemById(alarmId).expand()
+            smoothScrollTo(alarmPosition)
+        } else {
+            // Trying to display a deleted alarm should only happen from a missed notification for
+            // an alarm that has been marked deleted after use.
+            SnackbarManager.show(Snackbar.make(mMainLayout, R.string.missed_alarm_has_been_deleted,
+                    Snackbar.LENGTH_LONG))
+        }
+    }
+
+    override fun onLoaderReset(cursorLoader: Loader<Cursor>) {
+    }
+
+    override fun setSmoothScrollStableId(stableId: Long) {
+        mScrollToAlarmId = stableId
+    }
+
+    override fun onFabClick(fab: ImageView) {
+        mAlarmUpdateHandler.hideUndoBar()
+        startCreatingAlarm()
+    }
+
+    override fun onUpdateFab(fab: ImageView) {
+        fab.visibility = View.VISIBLE
+        fab.setImageResource(R.drawable.ic_add_white_24dp)
+        fab.contentDescription = fab.resources.getString(R.string.button_alarms)
+    }
+
+    override fun onUpdateFabButtons(left: Button, right: Button) {
+        left.visibility = View.INVISIBLE
+        right.visibility = View.INVISIBLE
+    }
+
+    private fun startCreatingAlarm() {
+        // Clear the currently selected alarm.
+        mAlarmTimeClickHandler.setSelectedAlarm(null)
+        TimePickerDialogFragment.show(this)
+    }
+
+    override fun onTimeSet(fragment: TimePickerDialogFragment?, hourOfDay: Int, minute: Int) {
+        mAlarmTimeClickHandler.onTimeSet(hourOfDay, minute)
+    }
+
+    fun removeItem(itemHolder: AlarmItemHolder) {
+        mItemAdapter.removeItem(itemHolder)
+    }
+
+    /**
+     * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls
+     * the recyclerview or when the size/position of elements within the recyclerview changes.
+     */
+    private inner class ScrollPositionWatcher
+        : RecyclerView.OnScrollListener(), OnLayoutChangeListener {
+        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
+        }
+
+        override fun onLayoutChange(
+            v: View,
+            left: Int,
+            top: Int,
+            right: Int,
+            bottom: Int,
+            oldLeft: Int,
+            oldTop: Int,
+            oldRight: Int,
+            oldBottom: Int
+        ) {
+            setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
+        }
+    }
+
+    /**
+     * This runnable executes at midnight and refreshes the display of all alarms. Collapsed alarms
+     * that do no repeat will have their "Tomorrow" strings updated to say "Today".
+     */
+    private inner class MidnightRunnable : Runnable {
+        override fun run() {
+            mItemAdapter.notifyDataSetChanged()
+        }
+    }
+
+    companion object {
+        // This extra is used when receiving an intent to create an alarm, but no alarm details
+        // have been passed in, so the alarm page should start the process of creating a new alarm.
+        const val ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new"
+
+        // This extra is used when receiving an intent to scroll to specific alarm. If alarm
+        // can not be found, and toast message will pop up that the alarm has be deleted.
+        const val SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm"
+
+        private const val KEY_EXPANDED_ID = "expandedId"
+    }
+}
\ No newline at end of file