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