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