[automerger skipped] DeskClock: Fix alarm clock screen show black when screen locked; am: 4ba26ea8d8 -s ours am: d95f940685 -s ours am: bfbd31ea20 -s ours am: d137270e5c -s ours am: 8b1e08a0ef -s ours
am skip reason: Change-Id I69c44571cd86c21203311c89081c8935d5ed1450 with SHA-1 504dfe5e4b is in history
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/DeskClock/+/12162447
Change-Id: Ia1116cc6e6908667002c66bbf3842022d7b52806
diff --git a/Android.bp b/Android.bp
index ec6d1fe..f0a26b2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2,9 +2,11 @@
name: "DeskClock",
resource_dirs: ["res"],
sdk_version: "current",
- overrides: ["AlarmClock"],
+ overrides: [
+ "AlarmClock",
+ ],
srcs: [
- "src/**/*.java",
+ "src/**/*.kt",
"gen/**/*.java",
],
product_specific: true,
@@ -26,4 +28,6 @@
"androidx.gridlayout_gridlayout",
"androidx.recyclerview_recyclerview",
],
+
+ aaptflags: ["--legacy"],
}
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 487361e..c36c9b1 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -22,7 +22,7 @@
<original-package android:name="com.android.alarmclock" />
<original-package android:name="com.android.deskclock" />
- <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="25" />
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="29" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@@ -30,6 +30,7 @@
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- WRITE_SETTINGS is required to record the upcoming alarm prior to L -->
<uses-permission
@@ -60,6 +61,7 @@
android:name=".DeskClock"
android:label="@string/app_label"
android:launchMode="singleTask"
+ android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -103,10 +105,14 @@
<!-- ============================================================== -->
<activity
- android:name=".HandleApiCalls"
+ android:name="com.android.deskclock.HandleApiCalls"
+ android:permission="com.android.alarm.permission.SET_ALARM"
+ android:directBootAware="true"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
+ android:showWhenLocked="true"
android:taskAffinity=""
+ android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.DISMISS_ALARM" />
@@ -121,9 +127,10 @@
</activity>
<activity-alias
- android:name=".HandleSetAlarmApiCalls"
+ android:name="com.android.deskclock.HandleSetAlarmApiCalls"
android:permission="com.android.alarm.permission.SET_ALARM"
- android:targetActivity=".HandleApiCalls">
+ android:exported="true"
+ android:targetActivity="com.android.deskclock.HandleApiCalls">
<intent-filter>
<action android:name="android.intent.action.SET_ALARM" />
<action android:name="android.intent.action.SET_TIMER" />
@@ -143,6 +150,7 @@
android:excludeFromRecents="true"
android:resizeableActivity="false"
android:showOnLockScreen="true"
+ android:showWhenLocked="true"
android:taskAffinity=""
android:windowSoftInputMode="stateAlwaysHidden" />
@@ -159,6 +167,7 @@
<receiver
android:name=".AlarmInitReceiver"
+ android:exported="true"
android:directBootAware="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
@@ -236,6 +245,7 @@
<service
android:name=".Screensaver"
android:label="@string/app_label"
+ android:exported="true"
android:permission="android.permission.BIND_DREAM_SERVICE">
<intent-filter>
<action android:name="android.service.dreams.DreamService" />
@@ -254,6 +264,7 @@
<receiver
android:name="com.android.alarmclock.AnalogAppWidgetProvider"
+ android:exported="true"
android:label="@string/analog_gadget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -269,6 +280,7 @@
<receiver
android:name="com.android.alarmclock.DigitalAppWidgetProvider"
+ android:exported="true"
android:label="@string/digital_gadget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
deleted file mode 100644
index e69de29..0000000
--- a/MODULE_LICENSE_APACHE2
+++ /dev/null
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 443d182..c966686 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1069,4 +1069,12 @@
-->
<string name="alarm_is_snoozed"><xliff:g id="alarm_time" example="14:20">%s</xliff:g> alarm snoozed for 10 minutes</string>
+ <!-- Strings for notification channel. -->
+ <string name="firing_alarms_timers_channel">Firing alarms & timers</string>
+ <string name="alarm_missed_channel">Missed alarms</string>
+ <string name="alarm_snooze_channel">Snoozed alarms</string>
+ <string name="alarm_upcoming_channel">Upcoming alarms</string>
+ <string name="stopwatch_channel">Stopwatch</string>
+ <string name="timer_channel">Timer</string>
+
</resources>
diff --git a/src/com/android/alarmclock/AnalogAppWidgetProvider.java b/src/com/android/alarmclock/AnalogAppWidgetProvider.java
deleted file mode 100644
index 80bc22f..0000000
--- a/src/com/android/alarmclock/AnalogAppWidgetProvider.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.alarmclock;
-
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.widget.RemoteViews;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-/**
- * Simple widget to show an analog clock.
- */
-public class AnalogAppWidgetProvider extends AppWidgetProvider {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- super.onReceive(context, intent);
-
- final AppWidgetManager wm = AppWidgetManager.getInstance(context);
- if (wm == null) {
- return;
- }
-
- // Send events for newly created/deleted widgets.
- final ComponentName provider = new ComponentName(context, getClass());
- final int widgetCount = wm.getAppWidgetIds(provider).length;
-
- final DataModel dm = DataModel.getDataModel();
- dm.updateWidgetCount(getClass(), widgetCount, R.string.category_analog_widget);
- }
-
- /**
- * Called when widgets must provide remote views.
- */
- @Override
- public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
- super.onUpdate(context, wm, widgetIds);
-
- for (int widgetId : widgetIds) {
- final String packageName = context.getPackageName();
- final RemoteViews widget = new RemoteViews(packageName, R.layout.analog_appwidget);
-
- // Tapping on the widget opens the app (if not on the lock screen).
- if (Utils.isWidgetClickable(wm, widgetId)) {
- final Intent openApp = new Intent(context, DeskClock.class);
- final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
- widget.setOnClickPendingIntent(R.id.analog_appwidget, pi);
- }
-
- wm.updateAppWidget(widgetId, widget);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/AnalogAppWidgetProvider.kt b/src/com/android/alarmclock/AnalogAppWidgetProvider.kt
new file mode 100644
index 0000000..743e441
--- /dev/null
+++ b/src/com/android/alarmclock/AnalogAppWidgetProvider.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.alarmclock
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.widget.RemoteViews
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+/**
+ * Simple widget to show an analog clock.
+ */
+class AnalogAppWidgetProvider : AppWidgetProvider() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ super.onReceive(context, intent)
+ val wm: AppWidgetManager = AppWidgetManager.getInstance(context) ?: return
+
+ // Send events for newly created/deleted widgets.
+ val provider = ComponentName(context, javaClass)
+ val widgetCount: Int = wm.getAppWidgetIds(provider).size
+ val dm = DataModel.dataModel
+ dm.updateWidgetCount(javaClass, widgetCount, R.string.category_analog_widget)
+ }
+
+ /**
+ * Called when widgets must provide remote views.
+ */
+ override fun onUpdate(context: Context, wm: AppWidgetManager, widgetIds: IntArray) {
+ super.onUpdate(context, wm, widgetIds)
+ widgetIds.forEach { widgetId ->
+ val packageName: String = context.getPackageName()
+ val widget = RemoteViews(packageName, R.layout.analog_appwidget)
+
+ // Tapping on the widget opens the app (if not on the lock screen).
+ if (Utils.isWidgetClickable(wm, widgetId)) {
+ val openApp = Intent(context, DeskClock::class.java)
+ val pi: PendingIntent = PendingIntent.getActivity(context, 0, openApp, 0)
+ widget.setOnClickPendingIntent(R.id.analog_appwidget, pi)
+ }
+ wm.updateAppWidget(widgetId, widget)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/DigitalAppWidgetCityService.java b/src/com/android/alarmclock/DigitalAppWidgetCityService.java
deleted file mode 100644
index 6392697..0000000
--- a/src/com/android/alarmclock/DigitalAppWidgetCityService.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.alarmclock;
-
-import android.content.Intent;
-import android.widget.RemoteViewsService;
-
-public class DigitalAppWidgetCityService extends RemoteViewsService {
-
- @Override
- public RemoteViewsFactory onGetViewFactory(Intent i) {
- return new DigitalAppWidgetCityViewsFactory(getApplicationContext(), i);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java b/src/com/android/alarmclock/DigitalAppWidgetCityService.kt
similarity index 60%
copy from src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
copy to src/com/android/alarmclock/DigitalAppWidgetCityService.kt
index c3e460d..36521b2 100644
--- a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
+++ b/src/com/android/alarmclock/DigitalAppWidgetCityService.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,18 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.alarmclock
-package com.android.deskclock.actionbarmenu;
+import android.content.Intent
+import android.widget.RemoteViewsService
-import android.app.Activity;
-
-/**
- * Provider for a {@link MenuItemController} instances.
- */
-public interface MenuItemProvider {
-
- /**
- * provides a {@link MenuItemController} that handles menu item.
- */
- MenuItemController provide(Activity activity);
-}
+class DigitalAppWidgetCityService : RemoteViewsService() {
+ override fun onGetViewFactory(i: Intent): RemoteViewsFactory {
+ return DigitalAppWidgetCityViewsFactory(getApplicationContext(), i)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java
deleted file mode 100644
index 849e8cf..0000000
--- a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.alarmclock;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.text.format.DateFormat;
-import android.util.TypedValue;
-import android.view.View;
-import android.widget.RemoteViews;
-import android.widget.RemoteViewsService.RemoteViewsFactory;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.City;
-import com.android.deskclock.data.DataModel;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.TimeZone;
-
-import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;
-import static android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID;
-import static java.util.Calendar.DAY_OF_WEEK;
-
-/**
- * This factory produces entries in the world cities list view displayed at the bottom of the
- * digital widget. Each row is comprised of two world cities located side-by-side.
- */
-public class DigitalAppWidgetCityViewsFactory implements RemoteViewsFactory {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigWidgetViewsFactory");
-
- private final Intent mFillInIntent = new Intent();
-
- private final Context mContext;
- private final float m12HourFontSize;
- private final float m24HourFontSize;
- private final int mWidgetId;
- private float mFontScale = 1;
-
- private City mHomeCity;
- private boolean mShowHomeClock;
- private List<City> mCities = Collections.emptyList();
-
- public DigitalAppWidgetCityViewsFactory(Context context, Intent intent) {
- mContext = context;
- mWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
-
- final Resources res = context.getResources();
- m12HourFontSize = res.getDimension(R.dimen.digital_widget_city_12_medium_font_size);
- m24HourFontSize = res.getDimension(R.dimen.digital_widget_city_24_medium_font_size);
- }
-
- @Override
- public void onCreate() {
- LOGGER.i("DigitalAppWidgetCityViewsFactory onCreate " + mWidgetId);
- }
-
- @Override
- public void onDestroy() {
- LOGGER.i("DigitalAppWidgetCityViewsFactory onDestroy " + mWidgetId);
- }
-
- /**
- * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
- * mShowHomeClock.</p>
- *
- * {@inheritDoc}
- */
- @Override
- public synchronized int getCount() {
- final int homeClockCount = mShowHomeClock ? 1 : 0;
- final int worldClockCount = mCities.size();
- final double totalClockCount = homeClockCount + worldClockCount;
-
- // number of clocks / 2 clocks per row
- return (int) Math.ceil(totalClockCount / 2);
- }
-
- /**
- * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
- * mShowHomeClock.</p>
- *
- * {@inheritDoc}
- */
- @Override
- public synchronized RemoteViews getViewAt(int position) {
- final int homeClockOffset = mShowHomeClock ? -1 : 0;
- final int leftIndex = position * 2 + homeClockOffset;
- final int rightIndex = leftIndex + 1;
-
- final City left = leftIndex == -1 ? mHomeCity :
- (leftIndex < mCities.size() ? mCities.get(leftIndex) : null);
- final City right = rightIndex < mCities.size() ? mCities.get(rightIndex) : null;
-
- final RemoteViews rv =
- new RemoteViews(mContext.getPackageName(), R.layout.world_clock_remote_list_item);
-
- // Show the left clock if one exists.
- if (left != null) {
- update(rv, left, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
- } else {
- hide(rv, R.id.left_clock, R.id.city_name_left, R.id.city_day_left);
- }
-
- // Show the right clock if one exists.
- if (right != null) {
- update(rv, right, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
- } else {
- hide(rv, R.id.right_clock, R.id.city_name_right, R.id.city_day_right);
- }
-
- // Hide last spacer in last row; show for all others.
- final boolean lastRow = position == getCount() - 1;
- rv.setViewVisibility(R.id.city_spacer, lastRow ? View.GONE : View.VISIBLE);
-
- rv.setOnClickFillInIntent(R.id.widget_item, mFillInIntent);
- return rv;
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public RemoteViews getLoadingView() {
- return null;
- }
-
- @Override
- public int getViewTypeCount() {
- return 1;
- }
-
- @Override
- public boolean hasStableIds() {
- return false;
- }
-
- /**
- * <p>Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
- * mShowHomeClock.</p>
- *
- * {@inheritDoc}
- */
- @Override
- public synchronized void onDataSetChanged() {
- // Fetch the data on the main Looper.
- final RefreshRunnable refreshRunnable = new RefreshRunnable();
- DataModel.getDataModel().run(refreshRunnable);
-
- // Store the data in local variables.
- mHomeCity = refreshRunnable.mHomeCity;
- mCities = refreshRunnable.mCities;
- mShowHomeClock = refreshRunnable.mShowHomeClock;
- mFontScale = WidgetUtils.getScaleRatio(mContext, null, mWidgetId, mCities.size());
- }
-
- private void update(RemoteViews rv, City city, int clockId, int labelId, int dayId) {
- rv.setCharSequence(clockId, "setFormat12Hour", Utils.get12ModeFormat(0.4f, false));
- rv.setCharSequence(clockId, "setFormat24Hour", Utils.get24ModeFormat(false));
-
- final boolean is24HourFormat = DateFormat.is24HourFormat(mContext);
- final float fontSize = is24HourFormat ? m24HourFontSize : m12HourFontSize;
- rv.setTextViewTextSize(clockId, TypedValue.COMPLEX_UNIT_PX, fontSize * mFontScale);
- rv.setString(clockId, "setTimeZone", city.getTimeZone().getID());
- rv.setTextViewText(labelId, city.getName());
-
- // Compute if the city week day matches the weekday of the current timezone.
- final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
- final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
- final boolean displayDayOfWeek = localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
-
- // Bind the week day display.
- if (displayDayOfWeek) {
- final Locale locale = Locale.getDefault();
- final String weekday = cityCal.getDisplayName(DAY_OF_WEEK, Calendar.SHORT, locale);
- final String slashDay = mContext.getString(R.string.world_day_of_week_label, weekday);
- rv.setTextViewText(dayId, slashDay);
- }
-
- rv.setViewVisibility(dayId, displayDayOfWeek ? View.VISIBLE : View.GONE);
- rv.setViewVisibility(clockId, View.VISIBLE);
- rv.setViewVisibility(labelId, View.VISIBLE);
- }
-
- private void hide(RemoteViews clock, int clockId, int labelId, int dayId) {
- clock.setViewVisibility(dayId, View.INVISIBLE);
- clock.setViewVisibility(clockId, View.INVISIBLE);
- clock.setViewVisibility(labelId, View.INVISIBLE);
- }
-
- /**
- * This Runnable fetches data for this factory on the main thread to ensure all DataModel reads
- * occur on the main thread.
- */
- private static final class RefreshRunnable implements Runnable {
-
- private City mHomeCity;
- private List<City> mCities;
- private boolean mShowHomeClock;
-
- @Override
- public void run() {
- mHomeCity = DataModel.getDataModel().getHomeCity();
- mCities = new ArrayList<>(DataModel.getDataModel().getSelectedCities());
- mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
- }
- }
-}
diff --git a/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.kt b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.kt
new file mode 100644
index 0000000..a4e55ad
--- /dev/null
+++ b/src/com/android/alarmclock/DigitalAppWidgetCityViewsFactory.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.alarmclock
+
+import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
+import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.text.format.DateFormat
+import android.util.TypedValue
+import android.view.View
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService.RemoteViewsFactory
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.City
+import com.android.deskclock.data.DataModel
+
+import java.util.ArrayList
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * This factory produces entries in the world cities list view displayed at the bottom of the
+ * digital widget. Each row is comprised of two world cities located side-by-side.
+ */
+class DigitalAppWidgetCityViewsFactory(context: Context, intent: Intent) : RemoteViewsFactory {
+ private val mFillInIntent: Intent = Intent()
+ private val mContext: Context = context
+ private val m12HourFontSize: Float
+ private val m24HourFontSize: Float
+ private val mWidgetId: Int = intent.getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID)
+ private var mFontScale = 1f
+ private var mHomeCity: City? = null
+ private var mShowHomeClock = false
+ private var mCities: List<City>? = emptyList()
+
+ init {
+ val res: Resources = context.getResources()
+ m12HourFontSize = res.getDimension(R.dimen.digital_widget_city_12_medium_font_size)
+ m24HourFontSize = res.getDimension(R.dimen.digital_widget_city_24_medium_font_size)
+ }
+
+ override fun onCreate() {
+ LOGGER.i("DigitalAppWidgetCityViewsFactory onCreate $mWidgetId")
+ }
+
+ override fun onDestroy() {
+ LOGGER.i("DigitalAppWidgetCityViewsFactory onDestroy $mWidgetId")
+ }
+
+ /**
+ * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
+ * mShowHomeClock.
+ *
+ * {@inheritDoc}
+ */
+ @Synchronized
+ override fun getCount(): Int {
+ val homeClockCount = if (mShowHomeClock) 1 else 0
+ val worldClockCount = mCities!!.size
+ val totalClockCount = homeClockCount + worldClockCount.toDouble()
+
+ // Number of clocks / 2 clocks per row
+ return Math.ceil(totalClockCount / 2).toInt()
+ }
+
+ /**
+ * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
+ * mShowHomeClock.
+ *
+ * {@inheritDoc}
+ */
+ @Synchronized
+ override fun getViewAt(position: Int): RemoteViews {
+ val homeClockOffset = if (mShowHomeClock) -1 else 0
+ val leftIndex = position * 2 + homeClockOffset
+ val rightIndex = leftIndex + 1
+ val left = when {
+ leftIndex == -1 -> mHomeCity
+ leftIndex < mCities!!.size -> mCities!![leftIndex]
+ else -> null
+ }
+ val right = if (rightIndex < mCities!!.size) mCities!![rightIndex] else null
+ val rv = RemoteViews(mContext.getPackageName(), R.layout.world_clock_remote_list_item)
+
+ // Show the left clock if one exists.
+ if (left != null) {
+ update(rv, left, R.id.left_clock, R.id.city_name_left, R.id.city_day_left)
+ } else {
+ hide(rv, R.id.left_clock, R.id.city_name_left, R.id.city_day_left)
+ }
+
+ // Show the right clock if one exists.
+ if (right != null) {
+ update(rv, right, R.id.right_clock, R.id.city_name_right, R.id.city_day_right)
+ } else {
+ hide(rv, R.id.right_clock, R.id.city_name_right, R.id.city_day_right)
+ }
+
+ // Hide last spacer in last row; show for all others.
+ val lastRow = position == count - 1
+ rv.setViewVisibility(R.id.city_spacer, if (lastRow) View.GONE else View.VISIBLE)
+ rv.setOnClickFillInIntent(R.id.widget_item, mFillInIntent)
+ return rv
+ }
+
+ override fun getItemId(position: Int): Long {
+ return position.toLong()
+ }
+
+ override fun getLoadingView(): RemoteViews? {
+ return null
+ }
+
+ override fun getViewTypeCount(): Int {
+ return 1
+ }
+
+ override fun hasStableIds(): Boolean {
+ return false
+ }
+
+ /**
+ * Synchronized to ensure single-threaded reading/writing of mCities, mHomeCity and
+ * mShowHomeClock.
+ *
+ * {@inheritDoc}
+ */
+ @Synchronized
+ override fun onDataSetChanged() {
+ // Fetch the data on the main Looper.
+ val refreshRunnable = RefreshRunnable()
+ DataModel.dataModel.run(refreshRunnable)
+
+ // Store the data in local variables.
+ mHomeCity = refreshRunnable.mHomeCity
+ mCities = refreshRunnable.mCities
+ mShowHomeClock = refreshRunnable.mShowHomeClock
+ mFontScale = WidgetUtils.getScaleRatio(mContext, null, mWidgetId, mCities!!.size)
+ }
+
+ private fun update(rv: RemoteViews, city: City, clockId: Int, labelId: Int, dayId: Int) {
+ rv.setCharSequence(clockId, "setFormat12Hour", Utils.get12ModeFormat(0.4f, false))
+ rv.setCharSequence(clockId, "setFormat24Hour", Utils.get24ModeFormat(false))
+
+ val is24HourFormat: Boolean = DateFormat.is24HourFormat(mContext)
+ val fontSize = if (is24HourFormat) m24HourFontSize else m12HourFontSize
+ rv.setTextViewTextSize(clockId, TypedValue.COMPLEX_UNIT_PX, fontSize * mFontScale)
+ rv.setString(clockId, "setTimeZone", city.timeZone.id)
+ rv.setTextViewText(labelId, city.name)
+
+ // Compute if the city week day matches the weekday of the current timezone.
+ val localCal = Calendar.getInstance(TimeZone.getDefault())
+ val cityCal = Calendar.getInstance(city.timeZone)
+ val displayDayOfWeek = localCal[Calendar.DAY_OF_WEEK] != cityCal[Calendar.DAY_OF_WEEK]
+
+ // Bind the week day display.
+ if (displayDayOfWeek) {
+ val locale = Locale.getDefault()
+ val weekday = cityCal.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, locale)
+ val slashDay: String = mContext.getString(R.string.world_day_of_week_label, weekday)
+ rv.setTextViewText(dayId, slashDay)
+ }
+
+ rv.setViewVisibility(dayId, if (displayDayOfWeek) View.VISIBLE else View.GONE)
+ rv.setViewVisibility(clockId, View.VISIBLE)
+ rv.setViewVisibility(labelId, View.VISIBLE)
+ }
+
+ private fun hide(clock: RemoteViews, clockId: Int, labelId: Int, dayId: Int) {
+ clock.setViewVisibility(dayId, View.INVISIBLE)
+ clock.setViewVisibility(clockId, View.INVISIBLE)
+ clock.setViewVisibility(labelId, View.INVISIBLE)
+ }
+
+ /**
+ * This Runnable fetches data for this factory on the main thread to ensure all DataModel reads
+ * occur on the main thread.
+ */
+ private class RefreshRunnable : Runnable {
+ var mHomeCity: City? = null
+ var mCities: List<City>? = null
+ var mShowHomeClock = false
+
+ override fun run() {
+ mHomeCity = DataModel.dataModel.homeCity
+ mCities = ArrayList(DataModel.dataModel.selectedCities)
+ mShowHomeClock = DataModel.dataModel.showHomeClock
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("DigWidgetViewsFactory")
+ }
+}
diff --git a/src/com/android/alarmclock/DigitalAppWidgetProvider.java b/src/com/android/alarmclock/DigitalAppWidgetProvider.java
deleted file mode 100644
index 3be07ec..0000000
--- a/src/com/android/alarmclock/DigitalAppWidgetProvider.java
+++ /dev/null
@@ -1,543 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.alarmclock;
-
-import android.annotation.SuppressLint;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.util.ArraySet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.RemoteViews;
-import android.widget.TextClock;
-import android.widget.TextView;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.City;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.worldclock.CitySelectionActivity;
-
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.TimeZone;
-
-import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
-import static android.app.PendingIntent.FLAG_NO_CREATE;
-import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
-import static android.content.Intent.ACTION_DATE_CHANGED;
-import static android.content.Intent.ACTION_LOCALE_CHANGED;
-import static android.content.Intent.ACTION_SCREEN_ON;
-import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
-import static android.content.Intent.ACTION_TIME_CHANGED;
-import static android.util.TypedValue.COMPLEX_UNIT_PX;
-import static android.view.View.GONE;
-import static android.view.View.MeasureSpec.UNSPECIFIED;
-import static android.view.View.VISIBLE;
-import static com.android.deskclock.alarms.AlarmStateManager.ACTION_ALARM_CHANGED;
-import static com.android.deskclock.data.DataModel.ACTION_WORLD_CITIES_CHANGED;
-import static java.lang.Math.max;
-import static java.lang.Math.round;
-
-/**
- * <p>This provider produces a widget resembling one of the formats below.</p>
- *
- * If an alarm is scheduled to ring in the future:
- * <pre>
- * 12:59 AM
- * WED, FEB 3 ⏰ THU 9:30 AM
- * </pre>
- *
- * If no alarm is scheduled to ring in the future:
- * <pre>
- * 12:59 AM
- * WED, FEB 3
- * </pre>
- *
- * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
- * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
- * choose optimal values.
- */
-public class DigitalAppWidgetProvider extends AppWidgetProvider {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DigitalWidgetProvider");
-
- /**
- * Intent action used for refreshing a world city display when any of them changes days or when
- * the default TimeZone changes days. This affects the widget display because the day-of-week is
- * only visible when the world city day-of-week differs from the default TimeZone's day-of-week.
- */
- private static final String ACTION_ON_DAY_CHANGE = "com.android.deskclock.ON_DAY_CHANGE";
-
- /** Intent used to deliver the {@link #ACTION_ON_DAY_CHANGE} callback. */
- private static final Intent DAY_CHANGE_INTENT = new Intent(ACTION_ON_DAY_CHANGE);
-
- @Override
- public void onEnabled(Context context) {
- super.onEnabled(context);
-
- // Schedule the day-change callback if necessary.
- updateDayChangeCallback(context);
- }
-
- @Override
- public void onDisabled(Context context) {
- super.onDisabled(context);
-
- // Remove any scheduled day-change callback.
- removeDayChangeCallback(context);
- }
-
- @Override
- public void onReceive(@NonNull Context context, @NonNull Intent intent) {
- LOGGER.i("onReceive: " + intent);
- super.onReceive(context, intent);
-
- final AppWidgetManager wm = AppWidgetManager.getInstance(context);
- if (wm == null) {
- return;
- }
-
- final ComponentName provider = new ComponentName(context, getClass());
- final int[] widgetIds = wm.getAppWidgetIds(provider);
-
- final String action = intent.getAction();
- switch (action) {
- case ACTION_NEXT_ALARM_CLOCK_CHANGED:
- case ACTION_DATE_CHANGED:
- case ACTION_LOCALE_CHANGED:
- case ACTION_SCREEN_ON:
- case ACTION_TIME_CHANGED:
- case ACTION_TIMEZONE_CHANGED:
- case ACTION_ALARM_CHANGED:
- case ACTION_ON_DAY_CHANGE:
- case ACTION_WORLD_CITIES_CHANGED:
- for (int widgetId : widgetIds) {
- relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
- }
- }
-
- final DataModel dm = DataModel.getDataModel();
- dm.updateWidgetCount(getClass(), widgetIds.length, R.string.category_digital_widget);
-
- if (widgetIds.length > 0) {
- updateDayChangeCallback(context);
- }
- }
-
- /**
- * Called when widgets must provide remote views.
- */
- @Override
- public void onUpdate(Context context, AppWidgetManager wm, int[] widgetIds) {
- super.onUpdate(context, wm, widgetIds);
-
- for (int widgetId : widgetIds) {
- relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId));
- }
- }
-
- /**
- * Called when the app widget changes sizes.
- */
- @Override
- public void onAppWidgetOptionsChanged(Context context, AppWidgetManager wm, int widgetId,
- Bundle options) {
- super.onAppWidgetOptionsChanged(context, wm, widgetId, options);
-
- // scale the fonts of the clock to fit inside the new size
- relayoutWidget(context, AppWidgetManager.getInstance(context), widgetId, options);
- }
-
- /**
- * Compute optimal font and icon sizes offscreen for both portrait and landscape orientations
- * using the last known widget size and apply them to the widget.
- */
- private static void relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
- Bundle options) {
- final RemoteViews portrait = relayoutWidget(context, wm, widgetId, options, true);
- final RemoteViews landscape = relayoutWidget(context, wm, widgetId, options, false);
- final RemoteViews widget = new RemoteViews(landscape, portrait);
- wm.updateAppWidget(widgetId, widget);
- wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list);
- }
-
- /**
- * Compute optimal font and icon sizes offscreen for the given orientation.
- */
- private static RemoteViews relayoutWidget(Context context, AppWidgetManager wm, int widgetId,
- Bundle options, boolean portrait) {
- // Create a remote view for the digital clock.
- final String packageName = context.getPackageName();
- final RemoteViews rv = new RemoteViews(packageName, R.layout.digital_widget);
-
- // Tapping on the widget opens the app (if not on the lock screen).
- if (Utils.isWidgetClickable(wm, widgetId)) {
- final Intent openApp = new Intent(context, DeskClock.class);
- final PendingIntent pi = PendingIntent.getActivity(context, 0, openApp, 0);
- rv.setOnClickPendingIntent(R.id.digital_widget, pi);
- }
-
- // Configure child views of the remote view.
- final CharSequence dateFormat = getDateFormat(context);
- rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat);
- rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat);
-
- final String nextAlarmTime = Utils.getNextAlarm(context);
- if (TextUtils.isEmpty(nextAlarmTime)) {
- rv.setViewVisibility(R.id.nextAlarm, GONE);
- rv.setViewVisibility(R.id.nextAlarmIcon, GONE);
- } else {
- rv.setTextViewText(R.id.nextAlarm, nextAlarmTime);
- rv.setViewVisibility(R.id.nextAlarm, VISIBLE);
- rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE);
- }
-
- if (options == null) {
- options = wm.getAppWidgetOptions(widgetId);
- }
-
- // Fetch the widget size selected by the user.
- final Resources resources = context.getResources();
- final float density = resources.getDisplayMetrics().density;
- final int minWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH));
- final int minHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT));
- final int maxWidthPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH));
- final int maxHeightPx = (int) (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT));
- final int targetWidthPx = portrait ? minWidthPx : maxWidthPx;
- final int targetHeightPx = portrait ? maxHeightPx : minHeightPx;
- final int largestClockFontSizePx =
- resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size);
-
- // Create a size template that describes the widget bounds.
- final Sizes template = new Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx);
-
- // Compute optimal font sizes and icon sizes to fit within the widget bounds.
- final Sizes sizes = optimizeSizes(context, template, nextAlarmTime);
- if (LOGGER.isVerboseLoggable()) {
- LOGGER.v(sizes.toString());
- }
-
- // Apply the computed sizes to the remote views.
- rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap);
- rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx);
- rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx);
- rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx);
-
- final int smallestWorldCityListSizePx =
- resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size);
- if (sizes.getListHeight() <= smallestWorldCityListSizePx) {
- // Insufficient space; hide the world city list.
- rv.setViewVisibility(R.id.world_city_list, GONE);
- } else {
- // Set an adapter on the world city list. That adapter connects to a Service via intent.
- final Intent intent = new Intent(context, DigitalAppWidgetCityService.class);
- intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);
- intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
- rv.setRemoteAdapter(R.id.world_city_list, intent);
- rv.setViewVisibility(R.id.world_city_list, VISIBLE);
-
- // Tapping on the widget opens the city selection activity (if not on the lock screen).
- if (Utils.isWidgetClickable(wm, widgetId)) {
- final Intent selectCity = new Intent(context, CitySelectionActivity.class);
- final PendingIntent pi = PendingIntent.getActivity(context, 0, selectCity, 0);
- rv.setPendingIntentTemplate(R.id.world_city_list, pi);
- }
- }
-
- return rv;
- }
-
- /**
- * Inflate an offscreen copy of the widget views. Binary search through the range of sizes until
- * the optimal sizes that fit within the widget bounds are located.
- */
- private static Sizes optimizeSizes(Context context, Sizes template, String nextAlarmTime) {
- // Inflate a test layout to compute sizes at different font sizes.
- final LayoutInflater inflater = LayoutInflater.from(context);
- @SuppressLint("InflateParams")
- final View sizer = inflater.inflate(R.layout.digital_widget_sizer, null /* root */);
-
- // Configure the date to display the current date string.
- final CharSequence dateFormat = getDateFormat(context);
- final TextClock date = (TextClock) sizer.findViewById(R.id.date);
- date.setFormat12Hour(dateFormat);
- date.setFormat24Hour(dateFormat);
-
- // Configure the next alarm views to display the next alarm time or be gone.
- final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
- final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
- if (TextUtils.isEmpty(nextAlarmTime)) {
- nextAlarm.setVisibility(GONE);
- nextAlarmIcon.setVisibility(GONE);
- } else {
- nextAlarm.setText(nextAlarmTime);
- nextAlarm.setVisibility(VISIBLE);
- nextAlarmIcon.setVisibility(VISIBLE);
- nextAlarmIcon.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
- }
-
- // Measure the widget at the largest possible size.
- Sizes high = measure(template, template.getLargestClockFontSizePx(), sizer);
- if (!high.hasViolations()) {
- return high;
- }
-
- // Measure the widget at the smallest possible size.
- Sizes low = measure(template, template.getSmallestClockFontSizePx(), sizer);
- if (low.hasViolations()) {
- return low;
- }
-
- // Binary search between the smallest and largest sizes until an optimum size is found.
- while (low.getClockFontSizePx() != high.getClockFontSizePx()) {
- final int midFontSize = (low.getClockFontSizePx() + high.getClockFontSizePx()) / 2;
- if (midFontSize == low.getClockFontSizePx()) {
- return low;
- }
-
- final Sizes midSize = measure(template, midFontSize, sizer);
- if (midSize.hasViolations()) {
- high = midSize;
- } else {
- low = midSize;
- }
- }
-
- return low;
- }
-
- /**
- * Remove the existing day-change callback if it is not needed (no selected cities exist).
- * Add the day-change callback if it is needed (selected cities exist).
- */
- private void updateDayChangeCallback(Context context) {
- final DataModel dm = DataModel.getDataModel();
- final List<City> selectedCities = dm.getSelectedCities();
- final boolean showHomeClock = dm.getShowHomeClock();
- if (selectedCities.isEmpty() && !showHomeClock) {
- // Remove the existing day-change callback.
- removeDayChangeCallback(context);
- return;
- }
-
- // Look up the time at which the next day change occurs across all timezones.
- final Set<TimeZone> zones = new ArraySet<>(selectedCities.size() + 2);
- zones.add(TimeZone.getDefault());
- if (showHomeClock) {
- zones.add(dm.getHomeCity().getTimeZone());
- }
- for (City city : selectedCities) {
- zones.add(city.getTimeZone());
- }
- final Date nextDay = Utils.getNextDay(new Date(), zones);
-
- // Schedule the next day-change callback; at least one city is displayed.
- final PendingIntent pi =
- PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_UPDATE_CURRENT);
- getAlarmManager(context).setExact(AlarmManager.RTC, nextDay.getTime(), pi);
- }
-
- /**
- * Remove the existing day-change callback.
- */
- private void removeDayChangeCallback(Context context) {
- final PendingIntent pi =
- PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_NO_CREATE);
- if (pi != null) {
- getAlarmManager(context).cancel(pi);
- pi.cancel();
- }
- }
-
- private static AlarmManager getAlarmManager(Context context) {
- return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- }
-
- /**
- * Compute all font and icon sizes based on the given {@code clockFontSize} and apply them to
- * the offscreen {@code sizer} view. Measure the {@code sizer} view and return the resulting
- * size measurements.
- */
- private static Sizes measure(Sizes template, int clockFontSize, View sizer) {
- // Create a copy of the given template sizes.
- final Sizes measuredSizes = template.newSize();
-
- // Configure the clock to display the widest time string.
- final TextClock date = (TextClock) sizer.findViewById(R.id.date);
- final TextClock clock = (TextClock) sizer.findViewById(R.id.clock);
- final TextView nextAlarm = (TextView) sizer.findViewById(R.id.nextAlarm);
- final TextView nextAlarmIcon = (TextView) sizer.findViewById(R.id.nextAlarmIcon);
-
- // Adjust the font sizes.
- measuredSizes.setClockFontSizePx(clockFontSize);
- clock.setText(getLongestTimeString(clock));
- clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx);
- date.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
- nextAlarm.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx);
- nextAlarmIcon.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mIconFontSizePx);
- nextAlarmIcon.setPadding(measuredSizes.mIconPaddingPx, 0, measuredSizes.mIconPaddingPx, 0);
-
- // Measure and layout the sizer.
- final int widthSize = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx);
- final int heightSize = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx);
- final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED);
- final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED);
- sizer.measure(widthMeasureSpec, heightMeasureSpec);
- sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight());
-
- // Copy the measurements into the result object.
- measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth();
- measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight();
- measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth();
- measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight();
-
- // If an alarm icon is required, generate one from the TextView with the special font.
- if (nextAlarmIcon.getVisibility() == VISIBLE) {
- measuredSizes.mIconBitmap = Utils.createBitmap(nextAlarmIcon);
- }
-
- return measuredSizes;
- }
-
- /**
- * @return "11:59" or "23:59" in the current locale
- */
- private static CharSequence getLongestTimeString(TextClock clock) {
- final CharSequence format = clock.is24HourModeEnabled()
- ? clock.getFormat24Hour()
- : clock.getFormat12Hour();
- final Calendar longestPMTime = Calendar.getInstance();
- longestPMTime.set(0, 0, 0, 23, 59);
- return DateFormat.format(format, longestPMTime);
- }
-
- /**
- * @return the locale-specific date pattern
- */
- private static String getDateFormat(Context context) {
- final Locale locale = Locale.getDefault();
- final String skeleton = context.getString(R.string.abbrev_wday_month_day_no_year);
- return DateFormat.getBestDateTimePattern(locale, skeleton);
- }
-
- /**
- * This class stores the target size of the widget as well as the measured size using a given
- * clock font size. All other fonts and icons are scaled proportional to the clock font.
- */
- private static final class Sizes {
-
- private final int mTargetWidthPx;
- private final int mTargetHeightPx;
- private final int mLargestClockFontSizePx;
- private final int mSmallestClockFontSizePx;
- private Bitmap mIconBitmap;
-
- private int mMeasuredWidthPx;
- private int mMeasuredHeightPx;
- private int mMeasuredTextClockWidthPx;
- private int mMeasuredTextClockHeightPx;
-
- /** The size of the font to use on the date / next alarm time fields. */
- private int mFontSizePx;
-
- /** The size of the font to use on the clock field. */
- private int mClockFontSizePx;
-
- private int mIconFontSizePx;
- private int mIconPaddingPx;
-
- private Sizes(int targetWidthPx, int targetHeightPx, int largestClockFontSizePx) {
- mTargetWidthPx = targetWidthPx;
- mTargetHeightPx = targetHeightPx;
- mLargestClockFontSizePx = largestClockFontSizePx;
- mSmallestClockFontSizePx = 1;
- }
-
- private int getLargestClockFontSizePx() { return mLargestClockFontSizePx; }
- private int getSmallestClockFontSizePx() { return mSmallestClockFontSizePx; }
- private int getClockFontSizePx() { return mClockFontSizePx; }
- private void setClockFontSizePx(int clockFontSizePx) {
- mClockFontSizePx = clockFontSizePx;
- mFontSizePx = max(1, round(clockFontSizePx / 7.5f));
- mIconFontSizePx = (int) (mFontSizePx * 1.4f);
- mIconPaddingPx = mFontSizePx / 3;
- }
-
- /**
- * @return the amount of widget height available to the world cities list
- */
- private int getListHeight() {
- return mTargetHeightPx - mMeasuredHeightPx;
- }
-
- private boolean hasViolations() {
- return mMeasuredWidthPx > mTargetWidthPx || mMeasuredHeightPx > mTargetHeightPx;
- }
-
- private Sizes newSize() {
- return new Sizes(mTargetWidthPx, mTargetHeightPx, mLargestClockFontSizePx);
- }
-
- @Override
- public String toString() {
- final StringBuilder builder = new StringBuilder(1000);
- builder.append("\n");
- append(builder, "Target dimensions: %dpx x %dpx\n", mTargetWidthPx, mTargetHeightPx);
- append(builder, "Last valid widget container measurement: %dpx x %dpx\n",
- mMeasuredWidthPx, mMeasuredHeightPx);
- append(builder, "Last text clock measurement: %dpx x %dpx\n",
- mMeasuredTextClockWidthPx, mMeasuredTextClockHeightPx);
- if (mMeasuredWidthPx > mTargetWidthPx) {
- append(builder, "Measured width %dpx exceeded widget width %dpx\n",
- mMeasuredWidthPx, mTargetWidthPx);
- }
- if (mMeasuredHeightPx > mTargetHeightPx) {
- append(builder, "Measured height %dpx exceeded widget height %dpx\n",
- mMeasuredHeightPx, mTargetHeightPx);
- }
- append(builder, "Clock font: %dpx\n", mClockFontSizePx);
- return builder.toString();
- }
-
- private static void append(StringBuilder builder, String format, Object... args) {
- builder.append(String.format(Locale.ENGLISH, format, args));
- }
- }
-}
diff --git a/src/com/android/alarmclock/DigitalAppWidgetProvider.kt b/src/com/android/alarmclock/DigitalAppWidgetProvider.kt
new file mode 100644
index 0000000..0f517c9
--- /dev/null
+++ b/src/com/android/alarmclock/DigitalAppWidgetProvider.kt
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.alarmclock
+
+import android.annotation.SuppressLint
+import android.app.AlarmManager
+import android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED
+import android.app.PendingIntent
+import android.app.PendingIntent.FLAG_NO_CREATE
+import android.app.PendingIntent.FLAG_UPDATE_CURRENT
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
+import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_DATE_CHANGED
+import android.content.Intent.ACTION_LOCALE_CHANGED
+import android.content.Intent.ACTION_SCREEN_ON
+import android.content.Intent.ACTION_TIMEZONE_CHANGED
+import android.content.Intent.ACTION_TIME_CHANGED
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.util.ArraySet
+import android.util.TypedValue.COMPLEX_UNIT_PX
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.View.VISIBLE
+import android.widget.RemoteViews
+import android.widget.TextClock
+import android.widget.TextView
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.worldclock.CitySelectionActivity
+
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * This provider produces a widget resembling one of the formats below.
+ *
+ * If an alarm is scheduled to ring in the future:
+ * <pre>
+ * 12:59 AM
+ * WED, FEB 3 ⏰ THU 9:30 AM
+ * </pre>
+ *
+ * If no alarm is scheduled to ring in the future:
+ * <pre>
+ * 12:59 AM
+ * WED, FEB 3
+ * </pre>
+ *
+ * This widget is scaling the font sizes to fit within the widget bounds chosen by the user without
+ * any clipping. To do so it measures layouts offscreen using a range of font sizes in order to
+ * choose optimal values.
+ */
+class DigitalAppWidgetProvider : AppWidgetProvider() {
+
+ override fun onEnabled(context: Context) {
+ super.onEnabled(context)
+
+ // Schedule the day-change callback if necessary.
+ updateDayChangeCallback(context)
+ }
+
+ override fun onDisabled(context: Context) {
+ super.onDisabled(context)
+
+ // Remove any scheduled day-change callback.
+ removeDayChangeCallback(context)
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ LOGGER.i("onReceive: $intent")
+ super.onReceive(context, intent)
+
+ val wm: AppWidgetManager = AppWidgetManager.getInstance(context) ?: return
+
+ val provider = ComponentName(context, javaClass)
+ val widgetIds: IntArray = wm.getAppWidgetIds(provider)
+
+ val action: String? = intent.action
+ when (action) {
+ ACTION_NEXT_ALARM_CLOCK_CHANGED,
+ ACTION_DATE_CHANGED,
+ ACTION_LOCALE_CHANGED,
+ ACTION_SCREEN_ON,
+ ACTION_TIME_CHANGED,
+ ACTION_TIMEZONE_CHANGED,
+ AlarmStateManager.ACTION_ALARM_CHANGED,
+ ACTION_ON_DAY_CHANGE,
+ DataModel.ACTION_WORLD_CITIES_CHANGED -> widgetIds.forEach { widgetId ->
+ relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId))
+ }
+ }
+
+ val dm = DataModel.dataModel
+ dm.updateWidgetCount(javaClass, widgetIds.size, R.string.category_digital_widget)
+
+ if (widgetIds.size > 0) {
+ updateDayChangeCallback(context)
+ }
+ }
+
+ /**
+ * Called when widgets must provide remote views.
+ */
+ override fun onUpdate(context: Context, wm: AppWidgetManager, widgetIds: IntArray) {
+ super.onUpdate(context, wm, widgetIds)
+
+ widgetIds.forEach { widgetId ->
+ relayoutWidget(context, wm, widgetId, wm.getAppWidgetOptions(widgetId))
+ }
+ }
+
+ /**
+ * Called when the app widget changes sizes.
+ */
+ override fun onAppWidgetOptionsChanged(
+ context: Context,
+ wm: AppWidgetManager?,
+ widgetId: Int,
+ options: Bundle
+ ) {
+ super.onAppWidgetOptionsChanged(context, wm, widgetId, options)
+
+ // Scale the fonts of the clock to fit inside the new size
+ relayoutWidget(context, AppWidgetManager.getInstance(context), widgetId, options)
+ }
+
+ /**
+ * Remove the existing day-change callback if it is not needed (no selected cities exist).
+ * Add the day-change callback if it is needed (selected cities exist).
+ */
+ private fun updateDayChangeCallback(context: Context) {
+ val dm = DataModel.dataModel
+ val selectedCities = dm.selectedCities
+ val showHomeClock = dm.showHomeClock
+ if (selectedCities.isEmpty() && !showHomeClock) {
+ // Remove the existing day-change callback.
+ removeDayChangeCallback(context)
+ return
+ }
+
+ // Look up the time at which the next day change occurs across all timezones.
+ val zones: MutableSet<TimeZone> = ArraySet(selectedCities.size + 2)
+ zones.add(TimeZone.getDefault())
+ if (showHomeClock) {
+ zones.add(dm.homeCity.timeZone)
+ }
+ selectedCities.forEach { city ->
+ zones.add(city.timeZone)
+ }
+ val nextDay = Utils.getNextDay(Date(), zones)
+
+ // Schedule the next day-change callback; at least one city is displayed.
+ val pi: PendingIntent =
+ PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_UPDATE_CURRENT)
+ getAlarmManager(context).setExact(AlarmManager.RTC, nextDay.time, pi)
+ }
+
+ /**
+ * Remove the existing day-change callback.
+ */
+ private fun removeDayChangeCallback(context: Context) {
+ val pi: PendingIntent? =
+ PendingIntent.getBroadcast(context, 0, DAY_CHANGE_INTENT, FLAG_NO_CREATE)
+ if (pi != null) {
+ getAlarmManager(context).cancel(pi)
+ pi.cancel()
+ }
+ }
+
+ /**
+ * This class stores the target size of the widget as well as the measured size using a given
+ * clock font size. All other fonts and icons are scaled proportional to the clock font.
+ */
+ private class Sizes(
+ val mTargetWidthPx: Int,
+ val mTargetHeightPx: Int,
+ val largestClockFontSizePx: Int
+ ) {
+ val smallestClockFontSizePx = 1
+ var mIconBitmap: Bitmap? = null
+
+ var mMeasuredWidthPx = 0
+ var mMeasuredHeightPx = 0
+ var mMeasuredTextClockWidthPx = 0
+ var mMeasuredTextClockHeightPx = 0
+
+ /** The size of the font to use on the date / next alarm time fields. */
+ var mFontSizePx = 0
+
+ /** The size of the font to use on the clock field. */
+ var mClockFontSizePx = 0
+
+ var mIconFontSizePx = 0
+ var mIconPaddingPx = 0
+
+ var clockFontSizePx: Int
+ get() = mClockFontSizePx
+ set(clockFontSizePx) {
+ mClockFontSizePx = clockFontSizePx
+ mFontSizePx = Math.max(1, Math.round(clockFontSizePx / 7.5f))
+ mIconFontSizePx = (mFontSizePx * 1.4f).toInt()
+ mIconPaddingPx = mFontSizePx / 3
+ }
+
+ /**
+ * @return the amount of widget height available to the world cities list
+ */
+ val listHeight: Int
+ get() = mTargetHeightPx - mMeasuredHeightPx
+
+ fun hasViolations(): Boolean {
+ return mMeasuredWidthPx > mTargetWidthPx || mMeasuredHeightPx > mTargetHeightPx
+ }
+
+ fun newSize(): Sizes {
+ return Sizes(mTargetWidthPx, mTargetHeightPx, largestClockFontSizePx)
+ }
+
+ override fun toString(): String {
+ val builder = StringBuilder(1000)
+ builder.append("\n")
+ append(builder, "Target dimensions: %dpx x %dpx\n", mTargetWidthPx, mTargetHeightPx)
+ append(builder, "Last valid widget container measurement: %dpx x %dpx\n",
+ mMeasuredWidthPx, mMeasuredHeightPx)
+ append(builder, "Last text clock measurement: %dpx x %dpx\n",
+ mMeasuredTextClockWidthPx, mMeasuredTextClockHeightPx)
+ if (mMeasuredWidthPx > mTargetWidthPx) {
+ append(builder, "Measured width %dpx exceeded widget width %dpx\n",
+ mMeasuredWidthPx, mTargetWidthPx)
+ }
+ if (mMeasuredHeightPx > mTargetHeightPx) {
+ append(builder, "Measured height %dpx exceeded widget height %dpx\n",
+ mMeasuredHeightPx, mTargetHeightPx)
+ }
+ append(builder, "Clock font: %dpx\n", mClockFontSizePx)
+ return builder.toString()
+ }
+
+ companion object {
+ private fun append(builder: StringBuilder, format: String, vararg args: Any) {
+ builder.append(String.format(Locale.ENGLISH, format, *args))
+ }
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("DigitalWidgetProvider")
+
+ /**
+ * Intent action used for refreshing a world city display when any of them changes days or when
+ * the default TimeZone changes days. This affects the widget display because the day-of-week is
+ * only visible when the world city day-of-week differs from the default TimeZone's day-of-week.
+ */
+ private const val ACTION_ON_DAY_CHANGE = "com.android.deskclock.ON_DAY_CHANGE"
+
+ /** Intent used to deliver the [.ACTION_ON_DAY_CHANGE] callback. */
+ private val DAY_CHANGE_INTENT: Intent = Intent(ACTION_ON_DAY_CHANGE)
+
+ /**
+ * Compute optimal font and icon sizes offscreen for both portrait and landscape orientations
+ * using the last known widget size and apply them to the widget.
+ */
+ private fun relayoutWidget(
+ context: Context,
+ wm: AppWidgetManager,
+ widgetId: Int,
+ options: Bundle
+ ) {
+ val portrait: RemoteViews = relayoutWidget(context, wm, widgetId, options, true)
+ val landscape: RemoteViews = relayoutWidget(context, wm, widgetId, options, false)
+ val widget = RemoteViews(landscape, portrait)
+ wm.updateAppWidget(widgetId, widget)
+ wm.notifyAppWidgetViewDataChanged(widgetId, R.id.world_city_list)
+ }
+
+ /**
+ * Compute optimal font and icon sizes offscreen for the given orientation.
+ */
+ private fun relayoutWidget(
+ context: Context,
+ wm: AppWidgetManager,
+ widgetId: Int,
+ options: Bundle?,
+ portrait: Boolean
+ ): RemoteViews {
+ // Create a remote view for the digital clock.
+ val packageName: String = context.getPackageName()
+ val rv = RemoteViews(packageName, R.layout.digital_widget)
+
+ // Tapping on the widget opens the app (if not on the lock screen).
+ if (Utils.isWidgetClickable(wm, widgetId)) {
+ val openApp = Intent(context, DeskClock::class.java)
+ val pi: PendingIntent = PendingIntent.getActivity(context, 0, openApp, 0)
+ rv.setOnClickPendingIntent(R.id.digital_widget, pi)
+ }
+
+ // Configure child views of the remote view.
+ val dateFormat: CharSequence = getDateFormat(context)
+ rv.setCharSequence(R.id.date, "setFormat12Hour", dateFormat)
+ rv.setCharSequence(R.id.date, "setFormat24Hour", dateFormat)
+
+ val nextAlarmTime: String? = Utils.getNextAlarm(context)
+ if (TextUtils.isEmpty(nextAlarmTime)) {
+ rv.setViewVisibility(R.id.nextAlarm, GONE)
+ rv.setViewVisibility(R.id.nextAlarmIcon, GONE)
+ } else {
+ rv.setTextViewText(R.id.nextAlarm, nextAlarmTime)
+ rv.setViewVisibility(R.id.nextAlarm, VISIBLE)
+ rv.setViewVisibility(R.id.nextAlarmIcon, VISIBLE)
+ }
+
+ val options = options ?: wm.getAppWidgetOptions(widgetId)
+
+ // Fetch the widget size selected by the user.
+ val resources: Resources = context.getResources()
+ val density: Float = resources.getDisplayMetrics().density
+ val minWidthPx = (density * options.getInt(OPTION_APPWIDGET_MIN_WIDTH)).toInt()
+ val minHeightPx = (density * options.getInt(OPTION_APPWIDGET_MIN_HEIGHT)).toInt()
+ val maxWidthPx = (density * options.getInt(OPTION_APPWIDGET_MAX_WIDTH)).toInt()
+ val maxHeightPx = (density * options.getInt(OPTION_APPWIDGET_MAX_HEIGHT)).toInt()
+ val targetWidthPx = if (portrait) minWidthPx else maxWidthPx
+ val targetHeightPx = if (portrait) maxHeightPx else minHeightPx
+ val largestClockFontSizePx: Int =
+ resources.getDimensionPixelSize(R.dimen.widget_max_clock_font_size)
+
+ // Create a size template that describes the widget bounds.
+ val template = Sizes(targetWidthPx, targetHeightPx, largestClockFontSizePx)
+
+ // Compute optimal font sizes and icon sizes to fit within the widget bounds.
+ val sizes = optimizeSizes(context, template, nextAlarmTime)
+ if (LOGGER.isVerboseLoggable) {
+ LOGGER.v(sizes.toString())
+ }
+
+ // Apply the computed sizes to the remote views.
+ rv.setImageViewBitmap(R.id.nextAlarmIcon, sizes.mIconBitmap)
+ rv.setTextViewTextSize(R.id.date, COMPLEX_UNIT_PX, sizes.mFontSizePx.toFloat())
+ rv.setTextViewTextSize(R.id.nextAlarm, COMPLEX_UNIT_PX, sizes.mFontSizePx.toFloat())
+ rv.setTextViewTextSize(R.id.clock, COMPLEX_UNIT_PX, sizes.mClockFontSizePx.toFloat())
+
+ val smallestWorldCityListSizePx: Int =
+ resources.getDimensionPixelSize(R.dimen.widget_min_world_city_list_size)
+ if (sizes.listHeight <= smallestWorldCityListSizePx) {
+ // Insufficient space; hide the world city list.
+ rv.setViewVisibility(R.id.world_city_list, GONE)
+ } else {
+ // Set an adapter on the world city list. That adapter connects to a Service via intent.
+ val intent = Intent(context, DigitalAppWidgetCityService::class.java)
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
+ intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)))
+ rv.setRemoteAdapter(R.id.world_city_list, intent)
+ rv.setViewVisibility(R.id.world_city_list, VISIBLE)
+
+ // Tapping on the widget opens the city selection activity (if not on the lock screen).
+ if (Utils.isWidgetClickable(wm, widgetId)) {
+ val selectCity = Intent(context, CitySelectionActivity::class.java)
+ val pi: PendingIntent = PendingIntent.getActivity(context, 0, selectCity, 0)
+ rv.setPendingIntentTemplate(R.id.world_city_list, pi)
+ }
+ }
+
+ return rv
+ }
+
+ /**
+ * Inflate an offscreen copy of the widget views. Binary search through the range of sizes
+ * until the optimal sizes that fit within the widget bounds are located.
+ */
+ private fun optimizeSizes(
+ context: Context,
+ template: Sizes,
+ nextAlarmTime: String?
+ ): Sizes {
+ // Inflate a test layout to compute sizes at different font sizes.
+ val inflater: LayoutInflater = LayoutInflater.from(context)
+ @SuppressLint("InflateParams") val sizer: View =
+ inflater.inflate(R.layout.digital_widget_sizer, null /* root */)
+
+ // Configure the date to display the current date string.
+ val dateFormat: CharSequence = getDateFormat(context)
+ val date: TextClock = sizer.findViewById(R.id.date) as TextClock
+ date.setFormat12Hour(dateFormat)
+ date.setFormat24Hour(dateFormat)
+
+ // Configure the next alarm views to display the next alarm time or be gone.
+ val nextAlarmIcon: TextView = sizer.findViewById(R.id.nextAlarmIcon) as TextView
+ val nextAlarm: TextView = sizer.findViewById(R.id.nextAlarm) as TextView
+ if (TextUtils.isEmpty(nextAlarmTime)) {
+ nextAlarm.setVisibility(GONE)
+ nextAlarmIcon.setVisibility(GONE)
+ } else {
+ nextAlarm.setText(nextAlarmTime)
+ nextAlarm.setVisibility(VISIBLE)
+ nextAlarmIcon.setVisibility(VISIBLE)
+ nextAlarmIcon.setTypeface(UiDataModel.uiDataModel.alarmIconTypeface)
+ }
+
+ // Measure the widget at the largest possible size.
+ var high = measure(template, template.largestClockFontSizePx, sizer)
+ if (!high.hasViolations()) {
+ return high
+ }
+
+ // Measure the widget at the smallest possible size.
+ var low = measure(template, template.smallestClockFontSizePx, sizer)
+ if (low.hasViolations()) {
+ return low
+ }
+
+ // Binary search between the smallest and largest sizes until an optimum size is found.
+ while (low.clockFontSizePx != high.clockFontSizePx) {
+ val midFontSize: Int = (low.clockFontSizePx + high.clockFontSizePx) / 2
+ if (midFontSize == low.clockFontSizePx) {
+ return low
+ }
+ val midSize = measure(template, midFontSize, sizer)
+ if (midSize.hasViolations()) {
+ high = midSize
+ } else {
+ low = midSize
+ }
+ }
+
+ return low
+ }
+
+ private fun getAlarmManager(context: Context): AlarmManager {
+ return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ }
+
+ /**
+ * Compute all font and icon sizes based on the given `clockFontSize` and apply them to
+ * the offscreen `sizer` view. Measure the `sizer` view and return the resulting
+ * size measurements.
+ */
+ private fun measure(template: Sizes, clockFontSize: Int, sizer: View): Sizes {
+ // Create a copy of the given template sizes.
+ val measuredSizes = template.newSize()
+
+ // Configure the clock to display the widest time string.
+ val date: TextClock = sizer.findViewById(R.id.date) as TextClock
+ val clock: TextClock = sizer.findViewById(R.id.clock) as TextClock
+ val nextAlarm: TextView = sizer.findViewById(R.id.nextAlarm) as TextView
+ val nextAlarmIcon: TextView = sizer.findViewById(R.id.nextAlarmIcon) as TextView
+
+ // Adjust the font sizes.
+ measuredSizes.clockFontSizePx = clockFontSize
+ clock.setText(getLongestTimeString(clock))
+ clock.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mClockFontSizePx.toFloat())
+ date.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx.toFloat())
+ nextAlarm.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mFontSizePx.toFloat())
+ nextAlarmIcon.setTextSize(COMPLEX_UNIT_PX, measuredSizes.mIconFontSizePx.toFloat())
+ nextAlarmIcon
+ .setPadding(measuredSizes.mIconPaddingPx, 0, measuredSizes.mIconPaddingPx, 0)
+
+ // Measure and layout the sizer.
+ val widthSize: Int = View.MeasureSpec.getSize(measuredSizes.mTargetWidthPx)
+ val heightSize: Int = View.MeasureSpec.getSize(measuredSizes.mTargetHeightPx)
+ val widthMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(widthSize, UNSPECIFIED)
+ val heightMeasureSpec: Int = View.MeasureSpec.makeMeasureSpec(heightSize, UNSPECIFIED)
+ sizer.measure(widthMeasureSpec, heightMeasureSpec)
+ sizer.layout(0, 0, sizer.getMeasuredWidth(), sizer.getMeasuredHeight())
+
+ // Copy the measurements into the result object.
+ measuredSizes.mMeasuredWidthPx = sizer.getMeasuredWidth()
+ measuredSizes.mMeasuredHeightPx = sizer.getMeasuredHeight()
+ measuredSizes.mMeasuredTextClockWidthPx = clock.getMeasuredWidth()
+ measuredSizes.mMeasuredTextClockHeightPx = clock.getMeasuredHeight()
+
+ // If an alarm icon is required, generate one from the TextView with the special font.
+ if (nextAlarmIcon.getVisibility() == VISIBLE) {
+ measuredSizes.mIconBitmap = Utils.createBitmap(nextAlarmIcon)
+ }
+
+ return measuredSizes
+ }
+
+ /**
+ * @return "11:59" or "23:59" in the current locale
+ */
+ private fun getLongestTimeString(clock: TextClock): CharSequence {
+ val format: CharSequence = if (clock.is24HourModeEnabled()) {
+ clock.getFormat24Hour()
+ } else {
+ clock.getFormat12Hour()
+ }
+ val longestPMTime = Calendar.getInstance()
+ longestPMTime[0, 0, 0, 23] = 59
+ return DateFormat.format(format, longestPMTime)
+ }
+
+ /**
+ * @return the locale-specific date pattern
+ */
+ private fun getDateFormat(context: Context): String {
+ val locale = Locale.getDefault()
+ val skeleton: String = context.getString(R.string.abbrev_wday_month_day_no_year)
+ return DateFormat.getBestDateTimePattern(locale, skeleton)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/WidgetUtils.java b/src/com/android/alarmclock/WidgetUtils.java
deleted file mode 100644
index be7bf33..0000000
--- a/src/com/android/alarmclock/WidgetUtils.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.alarmclock;
-
-import android.appwidget.AppWidgetManager;
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.Bundle;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-public final class WidgetUtils {
-
- private WidgetUtils() {}
-
- // Calculate the scale factor of the fonts in the widget
- public static float getScaleRatio(Context context, Bundle options, int id, int cityCount) {
- if (options == null) {
- AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
- if (widgetManager == null) {
- // no manager , do no scaling
- return 1f;
- }
- options = widgetManager.getAppWidgetOptions(id);
- }
- if (options != null) {
- int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
- if (minWidth == 0) {
- // No data , do no scaling
- return 1f;
- }
- final Resources res = context.getResources();
- float density = res.getDisplayMetrics().density;
- float ratio = (density * minWidth) / res.getDimension(R.dimen.min_digital_widget_width);
- ratio = Math.min(ratio, getHeightScaleRatio(context, options, id));
- ratio *= .83f;
-
- if (cityCount > 0) {
- return (ratio > 1f) ? 1f : ratio;
- }
-
- ratio = Math.min(ratio, 1.6f);
- if (Utils.isPortrait(context)) {
- ratio = Math.max(ratio, .71f);
- }
- else {
- ratio = Math.max(ratio, .45f);
- }
- return ratio;
- }
- return 1f;
- }
-
- // Calculate the scale factor of the fonts in the list of the widget using the widget height
- private static float getHeightScaleRatio(Context context, Bundle options, int id) {
- if (options == null) {
- AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
- if (widgetManager == null) {
- // no manager , do no scaling
- return 1f;
- }
- options = widgetManager.getAppWidgetOptions(id);
- }
- if (options != null) {
- int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
- if (minHeight == 0) {
- // No data , do no scaling
- return 1f;
- }
- final Resources res = context.getResources();
- float density = res.getDisplayMetrics().density;
- float ratio = density * minHeight / res.getDimension(R.dimen.min_digital_widget_height);
- if (Utils.isPortrait(context)) {
- return ratio * 1.75f;
- }
- return ratio;
- }
- return 1;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/alarmclock/WidgetUtils.kt b/src/com/android/alarmclock/WidgetUtils.kt
new file mode 100644
index 0000000..c8ff7aa
--- /dev/null
+++ b/src/com/android/alarmclock/WidgetUtils.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.alarmclock
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.res.Resources
+import android.os.Bundle
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+object WidgetUtils {
+ // Calculate the scale factor of the fonts in the widget
+ fun getScaleRatio(context: Context, options: Bundle?, id: Int, cityCount: Int): Float {
+ var options: Bundle? = options
+ if (options == null) {
+ val widgetManager: AppWidgetManager =
+ AppWidgetManager.getInstance(context) // no manager , do no scaling
+ ?: return 1f
+ options = widgetManager.getAppWidgetOptions(id)
+ }
+ options?.let {
+ val minWidth: Int = it.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
+ if (minWidth == 0) {
+ // No data , do no scaling
+ return 1f
+ }
+ val res: Resources = context.getResources()
+ val density: Float = res.getDisplayMetrics().density
+ var ratio: Float =
+ density * minWidth / res.getDimension(R.dimen.min_digital_widget_width)
+ ratio = Math.min(ratio, getHeightScaleRatio(context, it))
+ ratio *= .83f
+
+ if (cityCount > 0) {
+ return if (ratio > 1f) 1f else ratio
+ }
+
+ ratio = Math.min(ratio, 1.6f)
+ ratio = if (Utils.isPortrait(context)) {
+ Math.max(ratio, .71f)
+ } else {
+ Math.max(ratio, .45f)
+ }
+ return ratio
+ }
+ return 1f
+ }
+
+ // Calculate the scale factor of the fonts in the list of the widget using the widget height
+ private fun getHeightScaleRatio(context: Context, options: Bundle): Float {
+ val minHeight: Int = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
+ if (minHeight == 0) {
+ // No data , do no scaling
+ return 1f
+ }
+ val res: Resources = context.getResources()
+ val density: Float = res.getDisplayMetrics().density
+ val ratio: Float = density * minHeight / res.getDimension(R.dimen.min_digital_widget_height)
+ return if (Utils.isPortrait(context)) { ratio * 1.75f } else ratio
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmAlertWakeLock.java b/src/com/android/deskclock/AlarmAlertWakeLock.java
deleted file mode 100644
index 6af687e..0000000
--- a/src/com/android/deskclock/AlarmAlertWakeLock.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import android.os.PowerManager;
-
-/**
- * Utility class to hold wake lock in app.
- */
-public class AlarmAlertWakeLock {
-
- private static final String TAG = "AlarmAlertWakeLock";
-
- private static PowerManager.WakeLock sCpuWakeLock;
-
- public static PowerManager.WakeLock createPartialWakeLock(Context context) {
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
- }
-
- public static void acquireCpuWakeLock(Context context) {
- if (sCpuWakeLock != null) {
- return;
- }
-
- sCpuWakeLock = createPartialWakeLock(context);
- sCpuWakeLock.acquire();
- }
-
- public static void acquireScreenCpuWakeLock(Context context) {
- if (sCpuWakeLock != null) {
- return;
- }
- PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
- sCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK
- | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE, TAG);
- sCpuWakeLock.acquire();
- }
-
- public static void releaseCpuLock() {
- if (sCpuWakeLock != null) {
- sCpuWakeLock.release();
- sCpuWakeLock = null;
- }
- }
-}
diff --git a/src/com/android/deskclock/AlarmAlertWakeLock.kt b/src/com/android/deskclock/AlarmAlertWakeLock.kt
new file mode 100644
index 0000000..ca97ed3
--- /dev/null
+++ b/src/com/android/deskclock/AlarmAlertWakeLock.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.os.PowerManager
+import android.os.PowerManager.WakeLock
+
+/**
+ * Utility class to hold wake lock in app.
+ */
+object AlarmAlertWakeLock {
+ private const val TAG = "AlarmAlertWakeLock"
+
+ private var sCpuWakeLock: WakeLock? = null
+
+ @JvmStatic
+ fun createPartialWakeLock(context: Context): WakeLock {
+ val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ return pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG)
+ }
+
+ @JvmStatic
+ fun acquireCpuWakeLock(context: Context) {
+ if (sCpuWakeLock != null) {
+ return
+ }
+
+ sCpuWakeLock = createPartialWakeLock(context)
+ sCpuWakeLock!!.acquire()
+ }
+
+ @JvmStatic
+ fun acquireScreenCpuWakeLock(context: Context) {
+ if (sCpuWakeLock != null) {
+ return
+ }
+ val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
+ sCpuWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK
+ or PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.ON_AFTER_RELEASE, TAG)
+ sCpuWakeLock!!.acquire()
+ }
+
+ @JvmStatic
+ fun releaseCpuLock() {
+ if (sCpuWakeLock != null) {
+ sCpuWakeLock!!.release()
+ sCpuWakeLock = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmClockFragment.java b/src/com/android/deskclock/AlarmClockFragment.java
deleted file mode 100644
index 5c02d03..0000000
--- a/src/com/android/deskclock/AlarmClockFragment.java
+++ /dev/null
@@ -1,447 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.LoaderManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.Loader;
-import android.database.Cursor;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.deskclock.alarms.AlarmTimeClickHandler;
-import com.android.deskclock.alarms.AlarmUpdateHandler;
-import com.android.deskclock.alarms.ScrollHandler;
-import com.android.deskclock.alarms.TimePickerDialogFragment;
-import com.android.deskclock.alarms.dataadapter.AlarmItemHolder;
-import com.android.deskclock.alarms.dataadapter.CollapsedAlarmViewHolder;
-import com.android.deskclock.alarms.dataadapter.ExpandedAlarmViewHolder;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.widget.EmptyViewController;
-import com.android.deskclock.widget.toast.SnackbarManager;
-import com.android.deskclock.widget.toast.ToastManager;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.ALARMS;
-
-/**
- * A fragment that displays a list of alarm time and allows interaction with them.
- */
-public final class AlarmClockFragment extends DeskClockFragment implements
- LoaderManager.LoaderCallbacks<Cursor>,
- ScrollHandler,
- TimePickerDialogFragment.OnTimeSetListener {
-
- // This extra is used when receiving an intent to create an alarm, but no alarm details
- // have been passed in, so the alarm page should start the process of creating a new alarm.
- public static final String ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new";
-
- // This extra is used when receiving an intent to scroll to specific alarm. If alarm
- // can not be found, and toast message will pop up that the alarm has be deleted.
- public static final String SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm";
-
- private static final String KEY_EXPANDED_ID = "expandedId";
-
- // Updates "Today/Tomorrow" in the UI when midnight passes.
- private final Runnable mMidnightUpdater = new MidnightRunnable();
-
- // Views
- private ViewGroup mMainLayout;
- private RecyclerView mRecyclerView;
-
- // Data
- private Loader mCursorLoader;
- private long mScrollToAlarmId = Alarm.INVALID_ID;
- private long mExpandedAlarmId = Alarm.INVALID_ID;
- private long mCurrentUpdateToken;
-
- // Controllers
- private ItemAdapter<AlarmItemHolder> mItemAdapter;
- private AlarmUpdateHandler mAlarmUpdateHandler;
- private EmptyViewController mEmptyViewController;
- private AlarmTimeClickHandler mAlarmTimeClickHandler;
- private LinearLayoutManager mLayoutManager;
-
- /**
- * The public no-arg constructor required by all fragments.
- */
- public AlarmClockFragment() {
- super(ALARMS);
- }
-
- @Override
- public void onCreate(Bundle savedState) {
- super.onCreate(savedState);
- mCursorLoader = getLoaderManager().initLoader(0, null, this);
- if (savedState != null) {
- mExpandedAlarmId = savedState.getLong(KEY_EXPANDED_ID, Alarm.INVALID_ID);
- }
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
- // Inflate the layout for this fragment
- final View v = inflater.inflate(R.layout.alarm_clock, container, false);
- final Context context = getActivity();
-
- mRecyclerView = (RecyclerView) v.findViewById(R.id.alarms_recycler_view);
- mLayoutManager = new LinearLayoutManager(context) {
- @Override
- protected int getExtraLayoutSpace(RecyclerView.State state) {
- final int extraSpace = super.getExtraLayoutSpace(state);
- if (state.willRunPredictiveAnimations()) {
- return Math.max(getHeight(), extraSpace);
- }
- return extraSpace;
- }
- };
- mRecyclerView.setLayoutManager(mLayoutManager);
- mMainLayout = (ViewGroup) v.findViewById(R.id.main);
- mAlarmUpdateHandler = new AlarmUpdateHandler(context, this, mMainLayout);
- final TextView emptyView = (TextView) v.findViewById(R.id.alarms_empty_view);
- final Drawable noAlarms = Utils.getVectorDrawable(context, R.drawable.ic_noalarms);
- emptyView.setCompoundDrawablesWithIntrinsicBounds(null, noAlarms, null, null);
- mEmptyViewController = new EmptyViewController(mMainLayout, mRecyclerView, emptyView);
- mAlarmTimeClickHandler = new AlarmTimeClickHandler(this, savedState, mAlarmUpdateHandler,
- this);
-
- mItemAdapter = new ItemAdapter<>();
- mItemAdapter.setHasStableIds();
- mItemAdapter.withViewTypes(new CollapsedAlarmViewHolder.Factory(inflater),
- null, CollapsedAlarmViewHolder.VIEW_TYPE);
- mItemAdapter.withViewTypes(new ExpandedAlarmViewHolder.Factory(context),
- null, ExpandedAlarmViewHolder.VIEW_TYPE);
- mItemAdapter.setOnItemChangedListener(new ItemAdapter.OnItemChangedListener() {
- @Override
- public void onItemChanged(ItemAdapter.ItemHolder<?> holder) {
- if (((AlarmItemHolder) holder).isExpanded()) {
- if (mExpandedAlarmId != holder.itemId) {
- // Collapse the prior expanded alarm.
- final AlarmItemHolder aih = mItemAdapter.findItemById(mExpandedAlarmId);
- if (aih != null) {
- aih.collapse();
- }
- // Record the freshly expanded alarm.
- mExpandedAlarmId = holder.itemId;
- final RecyclerView.ViewHolder viewHolder =
- mRecyclerView.findViewHolderForItemId(mExpandedAlarmId);
- if (viewHolder != null) {
- smoothScrollTo(viewHolder.getAdapterPosition());
- }
- }
- } else if (mExpandedAlarmId == holder.itemId) {
- // The expanded alarm is now collapsed so update the tracking id.
- mExpandedAlarmId = Alarm.INVALID_ID;
- }
- }
-
- @Override
- public void onItemChanged(ItemAdapter.ItemHolder<?> holder, Object payload) {
- /* No additional work to do */
- }
- });
- final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
- mRecyclerView.addOnLayoutChangeListener(scrollPositionWatcher);
- mRecyclerView.addOnScrollListener(scrollPositionWatcher);
- mRecyclerView.setAdapter(mItemAdapter);
- final ItemAnimator itemAnimator = new ItemAnimator();
- itemAnimator.setChangeDuration(300L);
- itemAnimator.setMoveDuration(300L);
- mRecyclerView.setItemAnimator(itemAnimator);
- return v;
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- if (!isTabSelected()) {
- TimePickerDialogFragment.removeTimeEditDialog(getFragmentManager());
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- // Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
- // alarms when midnight passes.
- UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
-
- // Check if another app asked us to create a blank new alarm.
- final Intent intent = getActivity().getIntent();
- if (intent == null) {
- return;
- }
-
- if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
- UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
- if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
- // An external app asked us to create a blank alarm.
- startCreatingAlarm();
- }
-
- // Remove the CREATE_NEW extra now that we've processed it.
- intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA);
- } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
- UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
- long alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID);
- if (alarmId != Alarm.INVALID_ID) {
- setSmoothScrollStableId(alarmId);
- if (mCursorLoader != null && mCursorLoader.isStarted()) {
- // We need to force a reload here to make sure we have the latest view
- // of the data to scroll to.
- mCursorLoader.forceLoad();
- }
- }
-
- // Remove the SCROLL_TO_ALARM extra now that we've processed it.
- intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA);
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
- UiDataModel.getUiDataModel().removePeriodicCallback(mMidnightUpdater);
-
- // When the user places the app in the background by pressing "home",
- // dismiss the toast bar. However, since there is no way to determine if
- // home was pressed, just dismiss any existing toast bar when restarting
- // the app.
- mAlarmUpdateHandler.hideUndoBar();
- }
-
- @Override
- public void smoothScrollTo(int position) {
- mLayoutManager.scrollToPositionWithOffset(position, 0);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mAlarmTimeClickHandler.saveInstance(outState);
- outState.putLong(KEY_EXPANDED_ID, mExpandedAlarmId);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- ToastManager.cancelToast();
- }
-
- public void setLabel(Alarm alarm, String label) {
- alarm.label = label;
- mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true);
- }
-
- @Override
- public Loader<Cursor> onCreateLoader(int id, Bundle args) {
- return Alarm.getAlarmsCursorLoader(getActivity());
- }
-
- @Override
- public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor data) {
- final List<AlarmItemHolder> itemHolders = new ArrayList<>(data.getCount());
- for (data.moveToFirst(); !data.isAfterLast(); data.moveToNext()) {
- final Alarm alarm = new Alarm(data);
- final AlarmInstance alarmInstance = alarm.canPreemptivelyDismiss()
- ? new AlarmInstance(data, true /* joinedTable */) : null;
- final AlarmItemHolder itemHolder =
- new AlarmItemHolder(alarm, alarmInstance, mAlarmTimeClickHandler);
- itemHolders.add(itemHolder);
- }
- setAdapterItems(itemHolders, SystemClock.elapsedRealtime());
- }
-
- /**
- * Updates the adapters items, deferring the update until the current animation is finished or
- * if no animation is running then the listener will be automatically be invoked immediately.
- *
- * @param items the new list of {@link AlarmItemHolder} to use
- * @param updateToken a monotonically increasing value used to preserve ordering of deferred
- * updates
- */
- private void setAdapterItems(final List<AlarmItemHolder> items, final long updateToken) {
- if (updateToken < mCurrentUpdateToken) {
- LogUtils.v("Ignoring adapter update: %d < %d", updateToken, mCurrentUpdateToken);
- return;
- }
-
- if (mRecyclerView.getItemAnimator().isRunning()) {
- // RecyclerView is currently animating -> defer update.
- mRecyclerView.getItemAnimator().isRunning(
- new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
- @Override
- public void onAnimationsFinished() {
- setAdapterItems(items, updateToken);
- }
- });
- } else if (mRecyclerView.isComputingLayout()) {
- // RecyclerView is currently computing a layout -> defer update.
- mRecyclerView.post(new Runnable() {
- @Override
- public void run() {
- setAdapterItems(items, updateToken);
- }
- });
- } else {
- mCurrentUpdateToken = updateToken;
- mItemAdapter.setItems(items);
-
- // Show or hide the empty view as appropriate.
- final boolean noAlarms = items.isEmpty();
- mEmptyViewController.setEmpty(noAlarms);
- if (noAlarms) {
- // Ensure the drop shadow is hidden when no alarms exist.
- setTabScrolledToTop(true);
- }
-
- // Expand the correct alarm.
- if (mExpandedAlarmId != Alarm.INVALID_ID) {
- final AlarmItemHolder aih = mItemAdapter.findItemById(mExpandedAlarmId);
- if (aih != null) {
- mAlarmTimeClickHandler.setSelectedAlarm(aih.item);
- aih.expand();
- } else {
- mAlarmTimeClickHandler.setSelectedAlarm(null);
- mExpandedAlarmId = Alarm.INVALID_ID;
- }
- }
-
- // Scroll to the selected alarm.
- if (mScrollToAlarmId != Alarm.INVALID_ID) {
- scrollToAlarm(mScrollToAlarmId);
- setSmoothScrollStableId(Alarm.INVALID_ID);
- }
- }
- }
-
- /**
- * @param alarmId identifies the alarm to be displayed
- */
- private void scrollToAlarm(long alarmId) {
- final int alarmCount = mItemAdapter.getItemCount();
- int alarmPosition = -1;
- for (int i = 0; i < alarmCount; i++) {
- long id = mItemAdapter.getItemId(i);
- if (id == alarmId) {
- alarmPosition = i;
- break;
- }
- }
-
- if (alarmPosition >= 0) {
- mItemAdapter.findItemById(alarmId).expand();
- smoothScrollTo(alarmPosition);
- } else {
- // Trying to display a deleted alarm should only happen from a missed notification for
- // an alarm that has been marked deleted after use.
- SnackbarManager.show(Snackbar.make(mMainLayout, R.string
- .missed_alarm_has_been_deleted, Snackbar.LENGTH_LONG));
- }
- }
-
- @Override
- public void onLoaderReset(Loader<Cursor> cursorLoader) {
- }
-
- @Override
- public void setSmoothScrollStableId(long stableId) {
- mScrollToAlarmId = stableId;
- }
-
- @Override
- public void onFabClick(@NonNull ImageView fab) {
- mAlarmUpdateHandler.hideUndoBar();
- startCreatingAlarm();
- }
-
- @Override
- public void onUpdateFab(@NonNull ImageView fab) {
- fab.setVisibility(View.VISIBLE);
- fab.setImageResource(R.drawable.ic_add_white_24dp);
- fab.setContentDescription(fab.getResources().getString(R.string.button_alarms));
- }
-
- @Override
- public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
- left.setVisibility(View.INVISIBLE);
- right.setVisibility(View.INVISIBLE);
- }
-
- private void startCreatingAlarm() {
- // Clear the currently selected alarm.
- mAlarmTimeClickHandler.setSelectedAlarm(null);
- TimePickerDialogFragment.show(this);
- }
-
- @Override
- public void onTimeSet(TimePickerDialogFragment fragment, int hourOfDay, int minute) {
- mAlarmTimeClickHandler.onTimeSet(hourOfDay, minute);
- }
-
- public void removeItem(AlarmItemHolder itemHolder) {
- mItemAdapter.removeItem(itemHolder);
- }
-
- /**
- * Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls
- * the recyclerview or when the size/position of elements within the recyclerview changes.
- */
- private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
- implements View.OnLayoutChangeListener {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView));
- }
-
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView));
- }
- }
-
- /**
- * This runnable executes at midnight and refreshes the display of all alarms. Collapsed alarms
- * that do no repeat will have their "Tomorrow" strings updated to say "Today".
- */
- private final class MidnightRunnable implements Runnable {
- @Override
- public void run() {
- mItemAdapter.notifyDataSetChanged();
- }
- }
-}
diff --git a/src/com/android/deskclock/AlarmClockFragment.kt b/src/com/android/deskclock/AlarmClockFragment.kt
new file mode 100644
index 0000000..892794e
--- /dev/null
+++ b/src/com/android/deskclock/AlarmClockFragment.kt
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.database.Cursor
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnLayoutChangeListener
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.loader.app.LoaderManager.LoaderCallbacks
+import androidx.loader.content.Loader
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.ItemAdapter.OnItemChangedListener
+import com.android.deskclock.alarms.AlarmTimeClickHandler
+import com.android.deskclock.alarms.AlarmUpdateHandler
+import com.android.deskclock.alarms.ScrollHandler
+import com.android.deskclock.alarms.TimePickerDialogFragment
+import com.android.deskclock.alarms.dataadapter.AlarmItemHolder
+import com.android.deskclock.alarms.dataadapter.CollapsedAlarmViewHolder
+import com.android.deskclock.alarms.dataadapter.ExpandedAlarmViewHolder
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.widget.EmptyViewController
+import com.android.deskclock.widget.toast.SnackbarManager
+import com.android.deskclock.widget.toast.ToastManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import kotlin.math.max
+
+/**
+ * A fragment that displays a list of alarm time and allows interaction with them.
+ */
+class AlarmClockFragment : DeskClockFragment(UiDataModel.Tab.ALARMS),
+ LoaderCallbacks<Cursor>, ScrollHandler, TimePickerDialogFragment.OnTimeSetListener {
+ // Updates "Today/Tomorrow" in the UI when midnight passes.
+ private val mMidnightUpdater: Runnable = MidnightRunnable()
+
+ // Views
+ private lateinit var mMainLayout: ViewGroup
+ private lateinit var mRecyclerView: RecyclerView
+
+ // Data
+ private var mCursorLoader: Loader<*>? = null
+ private var mScrollToAlarmId = Alarm.INVALID_ID
+ private var mExpandedAlarmId = Alarm.INVALID_ID
+ private var mCurrentUpdateToken: Long = 0
+
+ // Controllers
+ private lateinit var mItemAdapter: ItemAdapter<AlarmItemHolder>
+ private lateinit var mAlarmUpdateHandler: AlarmUpdateHandler
+ private lateinit var mEmptyViewController: EmptyViewController
+ private lateinit var mAlarmTimeClickHandler: AlarmTimeClickHandler
+ private lateinit var mLayoutManager: LinearLayoutManager
+
+ override fun onCreate(savedState: Bundle?) {
+ super.onCreate(savedState)
+ mCursorLoader = loaderManager.initLoader(0, Bundle.EMPTY, this)
+ savedState?.let {
+ mExpandedAlarmId = it.getLong(KEY_EXPANDED_ID, Alarm.INVALID_ID)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedState: Bundle?
+ ): View? {
+ // Inflate the layout for this fragment
+ val v = inflater.inflate(R.layout.alarm_clock, container, false)
+ val context: Context = requireActivity()
+
+ mRecyclerView = v.findViewById<View>(R.id.alarms_recycler_view) as RecyclerView
+ mLayoutManager = object : LinearLayoutManager(context) {
+ override fun getExtraLayoutSpace(state: RecyclerView.State): Int {
+ val extraSpace: Int = super.getExtraLayoutSpace(state)
+ return if (state.willRunPredictiveAnimations()) {
+ max(getHeight(), extraSpace)
+ } else extraSpace
+ }
+ }
+ mRecyclerView.setLayoutManager(mLayoutManager)
+ mMainLayout = v.findViewById<View>(R.id.main) as ViewGroup
+ mAlarmUpdateHandler = AlarmUpdateHandler(context, this, mMainLayout)
+ val emptyView = v.findViewById<View>(R.id.alarms_empty_view) as TextView
+ val noAlarms: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_noalarms)
+ emptyView.setCompoundDrawablesWithIntrinsicBounds(null, noAlarms, null, null)
+ mEmptyViewController = EmptyViewController(mMainLayout, mRecyclerView, emptyView)
+ mAlarmTimeClickHandler = AlarmTimeClickHandler(this, savedState, mAlarmUpdateHandler, this)
+
+ mItemAdapter = ItemAdapter()
+ mItemAdapter.setHasStableIds()
+ mItemAdapter.withViewTypes(CollapsedAlarmViewHolder.Factory(inflater),
+ null, CollapsedAlarmViewHolder.VIEW_TYPE)
+ mItemAdapter.withViewTypes(ExpandedAlarmViewHolder.Factory(context),
+ null, ExpandedAlarmViewHolder.VIEW_TYPE)
+ mItemAdapter.setOnItemChangedListener(object : OnItemChangedListener {
+ override fun onItemChanged(holder: ItemHolder<*>) {
+ if ((holder as AlarmItemHolder).isExpanded) {
+ if (mExpandedAlarmId != holder.itemId) {
+ // Collapse the prior expanded alarm.
+ val aih = mItemAdapter.findItemById(mExpandedAlarmId)
+ aih?.collapse()
+ // Record the freshly expanded alarm.
+ mExpandedAlarmId = holder.itemId
+ val viewHolder: RecyclerView.ViewHolder? =
+ mRecyclerView.findViewHolderForItemId(mExpandedAlarmId)
+ viewHolder?.let {
+ smoothScrollTo(viewHolder.getAdapterPosition())
+ }
+ }
+ } else if (mExpandedAlarmId == holder.itemId) {
+ // The expanded alarm is now collapsed so update the tracking id.
+ mExpandedAlarmId = Alarm.INVALID_ID
+ }
+ }
+
+ override fun onItemChanged(holder: ItemHolder<*>, payload: Any) {
+ /* No additional work to do */
+ }
+ })
+ val scrollPositionWatcher = ScrollPositionWatcher()
+ mRecyclerView.addOnLayoutChangeListener(scrollPositionWatcher)
+ mRecyclerView.addOnScrollListener(scrollPositionWatcher)
+ mRecyclerView.setAdapter(mItemAdapter)
+ val itemAnimator = ItemAnimator()
+ itemAnimator.setChangeDuration(300L)
+ itemAnimator.setMoveDuration(300L)
+ mRecyclerView.setItemAnimator(itemAnimator)
+ return v
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ if (!isTabSelected) {
+ TimePickerDialogFragment.removeTimeEditDialog(parentFragmentManager)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // Schedule a runnable to update the "Today/Tomorrow" values displayed for non-repeating
+ // alarms when midnight passes.
+ UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
+
+ // Check if another app asked us to create a blank new alarm.
+ val intent = requireActivity().intent ?: return
+
+ if (intent.hasExtra(ALARM_CREATE_NEW_INTENT_EXTRA)) {
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+ if (intent.getBooleanExtra(ALARM_CREATE_NEW_INTENT_EXTRA, false)) {
+ // An external app asked us to create a blank alarm.
+ startCreatingAlarm()
+ }
+
+ // Remove the CREATE_NEW extra now that we've processed it.
+ intent.removeExtra(ALARM_CREATE_NEW_INTENT_EXTRA)
+ } else if (intent.hasExtra(SCROLL_TO_ALARM_INTENT_EXTRA)) {
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+ val alarmId = intent.getLongExtra(SCROLL_TO_ALARM_INTENT_EXTRA, Alarm.INVALID_ID)
+ if (alarmId != Alarm.INVALID_ID) {
+ setSmoothScrollStableId(alarmId)
+ if (mCursorLoader != null && mCursorLoader!!.isStarted) {
+ // We need to force a reload here to make sure we have the latest view
+ // of the data to scroll to.
+ mCursorLoader!!.forceLoad()
+ }
+ }
+
+ // Remove the SCROLL_TO_ALARM extra now that we've processed it.
+ intent.removeExtra(SCROLL_TO_ALARM_INTENT_EXTRA)
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
+
+ // When the user places the app in the background by pressing "home",
+ // dismiss the toast bar. However, since there is no way to determine if
+ // home was pressed, just dismiss any existing toast bar when restarting
+ // the app.
+ mAlarmUpdateHandler.hideUndoBar()
+ }
+
+ override fun smoothScrollTo(position: Int) {
+ mLayoutManager.scrollToPositionWithOffset(position, 0)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ mAlarmTimeClickHandler.saveInstance(outState)
+ outState.putLong(KEY_EXPANDED_ID, mExpandedAlarmId)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ ToastManager.cancelToast()
+ }
+
+ fun setLabel(alarm: Alarm, label: String?) {
+ alarm.label = label
+ mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+ }
+
+ override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
+ return Alarm.getAlarmsCursorLoader(requireActivity())
+ }
+
+ override fun onLoadFinished(cursorLoader: Loader<Cursor>, data: Cursor) {
+ val itemHolders: MutableList<AlarmItemHolder> = ArrayList(data.count)
+ data.moveToFirst()
+ while (!data.isAfterLast) {
+ val alarm = Alarm(data)
+ val alarmInstance = if (alarm.canPreemptivelyDismiss()) {
+ AlarmInstance(data, joinedTable = true)
+ } else {
+ null
+ }
+ val itemHolder = AlarmItemHolder(alarm, alarmInstance, mAlarmTimeClickHandler)
+ itemHolders.add(itemHolder)
+ data.moveToNext()
+ }
+ setAdapterItems(itemHolders, SystemClock.elapsedRealtime())
+ }
+
+ /**
+ * Updates the adapters items, deferring the update until the current animation is finished or
+ * if no animation is running then the listener will be automatically be invoked immediately.
+ *
+ * @param items the new list of [AlarmItemHolder] to use
+ * @param updateToken a monotonically increasing value used to preserve ordering of deferred
+ * updates
+ */
+ private fun setAdapterItems(items: List<AlarmItemHolder>, updateToken: Long) {
+ if (updateToken < mCurrentUpdateToken) {
+ LogUtils.v("Ignoring adapter update: %d < %d", updateToken, mCurrentUpdateToken)
+ return
+ }
+
+ if (mRecyclerView.getItemAnimator()!!.isRunning()) {
+ // RecyclerView is currently animating -> defer update.
+ mRecyclerView.getItemAnimator()!!.isRunning(
+ object : RecyclerView.ItemAnimator.ItemAnimatorFinishedListener {
+ override fun onAnimationsFinished() {
+ setAdapterItems(items, updateToken)
+ }
+ })
+ } else if (mRecyclerView.isComputingLayout()) {
+ // RecyclerView is currently computing a layout -> defer update.
+ mRecyclerView.post(Runnable { setAdapterItems(items, updateToken) })
+ } else {
+ mCurrentUpdateToken = updateToken
+ mItemAdapter.setItems(items)
+
+ // Show or hide the empty view as appropriate.
+ val noAlarms = items.isEmpty()
+ mEmptyViewController.setEmpty(noAlarms)
+ if (noAlarms) {
+ // Ensure the drop shadow is hidden when no alarms exist.
+ setTabScrolledToTop(true)
+ }
+
+ // Expand the correct alarm.
+ if (mExpandedAlarmId != Alarm.INVALID_ID) {
+ val aih = mItemAdapter.findItemById(mExpandedAlarmId)
+ if (aih != null) {
+ mAlarmTimeClickHandler.setSelectedAlarm(aih.item)
+ aih.expand()
+ } else {
+ mAlarmTimeClickHandler.setSelectedAlarm(null)
+ mExpandedAlarmId = Alarm.INVALID_ID
+ }
+ }
+
+ // Scroll to the selected alarm.
+ if (mScrollToAlarmId != Alarm.INVALID_ID) {
+ scrollToAlarm(mScrollToAlarmId)
+ setSmoothScrollStableId(Alarm.INVALID_ID)
+ }
+ }
+ }
+
+ /**
+ * @param alarmId identifies the alarm to be displayed
+ */
+ private fun scrollToAlarm(alarmId: Long) {
+ val alarmCount = mItemAdapter.itemCount
+ var alarmPosition = -1
+ for (i in 0 until alarmCount) {
+ val id = mItemAdapter.getItemId(i)
+ if (id == alarmId) {
+ alarmPosition = i
+ break
+ }
+ }
+
+ if (alarmPosition >= 0) {
+ mItemAdapter.findItemById(alarmId)?.expand()
+ smoothScrollTo(alarmPosition)
+ } else {
+ // Trying to display a deleted alarm should only happen from a missed notification for
+ // an alarm that has been marked deleted after use.
+ SnackbarManager.show(Snackbar.make(mMainLayout, R.string.missed_alarm_has_been_deleted,
+ Snackbar.LENGTH_LONG))
+ }
+ }
+
+ override fun onLoaderReset(cursorLoader: Loader<Cursor>) {
+ }
+
+ override fun setSmoothScrollStableId(stableId: Long) {
+ mScrollToAlarmId = stableId
+ }
+
+ override fun onFabClick(fab: ImageView) {
+ mAlarmUpdateHandler.hideUndoBar()
+ startCreatingAlarm()
+ }
+
+ override fun onUpdateFab(fab: ImageView) {
+ fab.visibility = View.VISIBLE
+ fab.setImageResource(R.drawable.ic_add_white_24dp)
+ fab.contentDescription = fab.resources.getString(R.string.button_alarms)
+ }
+
+ override fun onUpdateFabButtons(left: Button, right: Button) {
+ left.visibility = View.INVISIBLE
+ right.visibility = View.INVISIBLE
+ }
+
+ private fun startCreatingAlarm() {
+ // Clear the currently selected alarm.
+ mAlarmTimeClickHandler.setSelectedAlarm(null)
+ TimePickerDialogFragment.show(this)
+ }
+
+ override fun onTimeSet(fragment: TimePickerDialogFragment?, hourOfDay: Int, minute: Int) {
+ mAlarmTimeClickHandler.onTimeSet(hourOfDay, minute)
+ }
+
+ fun removeItem(itemHolder: AlarmItemHolder) {
+ mItemAdapter.removeItem(itemHolder)
+ }
+
+ /**
+ * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls
+ * the recyclerview or when the size/position of elements within the recyclerview changes.
+ */
+ private inner class ScrollPositionWatcher
+ : RecyclerView.OnScrollListener(), OnLayoutChangeListener {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
+ }
+
+ override fun onLayoutChange(
+ v: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ setTabScrolledToTop(Utils.isScrolledToTop(mRecyclerView))
+ }
+ }
+
+ /**
+ * This runnable executes at midnight and refreshes the display of all alarms. Collapsed alarms
+ * that do no repeat will have their "Tomorrow" strings updated to say "Today".
+ */
+ private inner class MidnightRunnable : Runnable {
+ override fun run() {
+ mItemAdapter.notifyDataSetChanged()
+ }
+ }
+
+ companion object {
+ // This extra is used when receiving an intent to create an alarm, but no alarm details
+ // have been passed in, so the alarm page should start the process of creating a new alarm.
+ const val ALARM_CREATE_NEW_INTENT_EXTRA = "deskclock.create.new"
+
+ // This extra is used when receiving an intent to scroll to specific alarm. If alarm
+ // can not be found, and toast message will pop up that the alarm has be deleted.
+ const val SCROLL_TO_ALARM_INTENT_EXTRA = "deskclock.scroll.to.alarm"
+
+ private const val KEY_EXPANDED_ID = "expandedId"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmInitReceiver.java b/src/com/android/deskclock/AlarmInitReceiver.java
deleted file mode 100644
index 8bd7cde..0000000
--- a/src/com/android/deskclock/AlarmInitReceiver.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.annotation.SuppressLint;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.PowerManager.WakeLock;
-
-import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.controller.Controller;
-import com.android.deskclock.data.DataModel;
-
-public class AlarmInitReceiver extends BroadcastReceiver {
-
- /**
- * When running on N devices, we're interested in the boot completed event that is sent while
- * the user is still locked, so that we can schedule alarms.
- */
- @SuppressLint("InlinedApi")
- private static final String ACTION_BOOT_COMPLETED = Utils.isNOrLater()
- ? Intent.ACTION_LOCKED_BOOT_COMPLETED : Intent.ACTION_BOOT_COMPLETED;
-
- /**
- * This receiver handles a variety of actions:
- *
- * <ul>
- * <li>Clean up backup data that was recently restored to this device on
- * ACTION_COMPLETE_RESTORE.</li>
- * <li>Reset timers and stopwatch on ACTION_BOOT_COMPLETED</li>
- * <li>Fix alarm states on ACTION_BOOT_COMPLETED, TIME_SET, TIMEZONE_CHANGED,
- * and LOCALE_CHANGED</li>
- * <li>Rebuild notifications on MY_PACKAGE_REPLACED</li>
- * </ul>
- */
- @Override
- public void onReceive(final Context context, Intent intent) {
- final String action = intent.getAction();
- LogUtils.i("AlarmInitReceiver " + action);
-
- final PendingResult result = goAsync();
- final WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
- wl.acquire();
-
- // We need to increment the global id out of the async task to prevent race conditions
- DataModel.getDataModel().updateGlobalIntentId();
-
- // Updates stopwatch and timer data after a device reboot so they are as accurate as
- // possible.
- if (ACTION_BOOT_COMPLETED.equals(action)) {
- DataModel.getDataModel().updateAfterReboot();
- // Stopwatch and timer data need to be updated on time change so the reboot
- // functionality works as expected.
- } else if (Intent.ACTION_TIME_CHANGED.equals(action)) {
- DataModel.getDataModel().updateAfterTimeSet();
- }
-
- // Update shortcuts so they exist for the user.
- if (Intent.ACTION_BOOT_COMPLETED.equals(action)
- || Intent.ACTION_LOCALE_CHANGED.equals(action)) {
- Controller.getController().updateShortcuts();
- }
-
- // Notifications are canceled by the system on application upgrade. This broadcast signals
- // that the new app is free to rebuild the notifications using the existing data.
- // Additionally on new app installs, make sure to enable shortcuts immediately as opposed
- // to waiting for system reboot.
- if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) {
- DataModel.getDataModel().updateAllNotifications();
- Controller.getController().updateShortcuts();
- }
-
- AsyncHandler.post(new Runnable() {
- @Override
- public void run() {
- try {
- // Process restored data if any exists
- if (!DeskClockBackupAgent.processRestoredData(context)) {
- // Update all the alarm instances on time change event
- AlarmStateManager.fixAlarmInstances(context);
- }
- } finally {
- result.finish();
- wl.release();
- LogUtils.v("AlarmInitReceiver finished");
- }
- }
- });
- }
-}
diff --git a/src/com/android/deskclock/AlarmInitReceiver.kt b/src/com/android/deskclock/AlarmInitReceiver.kt
new file mode 100644
index 0000000..548e939
--- /dev/null
+++ b/src/com/android/deskclock/AlarmInitReceiver.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+import com.android.deskclock.AlarmAlertWakeLock.createPartialWakeLock
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.controller.Controller
+import com.android.deskclock.data.DataModel
+
+class AlarmInitReceiver : BroadcastReceiver() {
+ /**
+ * This receiver handles a variety of actions:
+ *
+ * <ul>
+ * <li>Clean up backup data that was recently restored to this device on
+ * ACTION_COMPLETE_RESTORE.</li>
+ * <li>Reset timers and stopwatch on ACTION_BOOT_COMPLETED</li>
+ * <li>Fix alarm states on ACTION_BOOT_COMPLETED, TIME_SET, TIMEZONE_CHANGED,
+ * and LOCALE_CHANGED</li>
+ * <li>Rebuild notifications on MY_PACKAGE_REPLACED</li>
+ * </ul>
+ */
+ override fun onReceive(context: Context, intent: Intent) {
+ val action = intent.action
+ LogUtils.i("AlarmInitReceiver $action")
+
+ val result = goAsync()
+ val wl = createPartialWakeLock(context)
+ wl.acquire()
+
+ // We need to increment the global id out of the async task to prevent race conditions
+ DataModel.dataModel.updateGlobalIntentId()
+
+ // Updates stopwatch and timer data after a device reboot so they are as accurate as
+ // possible.
+ if (ACTION_BOOT_COMPLETED == action) {
+ DataModel.dataModel.updateAfterReboot()
+ // Stopwatch and timer data need to be updated on time change so the reboot
+ // functionality works as expected.
+ } else if (Intent.ACTION_TIME_CHANGED == action) {
+ DataModel.dataModel.updateAfterTimeSet()
+ }
+
+ // Update shortcuts so they exist for the user.
+ if (Intent.ACTION_BOOT_COMPLETED == action || Intent.ACTION_LOCALE_CHANGED == action) {
+ Controller.getController().updateShortcuts()
+ NotificationUtils.updateNotificationChannels(context)
+ }
+
+ // Notifications are canceled by the system on application upgrade. This broadcast signals
+ // that the new app is free to rebuild the notifications using the existing data.
+ // Additionally on new app installs, make sure to enable shortcuts immediately as opposed
+ // to waiting for system reboot.
+ if (Intent.ACTION_MY_PACKAGE_REPLACED == action) {
+ DataModel.dataModel.updateAllNotifications()
+ Controller.getController().updateShortcuts()
+ }
+
+ AsyncHandler.post {
+ try {
+ // Process restored data if any exists
+ if (!DeskClockBackupAgent.processRestoredData(context)) {
+ // Update all the alarm instances on time change event
+ AlarmStateManager.fixAlarmInstances(context)
+ }
+ } finally {
+ result.finish()
+ wl.release()
+ LogUtils.v("AlarmInitReceiver finished")
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * When running on N devices, we're interested in the boot completed event that is sent
+ * while the user is still locked, so that we can schedule alarms.
+ */
+ @SuppressLint("InlinedApi")
+ private val ACTION_BOOT_COMPLETED = if (Utils.isNOrLater) {
+ Intent.ACTION_LOCKED_BOOT_COMPLETED
+ } else {
+ Intent.ACTION_BOOT_COMPLETED
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmRecyclerView.java b/src/com/android/deskclock/AlarmRecyclerView.java
deleted file mode 100644
index 13bffcf..0000000
--- a/src/com/android/deskclock/AlarmRecyclerView.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.RecyclerView;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-/**
- * Thin wrapper around RecyclerView to prevent simultaneous layout passes, particularly during
- * animations.
- */
-public class AlarmRecyclerView extends RecyclerView {
-
- private boolean mIgnoreRequestLayout;
-
- public AlarmRecyclerView(Context context) {
- this(context, null);
- }
-
- public AlarmRecyclerView(Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public AlarmRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
- @Override
- public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
- // Disable scrolling/user action to prevent choppy animations.
- return rv.getItemAnimator().isRunning();
- }
- });
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- mIgnoreRequestLayout = true;
- super.onLayout(changed, left, top, right, bottom);
- mIgnoreRequestLayout = false;
- }
-
- @Override
- public void requestLayout() {
- if (!mIgnoreRequestLayout &&
- (getItemAnimator() == null || !getItemAnimator().isRunning())) {
- super.requestLayout();
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmRecyclerView.kt b/src/com/android/deskclock/AlarmRecyclerView.kt
new file mode 100644
index 0000000..02f6da8
--- /dev/null
+++ b/src/com/android/deskclock/AlarmRecyclerView.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.deskclock
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * Thin wrapper around RecyclerView to prevent simultaneous layout passes, particularly during
+ * animations.
+ */
+class AlarmRecyclerView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : RecyclerView(context, attrs, defStyle) {
+ private var mIgnoreRequestLayout = false
+
+ init {
+ addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {
+ override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
+ // Disable scrolling/user action to prevent choppy animations.
+ return rv.getItemAnimator()!!.isRunning()
+ }
+ })
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ mIgnoreRequestLayout = true
+ super.onLayout(changed, left, top, right, bottom)
+ mIgnoreRequestLayout = false
+ }
+
+ override fun requestLayout() {
+ if (!mIgnoreRequestLayout &&
+ (getItemAnimator() == null || !getItemAnimator()!!.isRunning())) {
+ super.requestLayout()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmSelectionActivity.java b/src/com/android/deskclock/AlarmSelectionActivity.java
deleted file mode 100644
index 4bd983f..0000000
--- a/src/com/android/deskclock/AlarmSelectionActivity.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock;
-
-import android.app.Activity;
-import android.app.ListActivity;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ListView;
-
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.widget.selector.AlarmSelection;
-import com.android.deskclock.widget.selector.AlarmSelectionAdapter;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-
-public class AlarmSelectionActivity extends ListActivity {
-
- /** Used by default when an invalid action provided. */
- private static final int ACTION_INVALID = -1;
-
- /** Action used to signify alarm should be dismissed on selection. */
- public static final int ACTION_DISMISS = 0;
-
- public static final String EXTRA_ACTION = "com.android.deskclock.EXTRA_ACTION";
- public static final String EXTRA_ALARMS = "com.android.deskclock.EXTRA_ALARMS";
-
- private final List<AlarmSelection> mSelections = new ArrayList<>();
-
- private int mAction;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // this activity is shown if:
- // a) no search mode was specified in which case we show all
- // enabled alarms
- // b) if search mode was next and there was multiple alarms firing next
- // (at the same time) then we only show those alarms firing at the same time
- // c) if search mode was time and there are multiple alarms with that time
- // then we only show those alarms with that time
-
- super.onCreate(savedInstanceState);
- setContentView(R.layout.selection_layout);
-
- final Button cancelButton = (Button) findViewById(R.id.cancel_button);
- cancelButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- finish();
- }
- });
-
- final Intent intent = getIntent();
- final Parcelable[] alarmsFromIntent = intent.getParcelableArrayExtra(EXTRA_ALARMS);
- mAction = intent.getIntExtra(EXTRA_ACTION, ACTION_INVALID);
-
- // reading alarms from intent
- // PickSelection is started only if there are more than 1 relevant alarm
- // so no need to check if alarmsFromIntent is empty
- for (Parcelable parcelable : alarmsFromIntent) {
- final Alarm alarm = (Alarm) parcelable;
-
- // filling mSelections that go into the UI picker list
- final String label = String.format(Locale.US, "%d %02d", alarm.hour, alarm.minutes);
- mSelections.add(new AlarmSelection(label, alarm));
- }
-
- setListAdapter(new AlarmSelectionAdapter(this, R.layout.alarm_row, mSelections));
- }
-
- @Override
- public void onListItemClick(ListView l, View v, int position, long id) {
- super.onListItemClick(l, v, position, id);
- // id corresponds to mSelections id because the view adapter used mSelections
- final AlarmSelection selection = mSelections.get((int) id);
- final Alarm alarm = selection.getAlarm();
- if (alarm != null) {
- new ProcessAlarmActionAsync(alarm, this, mAction).execute();
- }
- finish();
- }
-
- private static class ProcessAlarmActionAsync extends AsyncTask<Void, Void, Void> {
-
- private final Alarm mAlarm;
- private final Activity mActivity;
- private final int mAction;
-
- public ProcessAlarmActionAsync(Alarm alarm, Activity activity, int action) {
- mAlarm = alarm;
- mActivity = activity;
- mAction = action;
- }
-
- @Override
- protected Void doInBackground(Void... parameters) {
- switch (mAction) {
- case ACTION_DISMISS:
- HandleApiCalls.dismissAlarm(mAlarm, mActivity);
- break;
- case ACTION_INVALID:
- LogUtils.i("Invalid action");
- }
- return null;
- }
- }
-}
diff --git a/src/com/android/deskclock/AlarmSelectionActivity.kt b/src/com/android/deskclock/AlarmSelectionActivity.kt
new file mode 100644
index 0000000..e20e613
--- /dev/null
+++ b/src/com/android/deskclock/AlarmSelectionActivity.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.deskclock
+
+import android.app.Activity
+import android.app.ListActivity
+import android.os.AsyncTask
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+import android.widget.ListView
+
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.widget.selector.AlarmSelection
+import com.android.deskclock.widget.selector.AlarmSelectionAdapter
+
+import java.util.Locale
+
+class AlarmSelectionActivity : ListActivity() {
+ private val mSelections: MutableList<AlarmSelection> = ArrayList()
+ private var mAction = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ // this activity is shown if:
+ // a) no search mode was specified in which case we show all
+ // enabled alarms
+ // b) if search mode was next and there was multiple alarms firing next
+ // (at the same time) then we only show those alarms firing at the same time
+ // c) if search mode was time and there are multiple alarms with that time
+ // then we only show those alarms with that time
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.selection_layout)
+
+ val cancelButton = findViewById<View>(R.id.cancel_button) as Button
+ cancelButton.setOnClickListener { finish() }
+
+ val intent = intent
+ val alarmsFromIntent = intent.getParcelableArrayExtra(EXTRA_ALARMS)
+ mAction = intent.getIntExtra(EXTRA_ACTION, ACTION_INVALID)
+
+ // reading alarms from intent
+ // PickSelection is started only if there are more than 1 relevant alarm
+ // so no need to check if alarmsFromIntent is empty
+ for (parcelable in alarmsFromIntent!!) {
+ val alarm = parcelable as Alarm
+
+ // filling mSelections that go into the UI picker list
+ val label = String.format(Locale.US, "%d %02d", alarm.hour, alarm.minutes)
+ mSelections.add(AlarmSelection(label, alarm))
+ }
+
+ listAdapter = AlarmSelectionAdapter(this, R.layout.alarm_row, mSelections)
+ }
+
+ public override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
+ super.onListItemClick(l, v, position, id)
+ // id corresponds to mSelections id because the view adapter used mSelections
+ val selection = mSelections[id.toInt()]
+ val alarm: Alarm? = selection.alarm
+ alarm?.let {
+ ProcessAlarmActionAsync(it, this, mAction).execute()
+ }
+ finish()
+ }
+
+ // TODO(b/165664115) Replace deprecated AsyncTask calls
+ private class ProcessAlarmActionAsync(
+ private val mAlarm: Alarm,
+ private val mActivity: Activity,
+ private val mAction: Int
+ ) : AsyncTask<Void?, Void?, Void?>() {
+ override fun doInBackground(vararg parameters: Void?): Void? {
+ when (mAction) {
+ ACTION_DISMISS -> HandleApiCalls.dismissAlarm(mAlarm, mActivity)
+ ACTION_INVALID -> LogUtils.i("Invalid action")
+ }
+ return null
+ }
+ }
+
+ companion object {
+ /** Used by default when an invalid action provided. */
+ private const val ACTION_INVALID = -1
+
+ /** Action used to signify alarm should be dismissed on selection. */
+ const val ACTION_DISMISS = 0
+
+ const val EXTRA_ACTION = "com.android.deskclock.EXTRA_ACTION"
+ const val EXTRA_ALARMS = "com.android.deskclock.EXTRA_ALARMS"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AlarmUtils.java b/src/com/android/deskclock/AlarmUtils.java
deleted file mode 100644
index db60ace..0000000
--- a/src/com/android/deskclock/AlarmUtils.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import androidx.annotation.VisibleForTesting;
-import com.google.android.material.snackbar.Snackbar;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.view.View;
-import android.widget.Toast;
-
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.widget.toast.SnackbarManager;
-import com.android.deskclock.widget.toast.ToastManager;
-
-import java.util.Calendar;
-import java.util.Locale;
-
-/**
- * Static utility methods for Alarms.
- */
-public class AlarmUtils {
-
- public static String getFormattedTime(Context context, Calendar time) {
- final String skeleton = DateFormat.is24HourFormat(context) ? "EHm" : "Ehma";
- final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
- return (String) DateFormat.format(pattern, time);
- }
-
- public static String getFormattedTime(Context context, long timeInMillis) {
- final Calendar c = Calendar.getInstance();
- c.setTimeInMillis(timeInMillis);
- return getFormattedTime(context, c);
- }
-
- public static String getAlarmText(Context context, AlarmInstance instance,
- boolean includeLabel) {
- String alarmTimeStr = getFormattedTime(context, instance.getAlarmTime());
- return (instance.mLabel.isEmpty() || !includeLabel)
- ? alarmTimeStr
- : alarmTimeStr + " - " + instance.mLabel;
- }
-
- /**
- * format "Alarm set for 2 days, 7 hours, and 53 minutes from now."
- */
- @VisibleForTesting
- static String formatElapsedTimeUntilAlarm(Context context, long delta) {
- // If the alarm will ring within 60 seconds, just report "less than a minute."
- final String[] formats = context.getResources().getStringArray(R.array.alarm_set);
- if (delta < DateUtils.MINUTE_IN_MILLIS) {
- return formats[0];
- }
-
- // Otherwise, format the remaining time until the alarm rings.
-
- // Round delta upwards to the nearest whole minute. (e.g. 7m 58s -> 8m)
- final long remainder = delta % DateUtils.MINUTE_IN_MILLIS;
- delta += remainder == 0 ? 0 : (DateUtils.MINUTE_IN_MILLIS - remainder);
-
- int hours = (int) delta / (1000 * 60 * 60);
- final int minutes = (int) delta / (1000 * 60) % 60;
- final int days = hours / 24;
- hours = hours % 24;
-
- String daySeq = Utils.getNumberFormattedQuantityString(context, R.plurals.days, days);
- String minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, minutes);
- String hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, hours);
-
- final boolean showDays = days > 0;
- final boolean showHours = hours > 0;
- final boolean showMinutes = minutes > 0;
-
- // Compute the index of the most appropriate time format based on the time delta.
- final int index = (showDays ? 1 : 0) | (showHours ? 2 : 0) | (showMinutes ? 4 : 0);
-
- return String.format(formats[index], daySeq, hourSeq, minSeq);
- }
-
- public static void popAlarmSetToast(Context context, long alarmTime) {
- final long alarmTimeDelta = alarmTime - System.currentTimeMillis();
- final String text = formatElapsedTimeUntilAlarm(context, alarmTimeDelta);
- Toast toast = Toast.makeText(context, text, Toast.LENGTH_LONG);
- ToastManager.setToast(toast);
- toast.show();
- }
-
- public static void popAlarmSetSnackbar(View snackbarAnchor, long alarmTime) {
- final long alarmTimeDelta = alarmTime - System.currentTimeMillis();
- final String text = formatElapsedTimeUntilAlarm(
- snackbarAnchor.getContext(), alarmTimeDelta);
- SnackbarManager.show(Snackbar.make(snackbarAnchor, text, Snackbar.LENGTH_SHORT));
- snackbarAnchor.announceForAccessibility(text);
- }
-}
diff --git a/src/com/android/deskclock/AlarmUtils.kt b/src/com/android/deskclock/AlarmUtils.kt
new file mode 100644
index 0000000..d17fbd1
--- /dev/null
+++ b/src/com/android/deskclock/AlarmUtils.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.deskclock
+
+import android.content.Context
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+import android.view.View
+import android.widget.Toast
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.widget.toast.SnackbarManager
+import com.android.deskclock.widget.toast.ToastManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import java.util.Calendar
+import java.util.Locale
+
+/**
+ * Static utility methods for Alarms.
+ */
+object AlarmUtils {
+ @JvmStatic
+ fun getFormattedTime(context: Context, time: Calendar): String {
+ val skeleton = if (DateFormat.is24HourFormat(context)) "EHm" else "Ehma"
+ val pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton)
+ return DateFormat.format(pattern, time) as String
+ }
+
+ @JvmStatic
+ fun getFormattedTime(context: Context, timeInMillis: Long): String {
+ val c = Calendar.getInstance()
+ c.timeInMillis = timeInMillis
+ return getFormattedTime(context, c)
+ }
+
+ @JvmStatic
+ fun getAlarmText(context: Context, instance: AlarmInstance, includeLabel: Boolean): String {
+ val alarmTimeStr: String = getFormattedTime(context, instance.alarmTime)
+ return if (instance.mLabel!!.isEmpty() || !includeLabel) {
+ alarmTimeStr
+ } else {
+ alarmTimeStr + " - " + instance.mLabel
+ }
+ }
+
+ /**
+ * format "Alarm set for 2 days, 7 hours, and 53 minutes from now."
+ */
+ @VisibleForTesting
+ fun formatElapsedTimeUntilAlarm(context: Context, delta: Long): String {
+ // If the alarm will ring within 60 seconds, just report "less than a minute."
+ var variableDelta = delta
+ val formats = context.resources.getStringArray(R.array.alarm_set)
+ if (variableDelta < DateUtils.MINUTE_IN_MILLIS) {
+ return formats[0]
+ }
+
+ // Otherwise, format the remaining time until the alarm rings.
+
+ // Round delta upwards to the nearest whole minute. (e.g. 7m 58s -> 8m)
+ val remainder = variableDelta % DateUtils.MINUTE_IN_MILLIS
+ variableDelta += if (remainder == 0L) 0 else DateUtils.MINUTE_IN_MILLIS - remainder
+ var hours = variableDelta.toInt() / (1000 * 60 * 60)
+ val minutes = variableDelta.toInt() / (1000 * 60) % 60
+ val days = hours / 24
+ hours %= 24
+
+ val daySeq = Utils.getNumberFormattedQuantityString(context, R.plurals.days, days)
+ val minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, minutes)
+ val hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, hours)
+
+ val showDays = days > 0
+ val showHours = hours > 0
+ val showMinutes = minutes > 0
+
+ // Compute the index of the most appropriate time format based on the time delta.
+ val index = ((if (showDays) 1 else 0)
+ or (if (showHours) 2 else 0)
+ or (if (showMinutes) 4 else 0))
+
+ return String.format(formats[index], daySeq, hourSeq, minSeq)
+ }
+
+ @JvmStatic
+ fun popAlarmSetToast(context: Context, alarmTime: Long) {
+ val alarmTimeDelta = alarmTime - System.currentTimeMillis()
+ val text = formatElapsedTimeUntilAlarm(context, alarmTimeDelta)
+ val toast = Toast.makeText(context, text, Toast.LENGTH_LONG)
+ ToastManager.setToast(toast)
+ toast.show()
+ }
+
+ @JvmStatic
+ fun popAlarmSetSnackbar(snackbarAnchor: View, alarmTime: Long) {
+ val alarmTimeDelta = alarmTime - System.currentTimeMillis()
+ val text = formatElapsedTimeUntilAlarm(
+ snackbarAnchor.context, alarmTimeDelta)
+ SnackbarManager.show(Snackbar.make(snackbarAnchor, text, Snackbar.LENGTH_SHORT))
+ snackbarAnchor.announceForAccessibility(text)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AnalogClock.java b/src/com/android/deskclock/AnalogClock.java
deleted file mode 100644
index e5f9c24..0000000
--- a/src/com/android/deskclock/AnalogClock.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import androidx.appcompat.widget.AppCompatImageView;
-import android.text.format.DateFormat;
-import android.util.AttributeSet;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.TimeZone;
-
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-
-/**
- * This widget display an analog clock with two hands for hours and minutes.
- */
-public class AnalogClock extends FrameLayout {
-
- private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
- final String tz = intent.getStringExtra(Intent.EXTRA_TIMEZONE);
- mTime = Calendar.getInstance(TimeZone.getTimeZone(tz));
- }
- onTimeChanged();
- }
- };
-
- private final Runnable mClockTick = new Runnable() {
- @Override
- public void run() {
- onTimeChanged();
-
- if (mEnableSeconds) {
- final long now = System.currentTimeMillis();
- final long delay = SECOND_IN_MILLIS - now % SECOND_IN_MILLIS;
- postDelayed(this, delay);
- }
- }
- };
-
- private final ImageView mHourHand;
- private final ImageView mMinuteHand;
- private final ImageView mSecondHand;
-
- private Calendar mTime;
- private String mDescFormat;
- private TimeZone mTimeZone;
- private boolean mEnableSeconds = true;
-
- public AnalogClock(Context context) {
- this(context, null /* attrs */);
- }
-
- public AnalogClock(Context context, AttributeSet attrs) {
- this(context, attrs, 0 /* defStyleAttr */);
- }
-
- public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- mTime = Calendar.getInstance();
- mDescFormat = ((SimpleDateFormat) DateFormat.getTimeFormat(context)).toLocalizedPattern();
-
- // Must call mutate on these instances, otherwise the drawables will blur, because they're
- // sharing their size characteristics with the (smaller) world cities analog clocks.
- final ImageView dial = new AppCompatImageView(context);
- dial.setImageResource(R.drawable.clock_analog_dial);
- dial.getDrawable().mutate();
- addView(dial);
-
- mHourHand = new AppCompatImageView(context);
- mHourHand.setImageResource(R.drawable.clock_analog_hour);
- mHourHand.getDrawable().mutate();
- addView(mHourHand);
-
- mMinuteHand = new AppCompatImageView(context);
- mMinuteHand.setImageResource(R.drawable.clock_analog_minute);
- mMinuteHand.getDrawable().mutate();
- addView(mMinuteHand);
-
- mSecondHand = new AppCompatImageView(context);
- mSecondHand.setImageResource(R.drawable.clock_analog_second);
- mSecondHand.getDrawable().mutate();
- addView(mSecondHand);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- final IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_TIME_TICK);
- filter.addAction(Intent.ACTION_TIME_CHANGED);
- filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
- getContext().registerReceiver(mIntentReceiver, filter);
-
- // Refresh the calendar instance since the time zone may have changed while the receiver
- // wasn't registered.
- mTime = Calendar.getInstance(mTimeZone != null ? mTimeZone : TimeZone.getDefault());
- onTimeChanged();
-
- // Tick every second.
- if (mEnableSeconds) {
- mClockTick.run();
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- getContext().unregisterReceiver(mIntentReceiver);
- removeCallbacks(mClockTick);
- }
-
- private void onTimeChanged() {
- mTime.setTimeInMillis(System.currentTimeMillis());
- final float hourAngle = mTime.get(Calendar.HOUR) * 30f;
- mHourHand.setRotation(hourAngle);
- final float minuteAngle = mTime.get(Calendar.MINUTE) * 6f;
- mMinuteHand.setRotation(minuteAngle);
- if (mEnableSeconds) {
- final float secondAngle = mTime.get(Calendar.SECOND) * 6f;
- mSecondHand.setRotation(secondAngle);
- }
- setContentDescription(DateFormat.format(mDescFormat, mTime));
- invalidate();
- }
-
- public void setTimeZone(String id) {
- mTimeZone = TimeZone.getTimeZone(id);
- mTime.setTimeZone(mTimeZone);
- onTimeChanged();
- }
-
- public void enableSeconds(boolean enable) {
- mEnableSeconds = enable;
- if (mEnableSeconds) {
- mSecondHand.setVisibility(VISIBLE);
- mClockTick.run();
- } else {
- mSecondHand.setVisibility(GONE);
- }
- }
-}
diff --git a/src/com/android/deskclock/AnalogClock.kt b/src/com/android/deskclock/AnalogClock.kt
new file mode 100644
index 0000000..2627ff5
--- /dev/null
+++ b/src/com/android/deskclock/AnalogClock.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import androidx.appcompat.widget.AppCompatImageView
+
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.TimeZone
+
+/**
+ * This widget display an analog clock with two hands for hours and minutes.
+ */
+class AnalogClock @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+ private val mIntentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED == intent.action) {
+ val tz = intent.getStringExtra("time-zone")
+ mTime = Calendar.getInstance(TimeZone.getTimeZone(tz))
+ }
+ onTimeChanged()
+ }
+ }
+
+ private val mClockTick: Runnable = object : Runnable {
+ override fun run() {
+ onTimeChanged()
+
+ if (mEnableSeconds) {
+ val now = System.currentTimeMillis()
+ val delay = DateUtils.SECOND_IN_MILLIS - now % DateUtils.SECOND_IN_MILLIS
+ postDelayed(this, delay)
+ }
+ }
+ }
+
+ private val mHourHand: ImageView
+ private val mMinuteHand: ImageView
+ private val mSecondHand: ImageView
+
+ private var mTime = Calendar.getInstance()
+ private val mDescFormat =
+ (DateFormat.getTimeFormat(context) as SimpleDateFormat).toLocalizedPattern()
+ private var mTimeZone: TimeZone? = null
+ private var mEnableSeconds = true
+
+ init {
+ // Must call mutate on these instances, otherwise the drawables will blur, because they're
+ // sharing their size characteristics with the (smaller) world cities analog clocks.
+ val dial: ImageView = AppCompatImageView(context)
+ dial.setImageResource(R.drawable.clock_analog_dial)
+ dial.drawable.mutate()
+ addView(dial)
+
+ mHourHand = AppCompatImageView(context)
+ mHourHand.setImageResource(R.drawable.clock_analog_hour)
+ mHourHand.drawable.mutate()
+ addView(mHourHand)
+
+ mMinuteHand = AppCompatImageView(context)
+ mMinuteHand.setImageResource(R.drawable.clock_analog_minute)
+ mMinuteHand.drawable.mutate()
+ addView(mMinuteHand)
+
+ mSecondHand = AppCompatImageView(context)
+ mSecondHand.setImageResource(R.drawable.clock_analog_second)
+ mSecondHand.drawable.mutate()
+ addView(mSecondHand)
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+
+ val filter = IntentFilter()
+ filter.addAction(Intent.ACTION_TIME_TICK)
+ filter.addAction(Intent.ACTION_TIME_CHANGED)
+ filter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
+ context.registerReceiver(mIntentReceiver, filter)
+
+ // Refresh the calendar instance since the time zone may have changed while the receiver
+ // wasn't registered.
+ mTime = Calendar.getInstance(mTimeZone ?: TimeZone.getDefault())
+ onTimeChanged()
+
+ // Tick every second.
+ if (mEnableSeconds) {
+ mClockTick.run()
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+
+ context.unregisterReceiver(mIntentReceiver)
+ removeCallbacks(mClockTick)
+ }
+
+ private fun onTimeChanged() {
+ mTime.timeInMillis = System.currentTimeMillis()
+ val hourAngle = mTime[Calendar.HOUR] * 30f
+ mHourHand.rotation = hourAngle
+ val minuteAngle = mTime[Calendar.MINUTE] * 6f
+ mMinuteHand.rotation = minuteAngle
+ if (mEnableSeconds) {
+ val secondAngle = mTime[Calendar.SECOND] * 6f
+ mSecondHand.rotation = secondAngle
+ }
+ contentDescription = DateFormat.format(mDescFormat, mTime)
+ invalidate()
+ }
+
+ fun setTimeZone(id: String) {
+ mTimeZone = TimeZone.getTimeZone(id)
+ mTime.timeZone = mTimeZone!!
+ onTimeChanged()
+ }
+
+ fun enableSeconds(enable: Boolean) {
+ mEnableSeconds = enable
+ if (mEnableSeconds) {
+ mSecondHand.visibility = View.VISIBLE
+ mClockTick.run()
+ } else {
+ mSecondHand.visibility = View.GONE
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AnimatorUtils.java b/src/com/android/deskclock/AnimatorUtils.java
deleted file mode 100644
index 9b3cc6f..0000000
--- a/src/com/android/deskclock/AnimatorUtils.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.animation.Animator;
-import android.animation.ArgbEvaluator;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.TypeEvaluator;
-import android.animation.ValueAnimator;
-import android.graphics.Rect;
-import android.graphics.drawable.Animatable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
-import android.util.Property;
-import android.view.View;
-import android.view.animation.Interpolator;
-import android.widget.ImageView;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-public class AnimatorUtils {
-
- public static final Interpolator DECELERATE_ACCELERATE_INTERPOLATOR = new Interpolator() {
- @Override
- public float getInterpolation(float x) {
- return 0.5f + 4.0f * (x - 0.5f) * (x - 0.5f) * (x - 0.5f);
- }
- };
-
- public static final Interpolator INTERPOLATOR_FAST_OUT_SLOW_IN =
- new FastOutSlowInInterpolator();
-
- public static final Property<View, Integer> BACKGROUND_ALPHA =
- new Property<View, Integer>(Integer.class, "background.alpha") {
- @Override
- public Integer get(View view) {
- Drawable background = view.getBackground();
- if (background instanceof LayerDrawable
- && ((LayerDrawable) background).getNumberOfLayers() > 0) {
- background = ((LayerDrawable) background).getDrawable(0);
- }
- return background.getAlpha();
- }
-
- @Override
- public void set(View view, Integer value) {
- setBackgroundAlpha(view, value);
- }
- };
-
- /**
- * Sets the alpha of the top layer's drawable (of the background) only, if the background is a
- * layer drawable, to ensure that the other layers (i.e., the selectable item background, and
- * therefore the touch feedback RippleDrawable) are not affected.
- *
- * @param view the affected view
- * @param value the alpha value (0-255)
- */
- public static void setBackgroundAlpha(View view, Integer value) {
- Drawable background = view.getBackground();
- if (background instanceof LayerDrawable
- && ((LayerDrawable) background).getNumberOfLayers() > 0) {
- background = ((LayerDrawable) background).getDrawable(0);
- }
- background.setAlpha(value);
- }
-
- public static final Property<ImageView, Integer> DRAWABLE_ALPHA =
- new Property<ImageView, Integer>(Integer.class, "drawable.alpha") {
- @Override
- public Integer get(ImageView view) {
- return view.getDrawable().getAlpha();
- }
-
- @Override
- public void set(ImageView view, Integer value) {
- view.getDrawable().setAlpha(value);
- }
- };
-
- public static final Property<ImageView, Integer> DRAWABLE_TINT =
- new Property<ImageView, Integer>(Integer.class, "drawable.tint") {
- @Override
- public Integer get(ImageView view) {
- return null;
- }
-
- @Override
- public void set(ImageView view, Integer value) {
- // Ensure the drawable is wrapped using DrawableCompat.
- final Drawable drawable = view.getDrawable();
- final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
- if (wrappedDrawable != drawable) {
- view.setImageDrawable(wrappedDrawable);
- }
- // Set the new tint value via DrawableCompat.
- DrawableCompat.setTint(wrappedDrawable, value);
- }
- };
-
- @SuppressWarnings("unchecked")
- public static final TypeEvaluator<Integer> ARGB_EVALUATOR = new ArgbEvaluator();
-
- private static Method sAnimateValue;
- private static boolean sTryAnimateValue = true;
-
- public static void setAnimatedFraction(ValueAnimator animator, float fraction) {
- if (Utils.isLMR1OrLater()) {
- animator.setCurrentFraction(fraction);
- return;
- }
-
- if (sTryAnimateValue) {
- // try to set the animated fraction directly so that it isn't affected by the
- // internal animator scale or time (b/17938711)
- try {
- if (sAnimateValue == null) {
- sAnimateValue = ValueAnimator.class
- .getDeclaredMethod("animateValue", float.class);
- sAnimateValue.setAccessible(true);
- }
-
- sAnimateValue.invoke(animator, fraction);
- return;
- } catch (NoSuchMethodException | InvocationTargetException
- | IllegalAccessException e) {
- // something went wrong, don't try that again
- LogUtils.e("Unable to use animateValue directly", e);
- sTryAnimateValue = false;
- }
- }
-
- // if that doesn't work then just fall back to setting the current play time
- animator.setCurrentPlayTime(Math.round(fraction * animator.getDuration()));
- }
-
- public static void reverse(ValueAnimator... animators) {
- for (ValueAnimator animator : animators) {
- final float fraction = animator.getAnimatedFraction();
- if (fraction > 0.0f) {
- animator.reverse();
- setAnimatedFraction(animator, 1.0f - fraction);
- }
- }
- }
-
- public static void cancel(ValueAnimator... animators) {
- for (ValueAnimator animator : animators) {
- animator.cancel();
- }
- }
-
- public static ValueAnimator getScaleAnimator(View view, float... values) {
- return ObjectAnimator.ofPropertyValuesHolder(view,
- PropertyValuesHolder.ofFloat(View.SCALE_X, values),
- PropertyValuesHolder.ofFloat(View.SCALE_Y, values));
- }
-
- public static ValueAnimator getAlphaAnimator(View view, float... values) {
- return ObjectAnimator.ofFloat(view, View.ALPHA, values);
- }
-
- public static final Property<View, Integer> VIEW_LEFT =
- new Property<View, Integer>(Integer.class, "left") {
- @Override
- public Integer get(View view) {
- return view.getLeft();
- }
-
- @Override
- public void set(View view, Integer left) {
- view.setLeft(left);
- }
- };
-
- public static final Property<View, Integer> VIEW_TOP =
- new Property<View, Integer>(Integer.class, "top") {
- @Override
- public Integer get(View view) {
- return view.getTop();
- }
-
- @Override
- public void set(View view, Integer top) {
- view.setTop(top);
- }
- };
-
- public static final Property<View, Integer> VIEW_BOTTOM =
- new Property<View, Integer>(Integer.class, "bottom") {
- @Override
- public Integer get(View view) {
- return view.getBottom();
- }
-
- @Override
- public void set(View view, Integer bottom) {
- view.setBottom(bottom);
- }
- };
-
- public static final Property<View, Integer> VIEW_RIGHT =
- new Property<View, Integer>(Integer.class, "right") {
- @Override
- public Integer get(View view) {
- return view.getRight();
- }
-
- @Override
- public void set(View view, Integer right) {
- view.setRight(right);
- }
- };
-
- /**
- * @param target the view to be morphed
- * @param from the bounds of the {@code target} before animating
- * @param to the bounds of the {@code target} after animating
- * @return an animator that morphs the {@code target} between the {@code from} bounds and the
- * {@code to} bounds. Note that it is the *content* bounds that matter here, so padding
- * insets contributed by the background are subtracted from the views when computing the
- * {@code target} bounds.
- */
- public static Animator getBoundsAnimator(View target, View from, View to) {
- // Fetch the content insets for the views. Content bounds are what matter, not total bounds.
- final Rect targetInsets = new Rect();
- target.getBackground().getPadding(targetInsets);
- final Rect fromInsets = new Rect();
- from.getBackground().getPadding(fromInsets);
- final Rect toInsets = new Rect();
- to.getBackground().getPadding(toInsets);
-
- // Before animating, the content bounds of target must match the content bounds of from.
- final int startLeft = from.getLeft() - fromInsets.left + targetInsets.left;
- final int startTop = from.getTop() - fromInsets.top + targetInsets.top;
- final int startRight = from.getRight() - fromInsets.right + targetInsets.right;
- final int startBottom = from.getBottom() - fromInsets.bottom + targetInsets.bottom;
-
- // After animating, the content bounds of target must match the content bounds of to.
- final int endLeft = to.getLeft() - toInsets.left + targetInsets.left;
- final int endTop = to.getTop() - toInsets.top + targetInsets.top;
- final int endRight = to.getRight() - toInsets.right + targetInsets.right;
- final int endBottom = to.getBottom() - toInsets.bottom + targetInsets.bottom;
-
- return getBoundsAnimator(target, startLeft, startTop, startRight, startBottom, endLeft,
- endTop, endRight, endBottom);
- }
-
- /**
- * Returns an animator that animates the bounds of a single view.
- */
- public static Animator getBoundsAnimator(View view, int fromLeft, int fromTop, int fromRight,
- int fromBottom, int toLeft, int toTop, int toRight, int toBottom) {
- view.setLeft(fromLeft);
- view.setTop(fromTop);
- view.setRight(fromRight);
- view.setBottom(fromBottom);
-
- return ObjectAnimator.ofPropertyValuesHolder(view,
- PropertyValuesHolder.ofInt(VIEW_LEFT, toLeft),
- PropertyValuesHolder.ofInt(VIEW_TOP, toTop),
- PropertyValuesHolder.ofInt(VIEW_RIGHT, toRight),
- PropertyValuesHolder.ofInt(VIEW_BOTTOM, toBottom));
- }
-
- public static void startDrawableAnimation(ImageView view) {
- final Drawable d = view.getDrawable();
- if (d instanceof Animatable) {
- ((Animatable) d).start();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AnimatorUtils.kt b/src/com/android/deskclock/AnimatorUtils.kt
new file mode 100644
index 0000000..3172738
--- /dev/null
+++ b/src/com/android/deskclock/AnimatorUtils.kt
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.animation.Animator
+import android.animation.ArgbEvaluator
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.animation.TypeEvaluator
+import android.animation.ValueAnimator
+import android.graphics.Rect
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.util.Property
+import android.view.View
+import android.view.animation.Interpolator
+import android.widget.ImageView
+import androidx.core.graphics.drawable.DrawableCompat
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator
+
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.Method
+
+import kotlin.math.roundToLong
+
+object AnimatorUtils {
+ @JvmField
+ val DECELERATE_ACCELERATE_INTERPOLATOR =
+ Interpolator { x -> 0.5f + 4.0f * (x - 0.5f) * (x - 0.5f) * (x - 0.5f) }
+
+ @JvmField
+ val INTERPOLATOR_FAST_OUT_SLOW_IN: Interpolator = FastOutSlowInInterpolator()
+
+ @JvmField
+ val BACKGROUND_ALPHA: Property<View, Int> =
+ object : Property<View, Int>(Int::class.java, "background.alpha") {
+ override fun get(view: View): Int {
+ var background = view.background
+ if (background is LayerDrawable &&
+ background.numberOfLayers > 0) {
+ background = background.getDrawable(0)
+ }
+ return background.alpha
+ }
+
+ override fun set(view: View, value: Int) {
+ setBackgroundAlpha(view, value)
+ }
+ }
+
+ /**
+ * Sets the alpha of the top layer's drawable (of the background) only, if the background is a
+ * layer drawable, to ensure that the other layers (i.e., the selectable item background, and
+ * therefore the touch feedback RippleDrawable) are not affected.
+ *
+ * @param view the affected view
+ * @param value the alpha value (0-255)
+ */
+ @JvmStatic
+ fun setBackgroundAlpha(view: View, value: Int?) {
+ var background = view.background
+ if (background is LayerDrawable &&
+ background.numberOfLayers > 0) {
+ background = background.getDrawable(0)
+ }
+ background.alpha = value!!
+ }
+
+ @JvmField
+ val DRAWABLE_ALPHA: Property<ImageView, Int> =
+ object : Property<ImageView, Int>(Int::class.java, "drawable.alpha") {
+ override fun get(view: ImageView): Int {
+ return view.drawable.alpha
+ }
+
+ override fun set(view: ImageView, value: Int) {
+ view.drawable.alpha = value
+ }
+ }
+
+ @JvmField
+ val DRAWABLE_TINT: Property<ImageView, Int> =
+ object : Property<ImageView, Int>(Int::class.java, "drawable.tint") {
+ override fun get(view: ImageView): Int? {
+ return null
+ }
+
+ override fun set(view: ImageView, value: Int) {
+ // Ensure the drawable is wrapped using DrawableCompat.
+ val drawable = view.drawable
+ val wrappedDrawable: Drawable = DrawableCompat.wrap(drawable)
+ if (wrappedDrawable !== drawable) {
+ view.setImageDrawable(wrappedDrawable)
+ }
+ // Set the new tint value via DrawableCompat.
+ DrawableCompat.setTint(wrappedDrawable, value)
+ }
+ }
+
+ @JvmField
+ val ARGB_EVALUATOR: TypeEvaluator<Int> = ArgbEvaluator() as TypeEvaluator<Int>
+
+ private var sAnimateValue: Method? = null
+
+ private var sTryAnimateValue = true
+
+ @JvmStatic
+ fun setAnimatedFraction(animator: ValueAnimator, fraction: Float) {
+ if (Utils.isLMR1OrLater) {
+ animator.setCurrentFraction(fraction)
+ return
+ }
+
+ if (sTryAnimateValue) {
+ // try to set the animated fraction directly so that it isn't affected by the
+ // internal animator scale or time (b/17938711)
+ try {
+ if (sAnimateValue == null) {
+ sAnimateValue = ValueAnimator::class.java
+ .getDeclaredMethod("animateValue", Float::class.javaPrimitiveType)
+ sAnimateValue!!.isAccessible = true
+ }
+
+ sAnimateValue!!.invoke(animator, fraction)
+ return
+ } catch (e: NoSuchMethodException) {
+ // something went wrong, don't try that again
+ LogUtils.e("Unable to use animateValue directly", e)
+ sTryAnimateValue = false
+ } catch (e: InvocationTargetException) {
+ LogUtils.e("Unable to use animateValue directly", e)
+ sTryAnimateValue = false
+ } catch (e: IllegalAccessException) {
+ LogUtils.e("Unable to use animateValue directly", e)
+ sTryAnimateValue = false
+ }
+ }
+
+ // if that doesn't work then just fall back to setting the current play time
+ animator.currentPlayTime = (fraction * animator.duration).roundToLong()
+ }
+
+ @JvmStatic
+ fun reverse(vararg animators: ValueAnimator) {
+ for (animator in animators) {
+ val fraction = animator.animatedFraction
+ if (fraction > 0.0f) {
+ animator.reverse()
+ setAnimatedFraction(animator, 1.0f - fraction)
+ }
+ }
+ }
+
+ fun cancel(vararg animators: ValueAnimator) {
+ for (animator in animators) {
+ animator.cancel()
+ }
+ }
+
+ @JvmStatic
+ fun getScaleAnimator(view: View?, vararg values: Float): ValueAnimator {
+ return ObjectAnimator.ofPropertyValuesHolder(view,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, *values),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, *values))
+ }
+
+ @JvmStatic
+ fun getAlphaAnimator(view: View, vararg values: Float): ValueAnimator {
+ return ObjectAnimator.ofFloat(view, View.ALPHA, *values)
+ }
+
+ val VIEW_LEFT: Property<View, Int> = object : Property<View, Int>(Int::class.java, "left") {
+ override fun get(view: View): Int {
+ return view.left
+ }
+
+ override fun set(view: View, left: Int) {
+ view.left = left
+ }
+ }
+
+ val VIEW_TOP: Property<View, Int> = object : Property<View, Int>(Int::class.java, "top") {
+ override fun get(view: View): Int {
+ return view.top
+ }
+
+ override fun set(view: View, top: Int) {
+ view.top = top
+ }
+ }
+
+ val VIEW_BOTTOM: Property<View, Int> = object : Property<View, Int>(Int::class.java, "bottom") {
+ override fun get(view: View): Int {
+ return view.bottom
+ }
+
+ override fun set(view: View, bottom: Int) {
+ view.bottom = bottom
+ }
+ }
+
+ val VIEW_RIGHT: Property<View, Int> = object : Property<View, Int>(Int::class.java, "right") {
+ override fun get(view: View): Int {
+ return view.right
+ }
+
+ override fun set(view: View, right: Int) {
+ view.right = right
+ }
+ }
+
+ /**
+ * @param target the view to be morphed
+ * @param from the bounds of the `target` before animating
+ * @param to the bounds of the `target` after animating
+ * @return an animator that morphs the `target` between the `from` bounds and the
+ * `to` bounds. Note that it is the *content* bounds that matter here, so padding
+ * insets contributed by the background are subtracted from the views when computing the
+ * `target` bounds.
+ */
+ fun getBoundsAnimator(target: View, from: View, to: View): Animator {
+ // Fetch the content insets for the views. Content bounds are what matter, not total bounds.
+ val targetInsets = Rect()
+ target.background.getPadding(targetInsets)
+ val fromInsets = Rect()
+ from.background.getPadding(fromInsets)
+ val toInsets = Rect()
+ to.background.getPadding(toInsets)
+
+ // Before animating, the content bounds of target must match the content bounds of from.
+ val startLeft = from.left - fromInsets.left + targetInsets.left
+ val startTop = from.top - fromInsets.top + targetInsets.top
+ val startRight = from.right - fromInsets.right + targetInsets.right
+ val startBottom = from.bottom - fromInsets.bottom + targetInsets.bottom
+
+ // After animating, the content bounds of target must match the content bounds of to.
+ val endLeft = to.left - toInsets.left + targetInsets.left
+ val endTop = to.top - toInsets.top + targetInsets.top
+ val endRight = to.right - toInsets.right + targetInsets.right
+ val endBottom = to.bottom - toInsets.bottom + targetInsets.bottom
+
+ return getBoundsAnimator(target, startLeft, startTop, startRight, startBottom, endLeft,
+ endTop, endRight, endBottom)
+ }
+
+ /**
+ * Returns an animator that animates the bounds of a single view.
+ */
+ @JvmStatic
+ fun getBoundsAnimator(
+ view: View,
+ fromLeft: Int,
+ fromTop: Int,
+ fromRight: Int,
+ fromBottom: Int,
+ toLeft: Int,
+ toTop: Int,
+ toRight: Int,
+ toBottom: Int
+ ): Animator {
+ view.left = fromLeft
+ view.top = fromTop
+ view.right = fromRight
+ view.bottom = fromBottom
+
+ return ObjectAnimator.ofPropertyValuesHolder(view,
+ PropertyValuesHolder.ofInt(VIEW_LEFT, toLeft),
+ PropertyValuesHolder.ofInt(VIEW_TOP, toTop),
+ PropertyValuesHolder.ofInt(VIEW_RIGHT, toRight),
+ PropertyValuesHolder.ofInt(VIEW_BOTTOM, toBottom))
+ }
+
+ @JvmStatic
+ fun startDrawableAnimation(view: ImageView) {
+ val d = view.drawable
+ if (d is Animatable) {
+ d.start()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AsyncHandler.java b/src/com/android/deskclock/AsyncHandler.java
deleted file mode 100644
index 026d74e..0000000
--- a/src/com/android/deskclock/AsyncHandler.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-
-/**
- * Helper class for managing the background thread used to perform io operations
- * and handle async broadcasts.
- */
-public final class AsyncHandler {
- private static final HandlerThread sHandlerThread = new HandlerThread("AsyncHandler");
- private static final Handler sHandler;
-
- static {
- sHandlerThread.start();
- sHandler = new Handler(sHandlerThread.getLooper());
- }
-
- public static void post(Runnable r) {
- sHandler.post(r);
- }
-
- private AsyncHandler() {}
-}
diff --git a/src/com/android/deskclock/AsyncHandler.kt b/src/com/android/deskclock/AsyncHandler.kt
new file mode 100644
index 0000000..903c7b4
--- /dev/null
+++ b/src/com/android/deskclock/AsyncHandler.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.os.Handler
+import android.os.HandlerThread
+
+/**
+ * Helper class for managing the background thread used to perform io operations
+ * and handle async broadcasts.
+ */
+object AsyncHandler {
+ private val sHandlerThread = HandlerThread("AsyncHandler")
+ private val sHandler: Handler
+
+ init {
+ sHandlerThread.start()
+ sHandler = Handler(sHandlerThread.looper)
+ }
+
+ fun post(r: () -> Unit) {
+ sHandler.post(r)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AsyncRingtonePlayer.java b/src/com/android/deskclock/AsyncRingtonePlayer.java
deleted file mode 100644
index afb46d6..0000000
--- a/src/com/android/deskclock/AsyncRingtonePlayer.java
+++ /dev/null
@@ -1,639 +0,0 @@
-package com.android.deskclock;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.media.AudioManager;
-import android.media.MediaPlayer;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.telephony.TelephonyManager;
-
-import java.io.IOException;
-import java.lang.reflect.Method;
-
-import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
-import static android.media.AudioManager.STREAM_ALARM;
-
-/**
- * <p>This class controls playback of ringtones. Uses {@link Ringtone} or {@link MediaPlayer} in a
- * dedicated thread so that this class can be called from the main thread. Consequently, problems
- * controlling the ringtone do not cause ANRs in the main thread of the application.</p>
- *
- * <p>This class also serves a second purpose. It accomplishes alarm ringtone playback using two
- * different mechanisms depending on the underlying platform.</p>
- *
- * <ul>
- * <li>Prior to the M platform release, ringtone playback is accomplished using
- * {@link MediaPlayer}. android.permission.READ_EXTERNAL_STORAGE is required to play custom
- * ringtones located on the SD card using this mechanism. {@link MediaPlayer} allows clients to
- * adjust the volume of the stream and specify that the stream should be looped.</li>
- *
- * <li>Starting with the M platform release, ringtone playback is accomplished using
- * {@link Ringtone}. android.permission.READ_EXTERNAL_STORAGE is <strong>NOT</strong> required
- * to play custom ringtones located on the SD card using this mechanism. {@link Ringtone} allows
- * clients to adjust the volume of the stream and specify that the stream should be looped but
- * those methods are marked @hide in M and thus invoked using reflection. Consequently, revoking
- * the android.permission.READ_EXTERNAL_STORAGE permission has no effect on playback in M+.</li>
- * </ul>
- *
- * <p>If either the {@link Ringtone} or {@link MediaPlayer} fails to play the requested audio, an
- * {@link #getFallbackRingtoneUri in-app fallback} is used because playing <strong>some</strong>
- * sort of noise is always preferable to remaining silent.</p>
- */
-public final class AsyncRingtonePlayer {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AsyncRingtonePlayer");
-
- // Volume suggested by media team for in-call alarms.
- private static final float IN_CALL_VOLUME = 0.125f;
-
- // Message codes used with the ringtone thread.
- private static final int EVENT_PLAY = 1;
- private static final int EVENT_STOP = 2;
- private static final int EVENT_VOLUME = 3;
- private static final String RINGTONE_URI_KEY = "RINGTONE_URI_KEY";
- private static final String CRESCENDO_DURATION_KEY = "CRESCENDO_DURATION_KEY";
-
- /** Handler running on the ringtone thread. */
- private Handler mHandler;
-
- /** {@link MediaPlayerPlaybackDelegate} on pre M; {@link RingtonePlaybackDelegate} on M+ */
- private PlaybackDelegate mPlaybackDelegate;
-
- /** The context. */
- private final Context mContext;
-
- public AsyncRingtonePlayer(Context context) {
- mContext = context;
- }
-
- /** Plays the ringtone. */
- public void play(Uri ringtoneUri, long crescendoDuration) {
- LOGGER.d("Posting play.");
- postMessage(EVENT_PLAY, ringtoneUri, crescendoDuration, 0);
- }
-
- /** Stops playing the ringtone. */
- public void stop() {
- LOGGER.d("Posting stop.");
- postMessage(EVENT_STOP, null, 0, 0);
- }
-
- /** Schedules an adjustment of the playback volume 50ms in the future. */
- private void scheduleVolumeAdjustment() {
- LOGGER.v("Adjusting volume.");
-
- // Ensure we never have more than one volume adjustment queued.
- mHandler.removeMessages(EVENT_VOLUME);
-
- // Queue the next volume adjustment.
- postMessage(EVENT_VOLUME, null, 0, 50);
- }
-
- /**
- * Posts a message to the ringtone-thread handler.
- *
- * @param messageCode the message to post
- * @param ringtoneUri the ringtone in question, if any
- * @param crescendoDuration the length of time, in ms, over which to crescendo the ringtone
- * @param delayMillis the amount of time to delay sending the message, if any
- */
- private void postMessage(int messageCode, Uri ringtoneUri, long crescendoDuration,
- long delayMillis) {
- synchronized (this) {
- if (mHandler == null) {
- mHandler = getNewHandler();
- }
-
- final Message message = mHandler.obtainMessage(messageCode);
- if (ringtoneUri != null) {
- final Bundle bundle = new Bundle();
- bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri);
- bundle.putLong(CRESCENDO_DURATION_KEY, crescendoDuration);
- message.setData(bundle);
- }
-
- mHandler.sendMessageDelayed(message, delayMillis);
- }
- }
-
- /**
- * Creates a new ringtone Handler running in its own thread.
- */
- @SuppressLint("HandlerLeak")
- private Handler getNewHandler() {
- final HandlerThread thread = new HandlerThread("ringtone-player");
- thread.start();
-
- return new Handler(thread.getLooper()) {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case EVENT_PLAY:
- final Bundle data = msg.getData();
- final Uri ringtoneUri = data.getParcelable(RINGTONE_URI_KEY);
- final long crescendoDuration = data.getLong(CRESCENDO_DURATION_KEY);
- if (getPlaybackDelegate().play(mContext, ringtoneUri, crescendoDuration)) {
- scheduleVolumeAdjustment();
- }
- break;
- case EVENT_STOP:
- getPlaybackDelegate().stop(mContext);
- break;
- case EVENT_VOLUME:
- if (getPlaybackDelegate().adjustVolume(mContext)) {
- scheduleVolumeAdjustment();
- }
- break;
- }
- }
- };
- }
-
- /**
- * @return <code>true</code> iff the device is currently in a telephone call
- */
- private static boolean isInTelephoneCall(Context context) {
- final TelephonyManager tm = (TelephonyManager)
- context.getSystemService(Context.TELEPHONY_SERVICE);
- return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE;
- }
-
- /**
- * @return Uri of the ringtone to play when the user is in a telephone call
- */
- private static Uri getInCallRingtoneUri(Context context) {
- return Utils.getResourceUri(context, R.raw.alarm_expire);
- }
-
- /**
- * @return Uri of the ringtone to play when the chosen ringtone fails to play
- */
- private static Uri getFallbackRingtoneUri(Context context) {
- return Utils.getResourceUri(context, R.raw.alarm_expire);
- }
-
- /**
- * Check if the executing thread is the one dedicated to controlling the ringtone playback.
- */
- private void checkAsyncRingtonePlayerThread() {
- if (Looper.myLooper() != mHandler.getLooper()) {
- LOGGER.e("Must be on the AsyncRingtonePlayer thread!",
- new IllegalStateException());
- }
- }
-
- /**
- * @param currentTime current time of the device
- * @param stopTime time at which the crescendo finishes
- * @param duration length of time over which the crescendo occurs
- * @return the scalar volume value that produces a linear increase in volume (in decibels)
- */
- private static float computeVolume(long currentTime, long stopTime, long duration) {
- // Compute the percentage of the crescendo that has completed.
- final float elapsedCrescendoTime = stopTime - currentTime;
- final float fractionComplete = 1 - (elapsedCrescendoTime / duration);
-
- // Use the fraction to compute a target decibel between -40dB (near silent) and 0dB (max).
- final float gain = (fractionComplete * 40) - 40;
-
- // Convert the target gain (in decibels) into the corresponding volume scalar.
- final float volume = (float) Math.pow(10f, gain/20f);
-
- LOGGER.v("Ringtone crescendo %,.2f%% complete (scalar: %f, volume: %f dB)",
- fractionComplete * 100, volume, gain);
-
- return volume;
- }
-
- /**
- * @return the platform-specific playback delegate to use to play the ringtone
- */
- private PlaybackDelegate getPlaybackDelegate() {
- checkAsyncRingtonePlayerThread();
-
- if (mPlaybackDelegate == null) {
- if (Utils.isMOrLater()) {
- // Use the newer Ringtone-based playback delegate because it does not require
- // any permissions to read from the SD card. (M+)
- mPlaybackDelegate = new RingtonePlaybackDelegate();
- } else {
- // Fall back to the older MediaPlayer-based playback delegate because it is the only
- // way to force the looping of the ringtone before M. (pre M)
- mPlaybackDelegate = new MediaPlayerPlaybackDelegate();
- }
- }
-
- return mPlaybackDelegate;
- }
-
- /**
- * This interface abstracts away the differences between playing ringtones via {@link Ringtone}
- * vs {@link MediaPlayer}.
- */
- private interface PlaybackDelegate {
- /**
- * @return {@code true} iff a {@link #adjustVolume volume adjustment} should be scheduled
- */
- boolean play(Context context, Uri ringtoneUri, long crescendoDuration);
-
- /**
- * Stop any ongoing ringtone playback.
- */
- void stop(Context context);
-
- /**
- * @return {@code true} iff another volume adjustment should be scheduled
- */
- boolean adjustVolume(Context context);
- }
-
- /**
- * Loops playback of a ringtone using {@link MediaPlayer}.
- */
- private class MediaPlayerPlaybackDelegate implements PlaybackDelegate {
-
- /** The audio focus manager. Only used by the ringtone thread. */
- private AudioManager mAudioManager;
-
- /** Non-{@code null} while playing a ringtone; {@code null} otherwise. */
- private MediaPlayer mMediaPlayer;
-
- /** The duration over which to increase the volume. */
- private long mCrescendoDuration = 0;
-
- /** The time at which the crescendo shall cease; 0 if no crescendo is present. */
- private long mCrescendoStopTime = 0;
-
- /**
- * Starts the actual playback of the ringtone. Executes on ringtone-thread.
- */
- @Override
- public boolean play(final Context context, Uri ringtoneUri, long crescendoDuration) {
- checkAsyncRingtonePlayerThread();
- mCrescendoDuration = crescendoDuration;
-
- LOGGER.i("Play ringtone via android.media.MediaPlayer.");
-
- if (mAudioManager == null) {
- mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- }
-
- final boolean inTelephoneCall = isInTelephoneCall(context);
- Uri alarmNoise = inTelephoneCall ? getInCallRingtoneUri(context) : ringtoneUri;
- // Fall back to the system default alarm if the database does not have an alarm stored.
- if (alarmNoise == null) {
- alarmNoise = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
- LOGGER.v("Using default alarm: " + alarmNoise.toString());
- }
-
- mMediaPlayer = new MediaPlayer();
- mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
- @Override
- public boolean onError(MediaPlayer mp, int what, int extra) {
- LOGGER.e("Error occurred while playing audio. Stopping AlarmKlaxon.");
- stop(context);
- return true;
- }
- });
-
- try {
- // If alarmNoise is a custom ringtone on the sd card the app must be granted
- // android.permission.READ_EXTERNAL_STORAGE. Pre-M this is ensured at app
- // installation time. M+, this permission can be revoked by the user any time.
- mMediaPlayer.setDataSource(context, alarmNoise);
-
- return startPlayback(inTelephoneCall);
- } catch (Throwable t) {
- LOGGER.e("Using the fallback ringtone, could not play " + alarmNoise, t);
- // The alarmNoise may be on the sd card which could be busy right now.
- // Use the fallback ringtone.
- try {
- // Must reset the media player to clear the error state.
- mMediaPlayer.reset();
- mMediaPlayer.setDataSource(context, getFallbackRingtoneUri(context));
- return startPlayback(inTelephoneCall);
- } catch (Throwable t2) {
- // At this point we just don't play anything.
- LOGGER.e("Failed to play fallback ringtone", t2);
- }
- }
-
- return false;
- }
-
- /**
- * Prepare the MediaPlayer for playback if the alarm stream is not muted, then start the
- * playback.
- *
- * @param inTelephoneCall {@code true} if there is currently an active telephone call
- * @return {@code true} if a crescendo has started and future volume adjustments are
- * required to advance the crescendo effect
- */
- private boolean startPlayback(boolean inTelephoneCall)
- throws IOException {
- // Do not play alarms if stream volume is 0 (typically because ringer mode is silent).
- if (mAudioManager.getStreamVolume(STREAM_ALARM) == 0) {
- return false;
- }
-
- // Indicate the ringtone should be played via the alarm stream.
- if (Utils.isLOrLater()) {
- mMediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build());
- }
-
- // Check if we are in a call. If we are, use the in-call alarm resource at a low volume
- // to not disrupt the call.
- boolean scheduleVolumeAdjustment = false;
- if (inTelephoneCall) {
- LOGGER.v("Using the in-call alarm");
- mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME);
- } else if (mCrescendoDuration > 0) {
- mMediaPlayer.setVolume(0, 0);
-
- // Compute the time at which the crescendo will stop.
- mCrescendoStopTime = Utils.now() + mCrescendoDuration;
- scheduleVolumeAdjustment = true;
- }
-
- mMediaPlayer.setAudioStreamType(STREAM_ALARM);
- mMediaPlayer.setLooping(true);
- mMediaPlayer.prepare();
- mAudioManager.requestAudioFocus(null, STREAM_ALARM, AUDIOFOCUS_GAIN_TRANSIENT);
- mMediaPlayer.start();
-
- return scheduleVolumeAdjustment;
- }
-
- /**
- * Stops the playback of the ringtone. Executes on the ringtone-thread.
- */
- @Override
- public void stop(Context context) {
- checkAsyncRingtonePlayerThread();
-
- LOGGER.i("Stop ringtone via android.media.MediaPlayer.");
-
- mCrescendoDuration = 0;
- mCrescendoStopTime = 0;
-
- // Stop audio playing
- if (mMediaPlayer != null) {
- mMediaPlayer.stop();
- mMediaPlayer.release();
- mMediaPlayer = null;
- }
-
- if (mAudioManager != null) {
- mAudioManager.abandonAudioFocus(null);
- }
- }
-
- /**
- * Adjusts the volume of the ringtone being played to create a crescendo effect.
- */
- @Override
- public boolean adjustVolume(Context context) {
- checkAsyncRingtonePlayerThread();
-
- // If media player is absent or not playing, ignore volume adjustment.
- if (mMediaPlayer == null || !mMediaPlayer.isPlaying()) {
- mCrescendoDuration = 0;
- mCrescendoStopTime = 0;
- return false;
- }
-
- // If the crescendo is complete set the volume to the maximum; we're done.
- final long currentTime = Utils.now();
- if (currentTime > mCrescendoStopTime) {
- mCrescendoDuration = 0;
- mCrescendoStopTime = 0;
- mMediaPlayer.setVolume(1, 1);
- return false;
- }
-
- // The current volume of the crescendo is the percentage of the crescendo completed.
- final float volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration);
- mMediaPlayer.setVolume(volume, volume);
- LOGGER.i("MediaPlayer volume set to " + volume);
-
- // Schedule the next volume bump in the crescendo.
- return true;
- }
- }
-
- /**
- * Loops playback of a ringtone using {@link Ringtone}.
- */
- private class RingtonePlaybackDelegate implements PlaybackDelegate {
-
- /** The audio focus manager. Only used by the ringtone thread. */
- private AudioManager mAudioManager;
-
- /** The current ringtone. Only used by the ringtone thread. */
- private Ringtone mRingtone;
-
- /** The method to adjust playback volume; cannot be null. */
- private Method mSetVolumeMethod;
-
- /** The method to adjust playback looping; cannot be null. */
- private Method mSetLoopingMethod;
-
- /** The duration over which to increase the volume. */
- private long mCrescendoDuration = 0;
-
- /** The time at which the crescendo shall cease; 0 if no crescendo is present. */
- private long mCrescendoStopTime = 0;
-
- private RingtonePlaybackDelegate() {
- try {
- mSetVolumeMethod = Ringtone.class.getDeclaredMethod("setVolume", float.class);
- } catch (NoSuchMethodException nsme) {
- LOGGER.e("Unable to locate method: Ringtone.setVolume(float).", nsme);
- }
-
- try {
- mSetLoopingMethod = Ringtone.class.getDeclaredMethod("setLooping", boolean.class);
- } catch (NoSuchMethodException nsme) {
- LOGGER.e("Unable to locate method: Ringtone.setLooping(boolean).", nsme);
- }
- }
-
- /**
- * Starts the actual playback of the ringtone. Executes on ringtone-thread.
- */
- @Override
- public boolean play(Context context, Uri ringtoneUri, long crescendoDuration) {
- checkAsyncRingtonePlayerThread();
- mCrescendoDuration = crescendoDuration;
-
- LOGGER.i("Play ringtone via android.media.Ringtone.");
-
- if (mAudioManager == null) {
- mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- }
-
- final boolean inTelephoneCall = isInTelephoneCall(context);
- if (inTelephoneCall) {
- ringtoneUri = getInCallRingtoneUri(context);
- }
-
- // Attempt to fetch the specified ringtone.
- mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
-
- if (mRingtone == null) {
- // Fall back to the system default ringtone.
- ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
- mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
- }
-
- // Attempt to enable looping the ringtone.
- try {
- mSetLoopingMethod.invoke(mRingtone, true);
- } catch (Exception e) {
- LOGGER.e("Unable to turn looping on for android.media.Ringtone", e);
-
- // Fall back to the default ringtone if looping could not be enabled.
- // (Default alarm ringtone most likely has looping tags set within the .ogg file)
- mRingtone = null;
- }
-
- // If no ringtone exists at this point there isn't much recourse.
- if (mRingtone == null) {
- LOGGER.i("Unable to locate alarm ringtone, using internal fallback ringtone.");
- ringtoneUri = getFallbackRingtoneUri(context);
- mRingtone = RingtoneManager.getRingtone(context, ringtoneUri);
- }
-
- try {
- return startPlayback(inTelephoneCall);
- } catch (Throwable t) {
- LOGGER.e("Using the fallback ringtone, could not play " + ringtoneUri, t);
- // Recover from any/all playback errors by attempting to play the fallback tone.
- mRingtone = RingtoneManager.getRingtone(context, getFallbackRingtoneUri(context));
- try {
- return startPlayback(inTelephoneCall);
- } catch (Throwable t2) {
- // At this point we just don't play anything.
- LOGGER.e("Failed to play fallback ringtone", t2);
- }
- }
-
- return false;
- }
-
- /**
- * Prepare the Ringtone for playback, then start the playback.
- *
- * @param inTelephoneCall {@code true} if there is currently an active telephone call
- * @return {@code true} if a crescendo has started and future volume adjustments are
- * required to advance the crescendo effect
- */
- private boolean startPlayback(boolean inTelephoneCall) {
- // Indicate the ringtone should be played via the alarm stream.
- if (Utils.isLOrLater()) {
- mRingtone.setAudioAttributes(new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build());
- }
-
- // Attempt to adjust the ringtone volume if the user is in a telephone call.
- boolean scheduleVolumeAdjustment = false;
- if (inTelephoneCall) {
- LOGGER.v("Using the in-call alarm");
- setRingtoneVolume(IN_CALL_VOLUME);
- } else if (mCrescendoDuration > 0) {
- setRingtoneVolume(0);
-
- // Compute the time at which the crescendo will stop.
- mCrescendoStopTime = Utils.now() + mCrescendoDuration;
- scheduleVolumeAdjustment = true;
- }
-
- mAudioManager.requestAudioFocus(null, STREAM_ALARM, AUDIOFOCUS_GAIN_TRANSIENT);
-
- mRingtone.play();
-
- return scheduleVolumeAdjustment;
- }
-
- /**
- * Sets the volume of the ringtone.
- *
- * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
- * corresponds to no attenuation being applied.
- */
- private void setRingtoneVolume(float volume) {
- try {
- mSetVolumeMethod.invoke(mRingtone, volume);
- } catch (Exception e) {
- LOGGER.e("Unable to set volume for android.media.Ringtone", e);
- }
- }
-
- /**
- * Stops the playback of the ringtone. Executes on the ringtone-thread.
- */
- @Override
- public void stop(Context context) {
- checkAsyncRingtonePlayerThread();
-
- LOGGER.i("Stop ringtone via android.media.Ringtone.");
-
- mCrescendoDuration = 0;
- mCrescendoStopTime = 0;
-
- if (mRingtone != null && mRingtone.isPlaying()) {
- LOGGER.d("Ringtone.stop() invoked.");
- mRingtone.stop();
- }
-
- mRingtone = null;
-
- if (mAudioManager != null) {
- mAudioManager.abandonAudioFocus(null);
- }
- }
-
- /**
- * Adjusts the volume of the ringtone being played to create a crescendo effect.
- */
- @Override
- public boolean adjustVolume(Context context) {
- checkAsyncRingtonePlayerThread();
-
- // If ringtone is absent or not playing, ignore volume adjustment.
- if (mRingtone == null || !mRingtone.isPlaying()) {
- mCrescendoDuration = 0;
- mCrescendoStopTime = 0;
- return false;
- }
-
- // If the crescendo is complete set the volume to the maximum; we're done.
- final long currentTime = Utils.now();
- if (currentTime > mCrescendoStopTime) {
- mCrescendoDuration = 0;
- mCrescendoStopTime = 0;
- setRingtoneVolume(1);
- return false;
- }
-
- final float volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration);
- setRingtoneVolume(volume);
-
- // Schedule the next volume bump in the crescendo.
- return true;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/AsyncRingtonePlayer.kt b/src/com/android/deskclock/AsyncRingtonePlayer.kt
new file mode 100644
index 0000000..20a3f33
--- /dev/null
+++ b/src/com/android/deskclock/AsyncRingtonePlayer.kt
@@ -0,0 +1,638 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Message
+import android.telephony.TelephonyManager
+
+import java.io.IOException
+import java.lang.reflect.Method
+
+import kotlin.math.pow
+
+/**
+ *
+ * This class controls playback of ringtones. Uses [Ringtone] or [MediaPlayer] in a
+ * dedicated thread so that this class can be called from the main thread. Consequently, problems
+ * controlling the ringtone do not cause ANRs in the main thread of the application.
+ *
+ * This class also serves a second purpose. It accomplishes alarm ringtone playback using two
+ * different mechanisms depending on the underlying platform.
+ *
+ * Prior to the M platform release, ringtone playback is accomplished using
+ * [MediaPlayer]. android.permission.READ_EXTERNAL_STORAGE is required to play custom
+ * ringtones located on the SD card using this mechanism. [MediaPlayer] allows clients to
+ * adjust the volume of the stream and specify that the stream should be looped.
+ *
+ * Starting with the M platform release, ringtone playback is accomplished using
+ * [Ringtone]. android.permission.READ_EXTERNAL_STORAGE is **NOT** required
+ * to play custom ringtones located on the SD card using this mechanism. [Ringtone] allows
+ * clients to adjust the volume of the stream and specify that the stream should be looped but
+ * those methods are marked @hide in M and thus invoked using reflection. Consequently, revoking
+ * the android.permission.READ_EXTERNAL_STORAGE permission has no effect on playback in M+.
+ *
+ * If either the [Ringtone] or [MediaPlayer] fails to play the requested audio, an
+ * [in-app fallback][.getFallbackRingtoneUri] is used because playing **some**
+ * sort of noise is always preferable to remaining silent.
+ */
+class AsyncRingtonePlayer(private val mContext: Context) {
+ /** Handler running on the ringtone thread. */
+ private var mHandler: Handler? = null
+
+ /** [MediaPlayerPlaybackDelegate] on pre M; [RingtonePlaybackDelegate] on M+ */
+ private var mPlaybackDelegate: PlaybackDelegate? = null
+
+ /** Plays the ringtone. */
+ fun play(ringtoneUri: Uri?, crescendoDuration: Long) {
+ LOGGER.d("Posting play.")
+ postMessage(EVENT_PLAY, ringtoneUri, crescendoDuration, 0)
+ }
+
+ /** Stops playing the ringtone. */
+ fun stop() {
+ LOGGER.d("Posting stop.")
+ postMessage(EVENT_STOP, null, 0, 0)
+ }
+
+ /** Schedules an adjustment of the playback volume 50ms in the future. */
+ private fun scheduleVolumeAdjustment() {
+ LOGGER.v("Adjusting volume.")
+
+ // Ensure we never have more than one volume adjustment queued.
+ mHandler!!.removeMessages(EVENT_VOLUME)
+
+ // Queue the next volume adjustment.
+ postMessage(EVENT_VOLUME, null, 0, 50)
+ }
+
+ /**
+ * Posts a message to the ringtone-thread handler.
+ *
+ * @param messageCode the message to post
+ * @param ringtoneUri the ringtone in question, if any
+ * @param crescendoDuration the length of time, in ms, over which to crescendo the ringtone
+ * @param delayMillis the amount of time to delay sending the message, if any
+ */
+ private fun postMessage(
+ messageCode: Int,
+ ringtoneUri: Uri?,
+ crescendoDuration: Long,
+ delayMillis: Long
+ ) {
+ synchronized(this) {
+ if (mHandler == null) {
+ mHandler = getNewHandler()
+ }
+
+ val message = mHandler!!.obtainMessage(messageCode)
+ if (ringtoneUri != null) {
+ val bundle = Bundle()
+ bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri)
+ bundle.putLong(CRESCENDO_DURATION_KEY, crescendoDuration)
+ message.data = bundle
+ }
+
+ mHandler!!.sendMessageDelayed(message, delayMillis)
+ }
+ }
+
+ /**
+ * Creates a new ringtone Handler running in its own thread.
+ */
+ @SuppressLint("HandlerLeak")
+ private fun getNewHandler(): Handler {
+ val thread = HandlerThread("ringtone-player")
+ thread.start()
+
+ return object : Handler(thread.looper) {
+ override fun handleMessage(msg: Message) {
+ when (msg.what) {
+ EVENT_PLAY -> {
+ val data = msg.data
+ val ringtoneUri = data.getParcelable<Uri>(RINGTONE_URI_KEY)
+ val crescendoDuration = data.getLong(CRESCENDO_DURATION_KEY)
+ if (playbackDelegate.play(mContext, ringtoneUri, crescendoDuration)) {
+ scheduleVolumeAdjustment()
+ }
+ }
+ EVENT_STOP -> playbackDelegate.stop(mContext)
+ EVENT_VOLUME -> if (playbackDelegate.adjustVolume(mContext)) {
+ scheduleVolumeAdjustment()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if the executing thread is the one dedicated to controlling the ringtone playback.
+ */
+ private fun checkAsyncRingtonePlayerThread() {
+ if (Looper.myLooper() != mHandler!!.looper) {
+ LOGGER.e("Must be on the AsyncRingtonePlayer thread!",
+ IllegalStateException())
+ }
+ }
+
+ /**
+ * @return the platform-specific playback delegate to use to play the ringtone
+ */
+ private val playbackDelegate: PlaybackDelegate
+ get() {
+ checkAsyncRingtonePlayerThread()
+ if (mPlaybackDelegate == null) {
+ mPlaybackDelegate = if (Utils.isMOrLater) {
+ // Use the newer Ringtone-based playback delegate because it does not require
+ // any permissions to read from the SD card. (M+)
+ RingtonePlaybackDelegate()
+ } else {
+ // Fall back to the older MediaPlayer-based playback delegate because it is the
+ // only way to force the looping of the ringtone before M. (pre M)
+ MediaPlayerPlaybackDelegate()
+ }
+ }
+ return mPlaybackDelegate!!
+ }
+
+ /**
+ * This interface abstracts away the differences between playing ringtones via [Ringtone]
+ * vs [MediaPlayer].
+ */
+ private interface PlaybackDelegate {
+ /**
+ * @return `true` iff a [volume adjustment][.adjustVolume] should be scheduled
+ */
+ fun play(context: Context, ringtoneUri: Uri?, crescendoDuration: Long): Boolean
+
+ /**
+ * Stop any ongoing ringtone playback.
+ */
+ fun stop(context: Context?)
+
+ /**
+ * @return `true` iff another volume adjustment should be scheduled
+ */
+ fun adjustVolume(context: Context?): Boolean
+ }
+
+ /**
+ * Loops playback of a ringtone using [MediaPlayer].
+ */
+ private inner class MediaPlayerPlaybackDelegate : PlaybackDelegate {
+ /** The audio focus manager. Only used by the ringtone thread. */
+ private var mAudioManager: AudioManager? = null
+
+ /** Non-`null` while playing a ringtone; `null` otherwise. */
+ private var mMediaPlayer: MediaPlayer? = null
+
+ /** The duration over which to increase the volume. */
+ private var mCrescendoDuration: Long = 0
+
+ /** The time at which the crescendo shall cease; 0 if no crescendo is present. */
+ private var mCrescendoStopTime: Long = 0
+
+ /**
+ * Starts the actual playback of the ringtone. Executes on ringtone-thread.
+ */
+ override fun play(context: Context, ringtoneUri: Uri?, crescendoDuration: Long): Boolean {
+ checkAsyncRingtonePlayerThread()
+ mCrescendoDuration = crescendoDuration
+
+ LOGGER.i("Play ringtone via android.media.MediaPlayer.")
+
+ if (mAudioManager == null) {
+ mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ }
+
+ val inTelephoneCall = isInTelephoneCall(context)
+ var alarmNoise = if (inTelephoneCall) getInCallRingtoneUri(context) else ringtoneUri
+ // Fall back to the system default alarm if the database does not have an alarm stored.
+ if (alarmNoise == null) {
+ alarmNoise = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+ LOGGER.v("Using default alarm: $alarmNoise")
+ }
+
+ mMediaPlayer = MediaPlayer()
+ mMediaPlayer!!.setOnErrorListener { _, _, _ ->
+ LOGGER.e("Error occurred while playing audio. Stopping AlarmKlaxon.")
+ stop(context)
+ true
+ }
+
+ try {
+ // If alarmNoise is a custom ringtone on the sd card the app must be granted
+ // android.permission.READ_EXTERNAL_STORAGE. Pre-M this is ensured at app
+ // installation time. M+, this permission can be revoked by the user any time.
+ mMediaPlayer!!.setDataSource(context, alarmNoise!!)
+
+ return startPlayback(inTelephoneCall)
+ } catch (t: Throwable) {
+ LOGGER.e("Using the fallback ringtone, could not play $alarmNoise", t)
+ // The alarmNoise may be on the sd card which could be busy right now.
+ // Use the fallback ringtone.
+ try {
+ // Must reset the media player to clear the error state.
+ mMediaPlayer!!.reset()
+ mMediaPlayer!!.setDataSource(context, getFallbackRingtoneUri(context))
+ return startPlayback(inTelephoneCall)
+ } catch (t2: Throwable) {
+ // At this point we just don't play anything.
+ LOGGER.e("Failed to play fallback ringtone", t2)
+ }
+ }
+
+ return false
+ }
+
+ /**
+ * Prepare the MediaPlayer for playback if the alarm stream is not muted, then start the
+ * playback.
+ *
+ * @param inTelephoneCall `true` if there is currently an active telephone call
+ * @return `true` if a crescendo has started and future volume adjustments are
+ * required to advance the crescendo effect
+ */
+ @Throws(IOException::class)
+ private fun startPlayback(inTelephoneCall: Boolean): Boolean {
+ // Do not play alarms if stream volume is 0 (typically because ringer mode is silent).
+ if (mAudioManager!!.getStreamVolume(AudioManager.STREAM_ALARM) == 0) {
+ return false
+ }
+
+ // Indicate the ringtone should be played via the alarm stream.
+ if (Utils.isLOrLater) {
+ mMediaPlayer!!.setAudioAttributes(AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build())
+ }
+
+ // Check if we are in a call. If we are, use the in-call alarm resource at a low volume
+ // to not disrupt the call.
+ var scheduleVolumeAdjustment = false
+ if (inTelephoneCall) {
+ LOGGER.v("Using the in-call alarm")
+ mMediaPlayer!!.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME)
+ } else if (mCrescendoDuration > 0) {
+ mMediaPlayer!!.setVolume(0f, 0f)
+
+ // Compute the time at which the crescendo will stop.
+ mCrescendoStopTime = Utils.now() + mCrescendoDuration
+ scheduleVolumeAdjustment = true
+ }
+
+ mMediaPlayer!!.setAudioStreamType(AudioManager.STREAM_ALARM)
+ mMediaPlayer!!.isLooping = true
+ mMediaPlayer!!.prepare()
+ mAudioManager!!.requestAudioFocus(null, AudioManager.STREAM_ALARM,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+ mMediaPlayer!!.start()
+
+ return scheduleVolumeAdjustment
+ }
+
+ /**
+ * Stops the playback of the ringtone. Executes on the ringtone-thread.
+ */
+ override fun stop(context: Context?) {
+ checkAsyncRingtonePlayerThread()
+
+ LOGGER.i("Stop ringtone via android.media.MediaPlayer.")
+
+ mCrescendoDuration = 0
+ mCrescendoStopTime = 0
+
+ // Stop audio playing
+ if (mMediaPlayer != null) {
+ mMediaPlayer?.stop()
+ mMediaPlayer?.release()
+ mMediaPlayer = null
+ }
+
+ if (mAudioManager != null) {
+ mAudioManager?.abandonAudioFocus(null)
+ }
+ }
+
+ /**
+ * Adjusts the volume of the ringtone being played to create a crescendo effect.
+ */
+ override fun adjustVolume(context: Context?): Boolean {
+ checkAsyncRingtonePlayerThread()
+
+ // If media player is absent or not playing, ignore volume adjustment.
+ if (mMediaPlayer == null || !mMediaPlayer!!.isPlaying) {
+ mCrescendoDuration = 0
+ mCrescendoStopTime = 0
+ return false
+ }
+
+ // If the crescendo is complete set the volume to the maximum; we're done.
+ val currentTime = Utils.now()
+ if (currentTime > mCrescendoStopTime) {
+ mCrescendoDuration = 0
+ mCrescendoStopTime = 0
+ mMediaPlayer!!.setVolume(1f, 1f)
+ return false
+ }
+
+ // The current volume of the crescendo is the percentage of the crescendo completed.
+ val volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration)
+ mMediaPlayer!!.setVolume(volume, volume)
+ LOGGER.i("MediaPlayer volume set to $volume")
+
+ // Schedule the next volume bump in the crescendo.
+ return true
+ }
+ }
+
+ /**
+ * Loops playback of a ringtone using [Ringtone].
+ */
+ private inner class RingtonePlaybackDelegate : PlaybackDelegate {
+ /** The audio focus manager. Only used by the ringtone thread. */
+ private var mAudioManager: AudioManager? = null
+
+ /** The current ringtone. Only used by the ringtone thread. */
+ private var mRingtone: Ringtone? = null
+
+ /** The method to adjust playback volume; cannot be null. */
+ private lateinit var mSetVolumeMethod: Method
+
+ /** The method to adjust playback looping; cannot be null. */
+ private lateinit var mSetLoopingMethod: Method
+
+ /** The duration over which to increase the volume. */
+ private var mCrescendoDuration: Long = 0
+
+ /** The time at which the crescendo shall cease; 0 if no crescendo is present. */
+ private var mCrescendoStopTime: Long = 0
+
+ init {
+ try {
+ mSetVolumeMethod = Ringtone::class.java.getDeclaredMethod("setVolume",
+ Float::class.javaPrimitiveType)
+ } catch (nsme: NoSuchMethodException) {
+ LOGGER.e("Unable to locate method: Ringtone.setVolume(float).", nsme)
+ }
+ try {
+ mSetLoopingMethod = Ringtone::class.java.getDeclaredMethod("setLooping",
+ Boolean::class.javaPrimitiveType)
+ } catch (nsme: NoSuchMethodException) {
+ LOGGER.e("Unable to locate method: Ringtone.setLooping(boolean).", nsme)
+ }
+ }
+
+ /**
+ * Starts the actual playback of the ringtone. Executes on ringtone-thread.
+ */
+ override fun play(context: Context, ringtoneUri: Uri?, crescendoDuration: Long): Boolean {
+ var ringtoneUriVariable = ringtoneUri
+ checkAsyncRingtonePlayerThread()
+ mCrescendoDuration = crescendoDuration
+
+ LOGGER.i("Play ringtone via android.media.Ringtone.")
+
+ if (mAudioManager == null) {
+ mAudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ }
+
+ val inTelephoneCall = isInTelephoneCall(context)
+ if (inTelephoneCall) {
+ ringtoneUriVariable = getInCallRingtoneUri(context)
+ }
+
+ // Attempt to fetch the specified ringtone.
+ mRingtone = RingtoneManager.getRingtone(context, ringtoneUriVariable)
+
+ if (mRingtone == null) {
+ // Fall back to the system default ringtone.
+ ringtoneUriVariable = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+ mRingtone = RingtoneManager.getRingtone(context, ringtoneUriVariable)
+ }
+
+ // Attempt to enable looping the ringtone.
+ try {
+ mSetLoopingMethod.invoke(mRingtone, true)
+ } catch (e: Exception) {
+ LOGGER.e("Unable to turn looping on for android.media.Ringtone", e)
+
+ // Fall back to the default ringtone if looping could not be enabled.
+ // (Default alarm ringtone most likely has looping tags set within the .ogg file)
+ mRingtone = null
+ }
+
+ // If no ringtone exists at this point there isn't much recourse.
+ if (mRingtone == null) {
+ LOGGER.i("Unable to locate alarm ringtone, using internal fallback ringtone.")
+ ringtoneUriVariable = getFallbackRingtoneUri(context)
+ mRingtone = RingtoneManager.getRingtone(context, ringtoneUriVariable)
+ }
+
+ try {
+ return startPlayback(inTelephoneCall)
+ } catch (t: Throwable) {
+ LOGGER.e("Using the fallback ringtone, could not play $ringtoneUriVariable", t)
+ // Recover from any/all playback errors by attempting to play the fallback tone.
+ mRingtone = RingtoneManager.getRingtone(context, getFallbackRingtoneUri(context))
+ try {
+ return startPlayback(inTelephoneCall)
+ } catch (t2: Throwable) {
+ // At this point we just don't play anything.
+ LOGGER.e("Failed to play fallback ringtone", t2)
+ }
+ }
+
+ return false
+ }
+
+ /**
+ * Prepare the Ringtone for playback, then start the playback.
+ *
+ * @param inTelephoneCall `true` if there is currently an active telephone call
+ * @return `true` if a crescendo has started and future volume adjustments are
+ * required to advance the crescendo effect
+ */
+ private fun startPlayback(inTelephoneCall: Boolean): Boolean {
+ // Indicate the ringtone should be played via the alarm stream.
+ if (Utils.isLOrLater) {
+ mRingtone!!.audioAttributes = AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build()
+ }
+
+ // Attempt to adjust the ringtone volume if the user is in a telephone call.
+ var scheduleVolumeAdjustment = false
+ if (inTelephoneCall) {
+ LOGGER.v("Using the in-call alarm")
+ setRingtoneVolume(IN_CALL_VOLUME)
+ } else if (mCrescendoDuration > 0) {
+ setRingtoneVolume(0f)
+
+ // Compute the time at which the crescendo will stop.
+ mCrescendoStopTime = Utils.now() + mCrescendoDuration
+ scheduleVolumeAdjustment = true
+ }
+
+ mAudioManager!!.requestAudioFocus(null, AudioManager.STREAM_ALARM,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
+
+ mRingtone!!.play()
+
+ return scheduleVolumeAdjustment
+ }
+
+ /**
+ * Sets the volume of the ringtone.
+ *
+ * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
+ * corresponds to no attenuation being applied.
+ */
+ private fun setRingtoneVolume(volume: Float) {
+ try {
+ mSetVolumeMethod.invoke(mRingtone, volume)
+ } catch (e: Exception) {
+ LOGGER.e("Unable to set volume for android.media.Ringtone", e)
+ }
+ }
+
+ /**
+ * Stops the playback of the ringtone. Executes on the ringtone-thread.
+ */
+ override fun stop(context: Context?) {
+ checkAsyncRingtonePlayerThread()
+
+ LOGGER.i("Stop ringtone via android.media.Ringtone.")
+
+ mCrescendoDuration = 0
+ mCrescendoStopTime = 0
+
+ if (mRingtone != null && mRingtone!!.isPlaying) {
+ LOGGER.d("Ringtone.stop() invoked.")
+ mRingtone!!.stop()
+ }
+
+ mRingtone = null
+
+ if (mAudioManager != null) {
+ mAudioManager!!.abandonAudioFocus(null)
+ }
+ }
+
+ /**
+ * Adjusts the volume of the ringtone being played to create a crescendo effect.
+ */
+ override fun adjustVolume(context: Context?): Boolean {
+ checkAsyncRingtonePlayerThread()
+
+ // If ringtone is absent or not playing, ignore volume adjustment.
+ if (mRingtone == null || !mRingtone!!.isPlaying) {
+ mCrescendoDuration = 0
+ mCrescendoStopTime = 0
+ return false
+ }
+
+ // If the crescendo is complete set the volume to the maximum; we're done.
+ val currentTime = Utils.now()
+ if (currentTime > mCrescendoStopTime) {
+ mCrescendoDuration = 0
+ mCrescendoStopTime = 0
+ setRingtoneVolume(1f)
+ return false
+ }
+
+ val volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration)
+ setRingtoneVolume(volume)
+
+ // Schedule the next volume bump in the crescendo.
+ return true
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("AsyncRingtonePlayer")
+
+ // Volume suggested by media team for in-call alarms.
+ private const val IN_CALL_VOLUME = 0.125f
+
+ // Message codes used with the ringtone thread.
+ private const val EVENT_PLAY = 1
+ private const val EVENT_STOP = 2
+ private const val EVENT_VOLUME = 3
+ private const val RINGTONE_URI_KEY = "RINGTONE_URI_KEY"
+ private const val CRESCENDO_DURATION_KEY = "CRESCENDO_DURATION_KEY"
+
+ /**
+ * @return `true` iff the device is currently in a telephone call
+ */
+ private fun isInTelephoneCall(context: Context): Boolean {
+ val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+ return tm.callState != TelephonyManager.CALL_STATE_IDLE
+ }
+
+ /**
+ * @return Uri of the ringtone to play when the user is in a telephone call
+ */
+ private fun getInCallRingtoneUri(context: Context): Uri {
+ return Utils.getResourceUri(context, R.raw.alarm_expire)
+ }
+
+ /**
+ * @return Uri of the ringtone to play when the chosen ringtone fails to play
+ */
+ private fun getFallbackRingtoneUri(context: Context): Uri {
+ return Utils.getResourceUri(context, R.raw.alarm_expire)
+ }
+
+ /**
+ * @param currentTime current time of the device
+ * @param stopTime time at which the crescendo finishes
+ * @param duration length of time over which the crescendo occurs
+ * @return the scalar volume value that produces a linear increase in volume (in decibels)
+ */
+ private fun computeVolume(currentTime: Long, stopTime: Long, duration: Long): Float {
+ // Compute the percentage of the crescendo that has completed.
+ val elapsedCrescendoTime = stopTime - currentTime.toFloat()
+ val fractionComplete = 1 - elapsedCrescendoTime / duration
+
+ // Use the fraction to compute a target decibel between
+ // -40dB (near silent) and 0dB (max).
+ val gain = fractionComplete * 40 - 40
+
+ // Convert the target gain (in decibels) into the corresponding volume scalar.
+ val volume = 10.0.pow(gain / 20f.toDouble()).toFloat()
+
+ LOGGER.v("Ringtone crescendo %,.2f%% complete (scalar: %f, volume: %f dB)",
+ fractionComplete * 100, volume, gain)
+
+ return volume
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/BaseActivity.java b/src/com/android/deskclock/BaseActivity.java
deleted file mode 100644
index 143f238..0000000
--- a/src/com/android/deskclock/BaseActivity.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Bundle;
-import androidx.annotation.ColorInt;
-import androidx.appcompat.app.AppCompatActivity;
-import android.view.View;
-
-import static com.android.deskclock.AnimatorUtils.ARGB_EVALUATOR;
-
-/**
- * Base activity class that changes the app window's color based on the current hour.
- */
-public abstract class BaseActivity extends AppCompatActivity {
-
- /** Sets the app window color on each frame of the {@link #mAppColorAnimator}. */
- private final AppColorAnimationListener mAppColorAnimationListener
- = new AppColorAnimationListener();
-
- /** The current animator that is changing the app window color or {@code null}. */
- private ValueAnimator mAppColorAnimator;
-
- /** Draws the app window's color. */
- private ColorDrawable mBackground;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Allow the content to layout behind the status and navigation bars.
- getWindow().getDecorView().setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
-
- final @ColorInt int color = ThemeUtils.resolveColor(this, android.R.attr.windowBackground);
- adjustAppColor(color, false /* animate */);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
-
- // Ensure the app window color is up-to-date.
- final @ColorInt int color = ThemeUtils.resolveColor(this, android.R.attr.windowBackground);
- adjustAppColor(color, false /* animate */);
- }
-
- /**
- * Adjusts the current app window color of this activity; animates the change if desired.
- *
- * @param color the ARGB value to set as the current app window color
- * @param animate {@code true} if the change should be animated
- */
- protected void adjustAppColor(@ColorInt int color, boolean animate) {
- // Create and install the drawable that defines the window color.
- if (mBackground == null) {
- mBackground = new ColorDrawable(color);
- getWindow().setBackgroundDrawable(mBackground);
- }
-
- // Cancel the current window color animation if one exists.
- if (mAppColorAnimator != null) {
- mAppColorAnimator.cancel();
- }
-
- final @ColorInt int currentColor = mBackground.getColor();
- if (currentColor != color) {
- if (animate) {
- mAppColorAnimator = ValueAnimator.ofObject(ARGB_EVALUATOR, currentColor, color)
- .setDuration(3000L);
- mAppColorAnimator.addUpdateListener(mAppColorAnimationListener);
- mAppColorAnimator.addListener(mAppColorAnimationListener);
- mAppColorAnimator.start();
- } else {
- setAppColor(color);
- }
- }
- }
-
- private void setAppColor(@ColorInt int color) {
- mBackground.setColor(color);
- }
-
- /**
- * Sets the app window color to the current color produced by the animator.
- */
- private final class AppColorAnimationListener extends AnimatorListenerAdapter
- implements AnimatorUpdateListener {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- final @ColorInt int color = (int) valueAnimator.getAnimatedValue();
- setAppColor(color);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (mAppColorAnimator == animation) {
- mAppColorAnimator = null;
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/BaseActivity.kt b/src/com/android/deskclock/BaseActivity.kt
new file mode 100644
index 0000000..79213c5
--- /dev/null
+++ b/src/com/android/deskclock/BaseActivity.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.R
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.graphics.drawable.ColorDrawable
+import android.os.Bundle
+import android.view.View
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Base activity class that changes the app window's color based on the current hour.
+ */
+abstract class BaseActivity : AppCompatActivity() {
+ /** Sets the app window color on each frame of the [.mAppColorAnimator]. */
+ private val mAppColorAnimationListener = AppColorAnimationListener()
+
+ /** The current animator that is changing the app window color or `null`. */
+ private var mAppColorAnimator: ValueAnimator? = null
+
+ /** Draws the app window's color. */
+ private var mBackground: ColorDrawable? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Allow the content to layout behind the status and navigation bars.
+ getWindow().getDecorView().setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
+
+ @ColorInt val color = ThemeUtils.resolveColor(this, R.attr.windowBackground)
+ adjustAppColor(color, animate = false)
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ // Ensure the app window color is up-to-date.
+ @ColorInt val color = ThemeUtils.resolveColor(this, R.attr.windowBackground)
+ adjustAppColor(color, animate = false)
+ }
+
+ /**
+ * Adjusts the current app window color of this activity; animates the change if desired.
+ *
+ * @param color the ARGB value to set as the current app window color
+ * @param animate `true` if the change should be animated
+ */
+ protected fun adjustAppColor(@ColorInt color: Int, animate: Boolean) {
+ // Create and install the drawable that defines the window color.
+ if (mBackground == null) {
+ mBackground = ColorDrawable(color)
+ getWindow().setBackgroundDrawable(mBackground)
+ }
+
+ // Cancel the current window color animation if one exists.
+ mAppColorAnimator?.cancel()
+
+ @ColorInt val currentColor = mBackground!!.color
+ if (currentColor != color) {
+ if (animate) {
+ mAppColorAnimator = ValueAnimator.ofObject(AnimatorUtils.ARGB_EVALUATOR,
+ currentColor, color)
+ .setDuration(3000L)
+ mAppColorAnimator!!.addUpdateListener(mAppColorAnimationListener)
+ mAppColorAnimator!!.addListener(mAppColorAnimationListener)
+ mAppColorAnimator!!.start()
+ } else {
+ setAppColor(color)
+ }
+ }
+ }
+
+ private fun setAppColor(@ColorInt color: Int) {
+ mBackground!!.color = color
+ }
+
+ /**
+ * Sets the app window color to the current color produced by the animator.
+ */
+ private inner class AppColorAnimationListener
+ : AnimatorListenerAdapter(), AnimatorUpdateListener {
+ override fun onAnimationUpdate(valueAnimator: ValueAnimator) {
+ @ColorInt val color = valueAnimator.animatedValue as Int
+ setAppColor(color)
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ if (mAppColorAnimator === animation) {
+ mAppColorAnimator = null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/CircleButtonsLayout.java b/src/com/android/deskclock/CircleButtonsLayout.java
deleted file mode 100644
index 079472e..0000000
--- a/src/com/android/deskclock/CircleButtonsLayout.java
+++ /dev/null
@@ -1,137 +0,0 @@
-package com.android.deskclock;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-/**
- * This class adjusts the locations of children buttons and text of this view group by adjusting the
- * margins of each item. The left and right buttons are aligned with the bottom of the circle. The
- * stop button and label text are located within the circle with the stop button near the bottom and
- * the label text near the top. The maximum text size for the label text view is also calculated.
- */
-public class CircleButtonsLayout extends FrameLayout {
-
- private float mDiamOffset;
- private View mCircleView;
- private Button mResetAddButton;
- private TextView mLabel;
-
- @SuppressWarnings("unused")
- public CircleButtonsLayout(Context context) {
- this(context, null);
- }
-
- public CircleButtonsLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final Resources res = getContext().getResources();
- final float strokeSize = res.getDimension(R.dimen.circletimer_circle_size);
- final float dotStrokeSize = res.getDimension(R.dimen.circletimer_dot_size);
- final float markerStrokeSize = res.getDimension(R.dimen.circletimer_marker_size);
- mDiamOffset = Utils.calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize) * 2;
- }
-
- @Override
- public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // We must call onMeasure both before and after re-measuring our views because the circle
- // may not always be drawn here yet. The first onMeasure will force the circle to be drawn,
- // and the second will force our re-measurements to take effect.
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- remeasureViews();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- protected void remeasureViews() {
- if (mLabel == null) {
- mCircleView = findViewById(R.id.timer_time);
- mLabel = (TextView) findViewById(R.id.timer_label);
- mResetAddButton = (Button) findViewById(R.id.reset_add);
- }
-
- final int frameWidth = mCircleView.getMeasuredWidth();
- final int frameHeight = mCircleView.getMeasuredHeight();
- final int minBound = Math.min(frameWidth, frameHeight);
- final int circleDiam = (int) (minBound - mDiamOffset);
-
- if (mResetAddButton != null) {
- final MarginLayoutParams resetAddParams = (MarginLayoutParams) mResetAddButton
- .getLayoutParams();
- resetAddParams.bottomMargin = circleDiam / 6;
- if (minBound == frameWidth) {
- resetAddParams.bottomMargin += (frameHeight - frameWidth) / 2;
- }
- }
-
- if (mLabel != null) {
- MarginLayoutParams labelParams = (MarginLayoutParams) mLabel.getLayoutParams();
- labelParams.topMargin = circleDiam/6;
- if (minBound == frameWidth) {
- labelParams.topMargin += (frameHeight-frameWidth)/2;
- }
- /* The following formula has been simplified based on the following:
- * Our goal is to calculate the maximum width for the label frame.
- * We may do this with the following diagram to represent the top half of the circle:
- * ___
- * . | .
- * ._________| .
- * . ^ | .
- * / x | \
- * |_______________|_______________|
- *
- * where x represents the value we would like to calculate, and the final width of the
- * label will be w = 2 * x.
- *
- * We may find x by drawing a right triangle from the center of the circle:
- * ___
- * . | .
- * ._________| .
- * . . | .
- * / . | }y \
- * |_____________.t|_______________|
- *
- * where t represents the angle of that triangle, and y is the height of that triangle.
- *
- * If r = radius of the circle, we know the following trigonometric identities:
- * cos(t) = y / r
- * and sin(t) = x / r
- * => r * sin(t) = x
- * and sin^2(t) = 1 - cos^2(t)
- * => sin(t) = +/- sqrt(1 - cos^2(t))
- * (note: because we need the positive value, we may drop the +/-).
- *
- * To calculate the final width, we may combine our formulas:
- * w = 2 * x
- * => w = 2 * r * sin(t)
- * => w = 2 * r * sqrt(1 - cos^2(t))
- * => w = 2 * r * sqrt(1 - (y / r)^2)
- *
- * Simplifying even further, to mitigate the complexity of the final formula:
- * sqrt(1 - (y / r)^2)
- * => sqrt(1 - (y^2 / r^2))
- * => sqrt((r^2 / r^2) - (y^2 / r^2))
- * => sqrt((r^2 - y^2) / (r^2))
- * => sqrt(r^2 - y^2) / sqrt(r^2)
- * => sqrt(r^2 - y^2) / r
- * => sqrt((r + y)*(r - y)) / r
- *
- * Placing this back in our formula, we end up with, as our final, reduced equation:
- * w = 2 * r * sqrt(1 - (y / r)^2)
- * => w = 2 * r * sqrt((r + y)*(r - y)) / r
- * => w = 2 * sqrt((r + y)*(r - y))
- */
- // Radius of the circle.
- int r = circleDiam / 2;
- // Y value of the top of the label, calculated from the center of the circle.
- int y = frameHeight / 2 - labelParams.topMargin;
- // New maximum width of the label.
- double w = 2 * Math.sqrt((r + y) * (r - y));
-
- mLabel.setMaxWidth((int) w);
- }
- }
-}
diff --git a/src/com/android/deskclock/CircleButtonsLayout.kt b/src/com/android/deskclock/CircleButtonsLayout.kt
new file mode 100644
index 0000000..a1dae04
--- /dev/null
+++ b/src/com/android/deskclock/CircleButtonsLayout.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.Button
+import android.widget.FrameLayout
+import android.widget.TextView
+
+import kotlin.math.min
+import kotlin.math.sqrt
+
+/**
+ * This class adjusts the locations of child buttons and text of this view group by adjusting the
+ * margins of each item. The left and right buttons are aligned with the bottom of the circle. The
+ * stop button and label text are located within the circle with the stop button near the bottom and
+ * the label text near the top. The maximum text size for the label text view is also calculated.
+ */
+class CircleButtonsLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : FrameLayout(context, attrs) {
+ private val mDiamOffset: Float
+ private var mCircleView: View? = null
+ private var mResetAddButton: Button? = null
+ private var mLabel: TextView? = null
+
+ init {
+ val res = getContext().resources
+ val strokeSize = res.getDimension(R.dimen.circletimer_circle_size)
+ val dotStrokeSize = res.getDimension(R.dimen.circletimer_dot_size)
+ val markerStrokeSize = res.getDimension(R.dimen.circletimer_marker_size)
+ mDiamOffset = Utils.calculateRadiusOffset(strokeSize, dotStrokeSize, markerStrokeSize) * 2
+ }
+
+ public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ // We must call onMeasure both before and after re-measuring our views because the circle
+ // may not always be drawn here yet. The first onMeasure will force the circle to be drawn,
+ // and the second will force our re-measurements to take effect.
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ remeasureViews()
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+
+ private fun remeasureViews() {
+ if (mLabel == null) {
+ mCircleView = findViewById(R.id.timer_time)
+ mLabel = findViewById<View>(R.id.timer_label) as TextView
+ mResetAddButton = findViewById<View>(R.id.reset_add) as Button
+ }
+
+ val frameWidth = mCircleView!!.measuredWidth
+ val frameHeight = mCircleView!!.measuredHeight
+ val minBound = min(frameWidth, frameHeight)
+ val circleDiam = (minBound - mDiamOffset).toInt()
+
+ mResetAddButton?.let {
+ val resetAddParams = it.layoutParams as MarginLayoutParams
+ resetAddParams.bottomMargin = circleDiam / 6
+ if (minBound == frameWidth) {
+ resetAddParams.bottomMargin += (frameHeight - frameWidth) / 2
+ }
+ }
+
+ mLabel?.let {
+ val labelParams = it.layoutParams as MarginLayoutParams
+ labelParams.topMargin = circleDiam / 6
+ if (minBound == frameWidth) {
+ labelParams.topMargin += (frameHeight - frameWidth) / 2
+ }
+ /* The following formula has been simplified based on the following:
+ * Our goal is to calculate the maximum width for the label frame.
+ * We may do this with the following diagram to represent the top half of the circle:
+ * ___
+ * . | .
+ * ._________| .
+ * . ^ | .
+ * / x | \
+ * |_______________|_______________|
+ *
+ * where x represents the value we would like to calculate, and the final width of the
+ * label will be w = 2 * x.
+ *
+ * We may find x by drawing a right triangle from the center of the circle:
+ * ___
+ * . | .
+ * ._________| .
+ * . . | .
+ * / . | }y \
+ * |_____________.t|_______________|
+ *
+ * where t represents the angle of that triangle, and y is the height of that triangle.
+ *
+ * If r = radius of the circle, we know the following trigonometric identities:
+ * cos(t) = y / r
+ * and sin(t) = x / r
+ * => r * sin(t) = x
+ * and sin^2(t) = 1 - cos^2(t)
+ * => sin(t) = +/- sqrt(1 - cos^2(t))
+ * (note: because we need the positive value, we may drop the +/-).
+ *
+ * To calculate the final width, we may combine our formulas:
+ * w = 2 * x
+ * => w = 2 * r * sin(t)
+ * => w = 2 * r * sqrt(1 - cos^2(t))
+ * => w = 2 * r * sqrt(1 - (y / r)^2)
+ *
+ * Simplifying even further, to mitigate the complexity of the final formula:
+ * sqrt(1 - (y / r)^2)
+ * => sqrt(1 - (y^2 / r^2))
+ * => sqrt((r^2 / r^2) - (y^2 / r^2))
+ * => sqrt((r^2 - y^2) / (r^2))
+ * => sqrt(r^2 - y^2) / sqrt(r^2)
+ * => sqrt(r^2 - y^2) / r
+ * => sqrt((r + y)*(r - y)) / r
+ *
+ * Placing this back in our formula, we end up with, as our final, reduced equation:
+ * w = 2 * r * sqrt(1 - (y / r)^2)
+ * => w = 2 * r * sqrt((r + y)*(r - y)) / r
+ * => w = 2 * sqrt((r + y)*(r - y))
+ */
+ // Radius of the circle.
+ val r = circleDiam / 2
+ // Y value of the top of the label, calculated from the center of the circle.
+ val y = frameHeight / 2 - labelParams.topMargin
+ // New maximum width of the label.
+ val w = 2 * sqrt((r + y) * (r - y).toDouble())
+
+ it.maxWidth = w.toInt()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ClockFragment.java b/src/com/android/deskclock/ClockFragment.java
deleted file mode 100644
index 1536b55..0000000
--- a/src/com/android/deskclock/ClockFragment.java
+++ /dev/null
@@ -1,547 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.Activity;
-import android.app.AlarmManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Resources;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.provider.Settings;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.text.format.DateUtils;
-import android.view.GestureDetector;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextClock;
-import android.widget.TextView;
-
-import com.android.deskclock.data.City;
-import com.android.deskclock.data.CityListener;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.worldclock.CitySelectionActivity;
-
-import java.util.Calendar;
-import java.util.List;
-import java.util.TimeZone;
-
-import static android.app.AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED;
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS;
-import static java.util.Calendar.DAY_OF_WEEK;
-
-/**
- * Fragment that shows the clock (analog or digital), the next alarm info and the world clock.
- */
-public final class ClockFragment extends DeskClockFragment {
-
- // Updates dates in the UI on every quarter-hour.
- private final Runnable mQuarterHourUpdater = new QuarterHourRunnable();
-
- // Updates the UI in response to changes to the scheduled alarm.
- private BroadcastReceiver mAlarmChangeReceiver;
-
- // Detects changes to the next scheduled alarm pre-L.
- private ContentObserver mAlarmObserver;
-
- private TextClock mDigitalClock;
- private AnalogClock mAnalogClock;
- private View mClockFrame;
- private SelectedCitiesAdapter mCityAdapter;
- private RecyclerView mCityList;
- private String mDateFormat;
- private String mDateFormatForAccessibility;
-
- /**
- * The public no-arg constructor required by all fragments.
- */
- public ClockFragment() {
- super(CLOCKS);
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mAlarmObserver = Utils.isPreL() ? new AlarmObserverPreL() : null;
- mAlarmChangeReceiver = Utils.isLOrLater() ? new AlarmChangedBroadcastReceiver() : null;
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
- super.onCreateView(inflater, container, icicle);
-
- final View fragmentView = inflater.inflate(R.layout.clock_fragment, container, false);
-
- mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
- mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
-
- mCityAdapter = new SelectedCitiesAdapter(getActivity(), mDateFormat,
- mDateFormatForAccessibility);
-
- mCityList = (RecyclerView) fragmentView.findViewById(R.id.cities);
- mCityList.setLayoutManager(new LinearLayoutManager(getActivity()));
- mCityList.setAdapter(mCityAdapter);
- mCityList.setItemAnimator(null);
- DataModel.getDataModel().addCityListener(mCityAdapter);
-
- final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
- mCityList.addOnScrollListener(scrollPositionWatcher);
-
- final Context context = container.getContext();
- mCityList.setOnTouchListener(new CityListOnLongClickListener(context));
- fragmentView.setOnLongClickListener(new StartScreenSaverListener());
-
- // On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added
- // on as a header to the main listview.
- mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane);
- if (mClockFrame != null) {
- mDigitalClock = (TextClock) mClockFrame.findViewById(R.id.digital_clock);
- mAnalogClock = (AnalogClock) mClockFrame.findViewById(R.id.analog_clock);
- Utils.setClockIconTypeface(mClockFrame);
- Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame);
- Utils.setClockStyle(mDigitalClock, mAnalogClock);
- Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
- }
-
- // Schedule a runnable to update the date every quarter hour.
- UiDataModel.getUiDataModel().addQuarterHourCallback(mQuarterHourUpdater, 100);
-
- return fragmentView;
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- final Activity activity = getActivity();
-
- mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
- mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
-
- // Watch for system events that effect clock time or format.
- if (mAlarmChangeReceiver != null) {
- final IntentFilter filter = new IntentFilter(ACTION_NEXT_ALARM_CLOCK_CHANGED);
- activity.registerReceiver(mAlarmChangeReceiver, filter);
- }
-
- // Resume can be invoked after changing the clock style or seconds display.
- if (mDigitalClock != null && mAnalogClock != null) {
- Utils.setClockStyle(mDigitalClock, mAnalogClock);
- Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
- }
-
- final View view = getView();
- if (view != null && view.findViewById(R.id.main_clock_left_pane) != null) {
- // Center the main clock frame by hiding the world clocks when none are selected.
- mCityList.setVisibility(mCityAdapter.getItemCount() == 0 ? GONE : VISIBLE);
- }
-
- refreshAlarm();
-
- // Alarm observer is null on L or later.
- if (mAlarmObserver != null) {
- @SuppressWarnings("deprecation")
- final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
- activity.getContentResolver().registerContentObserver(uri, false, mAlarmObserver);
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- final Activity activity = getActivity();
- if (mAlarmChangeReceiver != null) {
- activity.unregisterReceiver(mAlarmChangeReceiver);
- }
- if (mAlarmObserver != null) {
- activity.getContentResolver().unregisterContentObserver(mAlarmObserver);
- }
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
- UiDataModel.getUiDataModel().removePeriodicCallback(mQuarterHourUpdater);
- DataModel.getDataModel().removeCityListener(mCityAdapter);
- }
-
- @Override
- public void onFabClick(@NonNull ImageView fab) {
- startActivity(new Intent(getActivity(), CitySelectionActivity.class));
- }
-
- @Override
- public void onUpdateFab(@NonNull ImageView fab) {
- fab.setVisibility(VISIBLE);
- fab.setImageResource(R.drawable.ic_public);
- fab.setContentDescription(fab.getResources().getString(R.string.button_cities));
- }
-
- @Override
- public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
- left.setVisibility(INVISIBLE);
- right.setVisibility(INVISIBLE);
- }
-
- /**
- * Refresh the next alarm time.
- */
- private void refreshAlarm() {
- if (mClockFrame != null) {
- Utils.refreshAlarm(getActivity(), mClockFrame);
- } else {
- mCityAdapter.refreshAlarm();
- }
- }
-
- /**
- * Long pressing over the main clock starts the screen saver.
- */
- private final class StartScreenSaverListener implements View.OnLongClickListener {
-
- @Override
- public boolean onLongClick(View view) {
- startActivity(new Intent(getActivity(), ScreensaverActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock));
- return true;
- }
- }
-
- /**
- * Long pressing over the city list starts the screen saver.
- */
- private final class CityListOnLongClickListener extends GestureDetector.SimpleOnGestureListener
- implements View.OnTouchListener {
-
- private final GestureDetector mGestureDetector;
-
- private CityListOnLongClickListener(Context context) {
- mGestureDetector = new GestureDetector(context, this);
- }
-
- @Override
- public void onLongPress(MotionEvent e) {
- final View view = getView();
- if (view != null) {
- view.performLongClick();
- }
- }
-
- @Override
- public boolean onDown(MotionEvent e) {
- return true;
- }
-
- @Override
- public boolean onTouch(View v, MotionEvent event) {
- return mGestureDetector.onTouchEvent(event);
- }
- }
-
- /**
- * This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and
- * updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate
- * the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45).
- */
- private final class QuarterHourRunnable implements Runnable {
- @Override
- public void run() {
- mCityAdapter.notifyDataSetChanged();
- }
- }
-
- /**
- * Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm.
- * In L and beyond this is accomplished via a system broadcast of
- * {@link AlarmManager#ACTION_NEXT_ALARM_CLOCK_CHANGED}.
- */
- private final class AlarmObserverPreL extends ContentObserver {
- private AlarmObserverPreL() {
- super(new Handler());
- }
-
- @Override
- public void onChange(boolean selfChange) {
- refreshAlarm();
- }
- }
-
- /**
- * Update the display of the scheduled alarm as it changes.
- */
- private final class AlarmChangedBroadcastReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- refreshAlarm();
- }
- }
-
- /**
- * Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls
- * the recyclerview or when the size/position of elements within the recyclerview changes.
- */
- private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
- implements View.OnLayoutChangeListener {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- setTabScrolledToTop(Utils.isScrolledToTop(mCityList));
- }
-
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- setTabScrolledToTop(Utils.isScrolledToTop(mCityList));
- }
- }
-
- /**
- * This adapter lists all of the selected world clocks. Optionally, it also includes a clock at
- * the top for the home timezone if "Automatic home clock" is turned on in settings and the
- * current time at home does not match the current time in the timezone of the current location.
- * If the phone is in portrait mode it will also include the main clock at the top.
- */
- private static final class SelectedCitiesAdapter extends RecyclerView.Adapter
- implements CityListener {
-
- private final static int MAIN_CLOCK = R.layout.main_clock_frame;
- private final static int WORLD_CLOCK = R.layout.world_clock_item;
-
- private final LayoutInflater mInflater;
- private final Context mContext;
- private final boolean mIsPortrait;
- private final boolean mShowHomeClock;
- private final String mDateFormat;
- private final String mDateFormatForAccessibility;
-
- private SelectedCitiesAdapter(Context context, String dateFormat,
- String dateFormatForAccessibility) {
- mContext = context;
- mDateFormat = dateFormat;
- mDateFormatForAccessibility = dateFormatForAccessibility;
- mInflater = LayoutInflater.from(context);
- mIsPortrait = Utils.isPortrait(context);
- mShowHomeClock = DataModel.getDataModel().getShowHomeClock();
- }
-
- @Override
- public int getItemViewType(int position) {
- if (position == 0 && mIsPortrait) {
- return MAIN_CLOCK;
- }
- return WORLD_CLOCK;
- }
-
- @Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- final View view = mInflater.inflate(viewType, parent, false);
- switch (viewType) {
- case WORLD_CLOCK:
- return new CityViewHolder(view);
- case MAIN_CLOCK:
- return new MainClockViewHolder(view);
- default:
- throw new IllegalArgumentException("View type not recognized");
- }
- }
-
- @Override
- public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
- final int viewType = getItemViewType(position);
- switch (viewType) {
- case WORLD_CLOCK:
- // Retrieve the city to bind.
- final City city;
- // If showing home clock, put it at the top
- if (mShowHomeClock && position == (mIsPortrait ? 1 : 0)) {
- city = getHomeCity();
- } else {
- final int positionAdjuster = (mIsPortrait ? 1 : 0)
- + (mShowHomeClock ? 1 : 0);
- city = getCities().get(position - positionAdjuster);
- }
- ((CityViewHolder) holder).bind(mContext, city, position, mIsPortrait);
- break;
- case MAIN_CLOCK:
- ((MainClockViewHolder) holder).bind(mContext, mDateFormat,
- mDateFormatForAccessibility, getItemCount() > 1);
- break;
- default:
- throw new IllegalArgumentException("Unexpected view type: " + viewType);
- }
- }
-
- @Override
- public int getItemCount() {
- final int mainClockCount = mIsPortrait ? 1 : 0;
- final int homeClockCount = mShowHomeClock ? 1 : 0;
- final int worldClockCount = getCities().size();
- return mainClockCount + homeClockCount + worldClockCount;
- }
-
- private City getHomeCity() {
- return DataModel.getDataModel().getHomeCity();
- }
-
- private List<City> getCities() {
- return DataModel.getDataModel().getSelectedCities();
- }
-
- private void refreshAlarm() {
- if (mIsPortrait && getItemCount() > 0) {
- notifyItemChanged(0);
- }
- }
-
- @Override
- public void citiesChanged(List<City> oldCities, List<City> newCities) {
- notifyDataSetChanged();
- }
-
- private static final class CityViewHolder extends RecyclerView.ViewHolder {
-
- private final TextView mName;
- private final TextClock mDigitalClock;
- private final AnalogClock mAnalogClock;
- private final TextView mHoursAhead;
-
- private CityViewHolder(View itemView) {
- super(itemView);
-
- mName = (TextView) itemView.findViewById(R.id.city_name);
- mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock);
- mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock);
- mHoursAhead = (TextView) itemView.findViewById(R.id.hours_ahead);
- }
-
- private void bind(Context context, City city, int position, boolean isPortrait) {
- final String cityTimeZoneId = city.getTimeZone().getID();
-
- // Configure the digital clock or analog clock depending on the user preference.
- if (DataModel.getDataModel().getClockStyle() == DataModel.ClockStyle.ANALOG) {
- mDigitalClock.setVisibility(GONE);
- mAnalogClock.setVisibility(VISIBLE);
- mAnalogClock.setTimeZone(cityTimeZoneId);
- mAnalogClock.enableSeconds(false);
- } else {
- mAnalogClock.setVisibility(GONE);
- mDigitalClock.setVisibility(VISIBLE);
- mDigitalClock.setTimeZone(cityTimeZoneId);
- mDigitalClock.setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */,
- false));
- mDigitalClock.setFormat24Hour(Utils.get24ModeFormat(false));
- }
-
- // Supply top and bottom padding dynamically.
- final Resources res = context.getResources();
- final int padding = res.getDimensionPixelSize(R.dimen.medium_space_top);
- final int top = position == 0 && !isPortrait ? 0 : padding;
- final int left = itemView.getPaddingLeft();
- final int right = itemView.getPaddingRight();
- final int bottom = itemView.getPaddingBottom();
- itemView.setPadding(left, top, right, bottom);
-
- // Bind the city name.
- mName.setText(city.getName());
-
- // Compute if the city week day matches the weekday of the current timezone.
- final Calendar localCal = Calendar.getInstance(TimeZone.getDefault());
- final Calendar cityCal = Calendar.getInstance(city.getTimeZone());
- final boolean displayDayOfWeek =
- localCal.get(DAY_OF_WEEK) != cityCal.get(DAY_OF_WEEK);
-
- // Compare offset from UTC time on today's date (daylight savings time, etc.)
- final TimeZone currentTimeZone = TimeZone.getDefault();
- final TimeZone cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId);
- final long currentTimeMillis = System.currentTimeMillis();
- final long currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis);
- final long cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis);
- final long offsetDelta = cityUtcOffset - currentUtcOffset;
-
- final int hoursDifferent = (int) (offsetDelta / DateUtils.HOUR_IN_MILLIS);
- final int minutesDifferent = (int) (offsetDelta / DateUtils.MINUTE_IN_MILLIS) % 60;
- final boolean displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0;
- final boolean isAhead = hoursDifferent > 0 || (hoursDifferent == 0
- && minutesDifferent > 0);
- if (!Utils.isLandscape(context)) {
- // Bind the number of hours ahead or behind, or hide if the time is the same.
- final boolean displayDifference = hoursDifferent != 0 || displayMinutes;
- mHoursAhead.setVisibility(displayDifference ? VISIBLE : GONE);
- final String timeString = Utils.createHoursDifferentString(
- context, displayMinutes, isAhead, hoursDifferent, minutesDifferent);
- mHoursAhead.setText(displayDayOfWeek ?
- (context.getString(isAhead ? R.string.world_hours_tomorrow
- : R.string.world_hours_yesterday, timeString))
- : timeString);
- } else {
- // Only tomorrow/yesterday should be shown in landscape view.
- mHoursAhead.setVisibility(displayDayOfWeek ? View.VISIBLE : View.GONE);
- if (displayDayOfWeek) {
- mHoursAhead.setText(context.getString(isAhead ? R.string.world_tomorrow
- : R.string.world_yesterday));
- }
-
- }
- }
- }
-
- private static final class MainClockViewHolder extends RecyclerView.ViewHolder {
-
- private final View mHairline;
- private final TextClock mDigitalClock;
- private final AnalogClock mAnalogClock;
-
- private MainClockViewHolder(View itemView) {
- super(itemView);
-
- mHairline = itemView.findViewById(R.id.hairline);
- mDigitalClock = (TextClock) itemView.findViewById(R.id.digital_clock);
- mAnalogClock = (AnalogClock) itemView.findViewById(R.id.analog_clock);
- Utils.setClockIconTypeface(itemView);
- }
-
- private void bind(Context context, String dateFormat,
- String dateFormatForAccessibility, boolean showHairline) {
- Utils.refreshAlarm(context, itemView);
-
- Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView);
- Utils.setClockStyle(mDigitalClock, mAnalogClock);
- mHairline.setVisibility(showHairline ? VISIBLE : GONE);
-
- Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock);
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/ClockFragment.kt b/src/com/android/deskclock/ClockFragment.kt
new file mode 100644
index 0000000..bdcb235
--- /dev/null
+++ b/src/com/android/deskclock/ClockFragment.kt
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.AlarmManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.os.Bundle
+import android.os.Handler
+import android.provider.Settings
+import android.text.format.DateUtils
+import android.view.GestureDetector
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.GestureDetector.SimpleOnGestureListener
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextClock
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.data.City
+import com.android.deskclock.data.CityListener
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.worldclock.CitySelectionActivity
+
+import java.util.Calendar
+import java.util.TimeZone
+
+/**
+ * Fragment that shows the clock (analog or digital), the next alarm info and the world clock.
+ */
+class ClockFragment : DeskClockFragment(UiDataModel.Tab.CLOCKS) {
+ // Updates dates in the UI on every quarter-hour.
+ private val mQuarterHourUpdater: Runnable = QuarterHourRunnable()
+
+ // Updates the UI in response to changes to the scheduled alarm.
+ private var mAlarmChangeReceiver: BroadcastReceiver? = null
+
+ // Detects changes to the next scheduled alarm pre-L.
+ private var mAlarmObserver: ContentObserver? = null
+
+ private var mDigitalClock: TextClock? = null
+ private var mAnalogClock: AnalogClock? = null
+ private var mClockFrame: View? = null
+ private lateinit var mCityAdapter: SelectedCitiesAdapter
+ private lateinit var mCityList: RecyclerView
+ private lateinit var mDateFormat: String
+ private lateinit var mDateFormatForAccessibility: String
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ mAlarmObserver = if (Utils.isPreL) AlarmObserverPreL() else null
+ mAlarmChangeReceiver = if (Utils.isLOrLater) AlarmChangedBroadcastReceiver() else null
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ icicle: Bundle?
+ ): View? {
+ super.onCreateView(inflater, container, icicle)
+
+ val fragmentView = inflater.inflate(R.layout.clock_fragment, container, false)
+
+ mDateFormat = getString(R.string.abbrev_wday_month_day_no_year)
+ mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year)
+
+ mCityAdapter = SelectedCitiesAdapter(requireActivity(), mDateFormat,
+ mDateFormatForAccessibility)
+
+ mCityList = fragmentView.findViewById<View>(R.id.cities) as RecyclerView
+ mCityList.setLayoutManager(LinearLayoutManager(requireActivity()))
+ mCityList.setAdapter(mCityAdapter)
+ mCityList.setItemAnimator(null)
+ DataModel.dataModel.addCityListener(mCityAdapter)
+
+ val scrollPositionWatcher = ScrollPositionWatcher()
+ mCityList.addOnScrollListener(scrollPositionWatcher)
+
+ val context = container!!.context
+ mCityList.setOnTouchListener(CityListOnLongClickListener(context))
+ fragmentView.setOnLongClickListener(StartScreenSaverListener())
+
+ // On tablet landscape, the clock frame will be a distinct view. Otherwise, it'll be added
+ // on as a header to the main listview.
+ mClockFrame = fragmentView.findViewById(R.id.main_clock_left_pane)
+ if (mClockFrame != null) {
+ mDigitalClock = mClockFrame!!.findViewById<View>(R.id.digital_clock) as TextClock
+ mAnalogClock = mClockFrame!!.findViewById<View>(R.id.analog_clock) as AnalogClock
+ Utils.setClockIconTypeface(mClockFrame)
+ Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mClockFrame)
+ Utils.setClockStyle(mDigitalClock!!, mAnalogClock!!)
+ Utils.setClockSecondsEnabled(mDigitalClock!!, mAnalogClock!!)
+ }
+
+ // Schedule a runnable to update the date every quarter hour.
+ UiDataModel.uiDataModel.addQuarterHourCallback(mQuarterHourUpdater)
+
+ return fragmentView
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ val activity = requireActivity()
+
+ mDateFormat = getString(R.string.abbrev_wday_month_day_no_year)
+ mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year)
+
+ // Watch for system events that effect clock time or format.
+ if (mAlarmChangeReceiver != null) {
+ val filter = IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)
+ activity.registerReceiver(mAlarmChangeReceiver, filter)
+ }
+
+ // Resume can be invoked after changing the clock style or seconds display.
+ if (mDigitalClock != null && mAnalogClock != null) {
+ Utils.setClockStyle(mDigitalClock!!, mAnalogClock!!)
+ Utils.setClockSecondsEnabled(mDigitalClock!!, mAnalogClock!!)
+ }
+
+ val view = view
+ if (view?.findViewById<View?>(R.id.main_clock_left_pane) != null) {
+ // Center the main clock frame by hiding the world clocks when none are selected.
+ mCityList.setVisibility(if (mCityAdapter.getItemCount() == 0) {
+ View.GONE
+ } else {
+ View.VISIBLE
+ })
+ }
+
+ refreshAlarm()
+
+ // Alarm observer is null on L or later.
+ mAlarmObserver?.let {
+ val uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED)
+ activity.contentResolver.registerContentObserver(uri, false, it)
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ val activity = requireActivity()
+ if (mAlarmChangeReceiver != null) {
+ activity.unregisterReceiver(mAlarmChangeReceiver)
+ }
+ if (mAlarmObserver != null) {
+ activity.contentResolver.unregisterContentObserver(mAlarmObserver!!)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ UiDataModel.uiDataModel.removePeriodicCallback(mQuarterHourUpdater)
+ DataModel.dataModel.removeCityListener(mCityAdapter)
+ }
+
+ override fun onFabClick(fab: ImageView) {
+ startActivity(Intent(requireActivity(), CitySelectionActivity::class.java))
+ }
+
+ override fun onUpdateFab(fab: ImageView) {
+ fab.visibility = View.VISIBLE
+ fab.setImageResource(R.drawable.ic_public)
+ fab.contentDescription = fab.resources.getString(R.string.button_cities)
+ }
+
+ override fun onUpdateFabButtons(left: Button, right: Button) {
+ left.visibility = View.INVISIBLE
+ right.visibility = View.INVISIBLE
+ }
+
+ /**
+ * Refresh the next alarm time.
+ */
+ private fun refreshAlarm() {
+ if (mClockFrame != null) {
+ Utils.refreshAlarm(requireActivity(), mClockFrame)
+ } else {
+ mCityAdapter.refreshAlarm()
+ }
+ }
+
+ /**
+ * Long pressing over the main clock starts the screen saver.
+ */
+ private inner class StartScreenSaverListener : View.OnLongClickListener {
+ override fun onLongClick(view: View): Boolean {
+ startActivity(Intent(requireActivity(), ScreensaverActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock))
+ return true
+ }
+ }
+
+ /**
+ * Long pressing over the city list starts the screen saver.
+ */
+ private inner class CityListOnLongClickListener(
+ context: Context
+ ) : SimpleOnGestureListener(), View.OnTouchListener {
+ private val mGestureDetector = GestureDetector(context, this)
+
+ override fun onLongPress(e: MotionEvent) {
+ val view = view
+ view?.performLongClick()
+ }
+
+ override fun onDown(e: MotionEvent): Boolean {
+ return true
+ }
+
+ override fun onTouch(v: View, event: MotionEvent): Boolean {
+ return mGestureDetector.onTouchEvent(event)
+ }
+ }
+
+ /**
+ * This runnable executes at every quarter-hour (e.g. 1:00, 1:15, 1:30, 1:45, etc...) and
+ * updates the dates displayed within the UI. Quarter-hour increments were chosen to accommodate
+ * the "weirdest" timezones (e.g. Nepal is UTC/GMT +05:45).
+ */
+ private inner class QuarterHourRunnable : Runnable {
+ override fun run() {
+ mCityAdapter.notifyDataSetChanged()
+ }
+ }
+
+ /**
+ * Prior to L, a ContentObserver was used to monitor changes to the next scheduled alarm.
+ * In L and beyond this is accomplished via a system broadcast of
+ * [AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED].
+ */
+ private inner class AlarmObserverPreL : ContentObserver(Handler()) {
+ override fun onChange(selfChange: Boolean) {
+ refreshAlarm()
+ }
+ }
+
+ /**
+ * Update the display of the scheduled alarm as it changes.
+ */
+ private inner class AlarmChangedBroadcastReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ refreshAlarm()
+ }
+ }
+
+ /**
+ * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls
+ * the recyclerview or when the size/position of elements within the recyclerview changes.
+ */
+ private inner class ScrollPositionWatcher
+ : RecyclerView.OnScrollListener(), View.OnLayoutChangeListener {
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ setTabScrolledToTop(Utils.isScrolledToTop(mCityList))
+ }
+
+ override fun onLayoutChange(
+ v: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ setTabScrolledToTop(Utils.isScrolledToTop(mCityList))
+ }
+ }
+
+ /**
+ * This adapter lists all of the selected world clocks. Optionally, it also includes a clock at
+ * the top for the home timezone if "Automatic home clock" is turned on in settings and the
+ * current time at home does not match the current time in the timezone of the current location.
+ * If the phone is in portrait mode it will also include the main clock at the top.
+ */
+ private class SelectedCitiesAdapter(
+ private val mContext: Context,
+ private val mDateFormat: String?,
+ private val mDateFormatForAccessibility: String?
+ ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), CityListener {
+ private val mInflater = LayoutInflater.from(mContext)
+ private val mIsPortrait: Boolean = Utils.isPortrait(mContext)
+ private val mShowHomeClock: Boolean = DataModel.dataModel.showHomeClock
+
+ override fun getItemViewType(position: Int): Int {
+ return if (position == 0 && mIsPortrait) {
+ MAIN_CLOCK
+ } else WORLD_CLOCK
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val view = mInflater.inflate(viewType, parent, false)
+ return when (viewType) {
+ WORLD_CLOCK -> CityViewHolder(view)
+ MAIN_CLOCK -> MainClockViewHolder(view)
+ else -> throw IllegalArgumentException("View type not recognized")
+ }
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ when (val viewType = getItemViewType(position)) {
+ WORLD_CLOCK -> {
+ // Retrieve the city to bind.
+ val city: City
+ // If showing home clock, put it at the top
+ city = if (mShowHomeClock && position == (if (mIsPortrait) 1 else 0)) {
+ homeCity
+ } else {
+ val positionAdjuster = ((if (mIsPortrait) 1 else 0) +
+ if (mShowHomeClock) 1 else 0)
+ cities[position - positionAdjuster]
+ }
+ (holder as CityViewHolder).bind(mContext, city, position, mIsPortrait)
+ }
+ MAIN_CLOCK -> (holder as MainClockViewHolder).bind(mContext, mDateFormat,
+ mDateFormatForAccessibility, getItemCount() > 1)
+ else -> throw IllegalArgumentException("Unexpected view type: $viewType")
+ }
+ }
+
+ override fun getItemCount(): Int {
+ val mainClockCount = if (mIsPortrait) 1 else 0
+ val homeClockCount = if (mShowHomeClock) 1 else 0
+ val worldClockCount = cities.size
+ return mainClockCount + homeClockCount + worldClockCount
+ }
+
+ private val homeCity: City
+ get() = DataModel.dataModel.homeCity
+
+ private val cities: List<City>
+ get() = DataModel.dataModel.selectedCities as List<City>
+
+ fun refreshAlarm() {
+ if (mIsPortrait && getItemCount() > 0) {
+ notifyItemChanged(0)
+ }
+ }
+
+ override fun citiesChanged(oldCities: List<City>, newCities: List<City>) {
+ notifyDataSetChanged()
+ }
+
+ private class CityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val mName: TextView = itemView.findViewById(R.id.city_name)
+ private val mDigitalClock: TextClock = itemView.findViewById(R.id.digital_clock)
+ private val mAnalogClock: AnalogClock = itemView.findViewById(R.id.analog_clock)
+ private val mHoursAhead: TextView = itemView.findViewById(R.id.hours_ahead)
+
+ fun bind(context: Context, city: City, position: Int, isPortrait: Boolean) {
+ val cityTimeZoneId: String = city.timeZone.id
+
+ // Configure the digital clock or analog clock depending on the user preference.
+ if (DataModel.dataModel.clockStyle == DataModel.ClockStyle.ANALOG) {
+ mDigitalClock.visibility = View.GONE
+ mAnalogClock.visibility = View.VISIBLE
+ mAnalogClock.setTimeZone(cityTimeZoneId)
+ mAnalogClock.enableSeconds(false)
+ } else {
+ mAnalogClock.visibility = View.GONE
+ mDigitalClock.visibility = View.VISIBLE
+ mDigitalClock.timeZone = cityTimeZoneId
+ mDigitalClock.format12Hour = Utils.get12ModeFormat(0.3f, false)
+ mDigitalClock.format24Hour = Utils.get24ModeFormat(false)
+ }
+
+ // Supply top and bottom padding dynamically.
+ val res = context.resources
+ val padding = res.getDimensionPixelSize(R.dimen.medium_space_top)
+ val top = if (position == 0 && !isPortrait) 0 else padding
+ val left: Int = itemView.paddingLeft
+ val right: Int = itemView.paddingRight
+ val bottom: Int = itemView.paddingBottom
+ itemView.setPadding(left, top, right, bottom)
+
+ // Bind the city name.
+ mName.text = city.name
+
+ // Compute if the city week day matches the weekday of the current timezone.
+ val localCal = Calendar.getInstance(TimeZone.getDefault())
+ val cityCal: Calendar = Calendar.getInstance(city.timeZone)
+ val displayDayOfWeek =
+ localCal[Calendar.DAY_OF_WEEK] != cityCal[Calendar.DAY_OF_WEEK]
+
+ // Compare offset from UTC time on today's date (daylight savings time, etc.)
+ val currentTimeZone = TimeZone.getDefault()
+ val cityTimeZone = TimeZone.getTimeZone(cityTimeZoneId)
+ val currentTimeMillis = System.currentTimeMillis()
+ val currentUtcOffset = currentTimeZone.getOffset(currentTimeMillis).toLong()
+ val cityUtcOffset = cityTimeZone.getOffset(currentTimeMillis).toLong()
+ val offsetDelta = cityUtcOffset - currentUtcOffset
+
+ val hoursDifferent = (offsetDelta / DateUtils.HOUR_IN_MILLIS).toInt()
+ val minutesDifferent = (offsetDelta / DateUtils.MINUTE_IN_MILLIS).toInt() % 60
+ val displayMinutes = offsetDelta % DateUtils.HOUR_IN_MILLIS != 0L
+ val isAhead = hoursDifferent > 0 || (hoursDifferent == 0 &&
+ minutesDifferent > 0)
+ if (!Utils.isLandscape(context)) {
+ // Bind the number of hours ahead or behind, or hide if the time is the same.
+ val displayDifference = hoursDifferent != 0 || displayMinutes
+ mHoursAhead.visibility = if (displayDifference) View.VISIBLE else View.GONE
+ val timeString = Utils.createHoursDifferentString(
+ context, displayMinutes, isAhead, hoursDifferent, minutesDifferent)
+ mHoursAhead.text = if (displayDayOfWeek) {
+ context.getString(if (isAhead) {
+ R.string.world_hours_tomorrow
+ } else {
+ R.string.world_hours_yesterday
+ }, timeString)
+ } else {
+ timeString
+ }
+ } else {
+ // Only tomorrow/yesterday should be shown in landscape view.
+ mHoursAhead.visibility = if (displayDayOfWeek) View.VISIBLE else View.GONE
+ if (displayDayOfWeek) {
+ mHoursAhead.text = context.getString(if (isAhead) {
+ R.string.world_tomorrow
+ } else {
+ R.string.world_yesterday
+ })
+ }
+ }
+ }
+ }
+
+ private class MainClockViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val mHairline: View = itemView.findViewById(R.id.hairline)
+ private val mDigitalClock: TextClock = itemView.findViewById(R.id.digital_clock)
+ private val mAnalogClock: AnalogClock = itemView.findViewById(R.id.analog_clock)
+
+ init {
+ Utils.setClockIconTypeface(itemView)
+ }
+
+ fun bind(
+ context: Context,
+ dateFormat: String?,
+ dateFormatForAccessibility: String?,
+ showHairline: Boolean
+ ) {
+ Utils.refreshAlarm(context, itemView)
+
+ Utils.updateDate(dateFormat, dateFormatForAccessibility, itemView)
+ Utils.setClockStyle(mDigitalClock, mAnalogClock)
+ mHairline.visibility = if (showHairline) View.VISIBLE else View.GONE
+
+ Utils.setClockSecondsEnabled(mDigitalClock, mAnalogClock)
+ }
+ }
+
+ companion object {
+ private const val MAIN_CLOCK = R.layout.main_clock_frame
+ private const val WORLD_CLOCK = R.layout.world_clock_item
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClock.java b/src/com/android/deskclock/DeskClock.java
deleted file mode 100644
index e53fbeb..0000000
--- a/src/com/android/deskclock/DeskClock.java
+++ /dev/null
@@ -1,680 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ValueAnimator;
-import android.app.Fragment;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import androidx.annotation.StringRes;
-import com.google.android.material.snackbar.Snackbar;
-import com.google.android.material.tabs.TabLayout;
-import androidx.viewpager.widget.ViewPager;
-import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.widget.Toolbar;
-import android.view.KeyEvent;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
-import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
-import com.android.deskclock.actionbarmenu.OptionsMenuManager;
-import com.android.deskclock.actionbarmenu.SettingsMenuItemController;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.DataModel.SilentSetting;
-import com.android.deskclock.data.OnSilentSettingsListener;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.uidata.TabListener;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.widget.toast.SnackbarManager;
-
-import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING;
-import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
-import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static com.android.deskclock.AnimatorUtils.getScaleAnimator;
-
-/**
- * The main activity of the application which displays 4 different tabs contains alarms, world
- * clocks, timers and a stopwatch.
- */
-public class DeskClock extends BaseActivity
- implements FabContainer, LabelDialogFragment.AlarmLabelDialogHandler {
-
- /** Models the interesting state of display the {@link #mFab} button may inhabit. */
- private enum FabState { SHOWING, HIDE_ARMED, HIDING }
-
- /** Coordinates handling of context menu items. */
- private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
-
- /** Shrinks the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to nothing. */
- private final AnimatorSet mHideAnimation = new AnimatorSet();
-
- /** Grows the {@link #mFab}, {@link #mLeftButton} and {@link #mRightButton} to natural sizes. */
- private final AnimatorSet mShowAnimation = new AnimatorSet();
-
- /** Hides, updates, and shows only the {@link #mFab}; the buttons are untouched. */
- private final AnimatorSet mUpdateFabOnlyAnimation = new AnimatorSet();
-
- /** Hides, updates, and shows only the {@link #mLeftButton} and {@link #mRightButton}. */
- private final AnimatorSet mUpdateButtonsOnlyAnimation = new AnimatorSet();
-
- /** Automatically starts the {@link #mShowAnimation} after {@link #mHideAnimation} ends. */
- private final AnimatorListenerAdapter mAutoStartShowListener = new AutoStartShowListener();
-
- /** Updates the user interface to reflect the selected tab from the backing model. */
- private final TabListener mTabChangeWatcher = new TabChangeWatcher();
-
- /** Shows/hides a snackbar explaining which setting is suppressing alarms from firing. */
- private final OnSilentSettingsListener mSilentSettingChangeWatcher =
- new SilentSettingChangeWatcher();
-
- /** Displays a snackbar explaining why alarms may not fire or may fire silently. */
- private Runnable mShowSilentSettingSnackbarRunnable;
-
- /** The view to which snackbar items are anchored. */
- private View mSnackbarAnchor;
-
- /** The current display state of the {@link #mFab}. */
- private FabState mFabState = FabState.SHOWING;
-
- /** The single floating-action button shared across all tabs in the user interface. */
- private ImageView mFab;
-
- /** The button left of the {@link #mFab} shared across all tabs in the user interface. */
- private Button mLeftButton;
-
- /** The button right of the {@link #mFab} shared across all tabs in the user interface. */
- private Button mRightButton;
-
- /** The controller that shows the drop shadow when content is not scrolled to the top. */
- private DropShadowController mDropShadowController;
-
- /** The ViewPager that pages through the fragments representing the content of the tabs. */
- private ViewPager mFragmentTabPager;
-
- /** Generates the fragments that are displayed by the {@link #mFragmentTabPager}. */
- private FragmentTabPagerAdapter mFragmentTabPagerAdapter;
-
- /** The container that stores the tab headers. */
- private TabLayout mTabLayout;
-
- /** {@code true} when a settings change necessitates recreating this activity. */
- private boolean mRecreateActivity;
-
- @Override
- public void onNewIntent(Intent newIntent) {
- super.onNewIntent(newIntent);
-
- // Fragments may query the latest intent for information, so update the intent.
- setIntent(newIntent);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.desk_clock);
- mSnackbarAnchor = findViewById(R.id.content);
-
- // Configure the toolbar.
- final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
-
- final ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayShowTitleEnabled(false);
- }
-
- // Configure the menu item controllers add behavior to the toolbar.
- mOptionsMenuManager.addMenuItemController(
- new NightModeMenuItemController(this), new SettingsMenuItemController(this));
- mOptionsMenuManager.addMenuItemController(
- MenuItemControllerFactory.getInstance().buildMenuItemControllers(this));
-
- // Inflate the menu during creation to avoid a double layout pass. Otherwise, the menu
- // inflation occurs *after* the initial draw and a second layout pass adds in the menu.
- onCreateOptionsMenu(toolbar.getMenu());
-
- // Create the tabs that make up the user interface.
- mTabLayout = (TabLayout) findViewById(R.id.tabs);
- final int tabCount = UiDataModel.getUiDataModel().getTabCount();
- final boolean showTabLabel = getResources().getBoolean(R.bool.showTabLabel);
- final boolean showTabHorizontally = getResources().getBoolean(R.bool.showTabHorizontally);
- for (int i = 0; i < tabCount; i++) {
- final UiDataModel.Tab tabModel = UiDataModel.getUiDataModel().getTab(i);
- final @StringRes int labelResId = tabModel.getLabelResId();
-
- final TabLayout.Tab tab = mTabLayout.newTab()
- .setTag(tabModel)
- .setIcon(tabModel.getIconResId())
- .setContentDescription(labelResId);
-
- if (showTabLabel) {
- tab.setText(labelResId);
- tab.setCustomView(R.layout.tab_item);
-
- @SuppressWarnings("ConstantConditions")
- final TextView text = (TextView) tab.getCustomView()
- .findViewById(android.R.id.text1);
- text.setTextColor(mTabLayout.getTabTextColors());
-
- // Bind the icon to the TextView.
- final Drawable icon = tab.getIcon();
- if (showTabHorizontally) {
- // Remove the icon so it doesn't affect the minimum TabLayout height.
- tab.setIcon(null);
- text.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
- } else {
- text.setCompoundDrawablesRelativeWithIntrinsicBounds(null, icon, null, null);
- }
- }
-
- mTabLayout.addTab(tab);
- }
-
- // Configure the buttons shared by the tabs.
- mFab = (ImageView) findViewById(R.id.fab);
- mLeftButton = (Button) findViewById(R.id.left_button);
- mRightButton = (Button) findViewById(R.id.right_button);
-
- mFab.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- getSelectedDeskClockFragment().onFabClick(mFab);
- }
- });
- mLeftButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- getSelectedDeskClockFragment().onLeftButtonClick(mLeftButton);
- }
- });
- mRightButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View view) {
- getSelectedDeskClockFragment().onRightButtonClick(mRightButton);
- }
- });
-
- final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
-
- final ValueAnimator hideFabAnimation = getScaleAnimator(mFab, 1f, 0f);
- final ValueAnimator showFabAnimation = getScaleAnimator(mFab, 0f, 1f);
-
- final ValueAnimator leftHideAnimation = getScaleAnimator(mLeftButton, 1f, 0f);
- final ValueAnimator rightHideAnimation = getScaleAnimator(mRightButton, 1f, 0f);
- final ValueAnimator leftShowAnimation = getScaleAnimator(mLeftButton, 0f, 1f);
- final ValueAnimator rightShowAnimation = getScaleAnimator(mRightButton, 0f, 1f);
-
- hideFabAnimation.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- getSelectedDeskClockFragment().onUpdateFab(mFab);
- }
- });
-
- leftHideAnimation.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- getSelectedDeskClockFragment().onUpdateFabButtons(mLeftButton, mRightButton);
- }
- });
-
- // Build the reusable animations that hide and show the fab and left/right buttons.
- // These may be used independently or be chained together.
- mHideAnimation
- .setDuration(duration)
- .play(hideFabAnimation)
- .with(leftHideAnimation)
- .with(rightHideAnimation);
-
- mShowAnimation
- .setDuration(duration)
- .play(showFabAnimation)
- .with(leftShowAnimation)
- .with(rightShowAnimation);
-
- // Build the reusable animation that hides and shows only the fab.
- mUpdateFabOnlyAnimation
- .setDuration(duration)
- .play(showFabAnimation)
- .after(hideFabAnimation);
-
- // Build the reusable animation that hides and shows only the buttons.
- mUpdateButtonsOnlyAnimation
- .setDuration(duration)
- .play(leftShowAnimation)
- .with(rightShowAnimation)
- .after(leftHideAnimation)
- .after(rightHideAnimation);
-
- // Customize the view pager.
- mFragmentTabPagerAdapter = new FragmentTabPagerAdapter(this);
- mFragmentTabPager = (ViewPager) findViewById(R.id.desk_clock_pager);
- // Keep all four tabs to minimize jank.
- mFragmentTabPager.setOffscreenPageLimit(3);
- // Set Accessibility Delegate to null so view pager doesn't intercept movements and
- // prevent the fab from being selected.
- mFragmentTabPager.setAccessibilityDelegate(null);
- // Mirror changes made to the selected page of the view pager into UiDataModel.
- mFragmentTabPager.addOnPageChangeListener(new PageChangeWatcher());
- mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter);
-
- // Mirror changes made to the selected tab into UiDataModel.
- mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
- @Override
- public void onTabSelected(TabLayout.Tab tab) {
- UiDataModel.getUiDataModel().setSelectedTab((UiDataModel.Tab) tab.getTag());
- }
-
- @Override
- public void onTabUnselected(TabLayout.Tab tab) {
- }
-
- @Override
- public void onTabReselected(TabLayout.Tab tab) {
- }
- });
-
- // Honor changes to the selected tab from outside entities.
- UiDataModel.getUiDataModel().addTabListener(mTabChangeWatcher);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- DataModel.getDataModel().addSilentSettingsListener(mSilentSettingChangeWatcher);
- DataModel.getDataModel().setApplicationInForeground(true);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- final View dropShadow = findViewById(R.id.drop_shadow);
- mDropShadowController = new DropShadowController(dropShadow, UiDataModel.getUiDataModel(),
- mSnackbarAnchor.findViewById(R.id.tab_hairline));
-
- // ViewPager does not save state; this honors the selected tab in the user interface.
- updateCurrentTab();
- }
-
- @Override
- protected void onPostResume() {
- super.onPostResume();
-
- if (mRecreateActivity) {
- mRecreateActivity = false;
-
- // A runnable must be posted here or the new DeskClock activity will be recreated in a
- // paused state, even though it is the foreground activity.
- mFragmentTabPager.post(new Runnable() {
- @Override
- public void run() {
- recreate();
- }
- });
- }
- }
-
- @Override
- public void onPause() {
- if (mDropShadowController != null) {
- mDropShadowController.stop();
- mDropShadowController = null;
- }
-
- super.onPause();
- }
-
- @Override
- protected void onStop() {
- DataModel.getDataModel().removeSilentSettingsListener(mSilentSettingChangeWatcher);
- if (!isChangingConfigurations()) {
- DataModel.getDataModel().setApplicationInForeground(false);
- }
-
- super.onStop();
- }
-
- @Override
- protected void onDestroy() {
- UiDataModel.getUiDataModel().removeTabListener(mTabChangeWatcher);
- super.onDestroy();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- mOptionsMenuManager.onCreateOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- super.onPrepareOptionsMenu(menu);
- mOptionsMenuManager.onPrepareOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
- }
-
- /**
- * Called by the LabelDialogFormat class after the dialog is finished.
- */
- @Override
- public void onDialogLabelSet(Alarm alarm, String label, String tag) {
- final Fragment frag = getFragmentManager().findFragmentByTag(tag);
- if (frag instanceof AlarmClockFragment) {
- ((AlarmClockFragment) frag).setLabel(alarm, label);
- }
- }
-
- /**
- * Listens for keyboard activity for the tab fragments to handle if necessary. A tab may want to
- * respond to key presses even if they are not currently focused.
- */
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- return getSelectedDeskClockFragment().onKeyDown(keyCode,event)
- || super.onKeyDown(keyCode, event);
- }
-
- @Override
- public void updateFab(@UpdateFabFlag int updateType) {
- final DeskClockFragment f = getSelectedDeskClockFragment();
-
- switch (updateType & FAB_ANIMATION_MASK) {
- case FAB_SHRINK_AND_EXPAND:
- mUpdateFabOnlyAnimation.start();
- break;
- case FAB_IMMEDIATE:
- f.onUpdateFab(mFab);
- break;
- case FAB_MORPH:
- f.onMorphFab(mFab);
- break;
- }
- switch (updateType & FAB_REQUEST_FOCUS_MASK) {
- case FAB_REQUEST_FOCUS:
- mFab.requestFocus();
- break;
- }
- switch (updateType & BUTTONS_ANIMATION_MASK) {
- case BUTTONS_IMMEDIATE:
- f.onUpdateFabButtons(mLeftButton, mRightButton);
- break;
- case BUTTONS_SHRINK_AND_EXPAND:
- mUpdateButtonsOnlyAnimation.start();
- break;
- }
- switch (updateType & BUTTONS_DISABLE_MASK) {
- case BUTTONS_DISABLE:
- mLeftButton.setClickable(false);
- mRightButton.setClickable(false);
- break;
- }
- switch (updateType & FAB_AND_BUTTONS_SHRINK_EXPAND_MASK) {
- case FAB_AND_BUTTONS_SHRINK:
- mHideAnimation.start();
- break;
- case FAB_AND_BUTTONS_EXPAND:
- mShowAnimation.start();
- break;
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- // Recreate the activity if any settings have been changed
- if (requestCode == SettingsMenuItemController.REQUEST_CHANGE_SETTINGS
- && resultCode == RESULT_OK) {
- mRecreateActivity = true;
- }
- }
-
- /**
- * Configure the {@link #mFragmentTabPager} and {@link #mTabLayout} to display UiDataModel's
- * selected tab.
- */
- private void updateCurrentTab() {
- // Fetch the selected tab from the source of truth: UiDataModel.
- final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
-
- // Update the selected tab in the tablayout if it does not agree with UiDataModel.
- for (int i = 0; i < mTabLayout.getTabCount(); i++) {
- final TabLayout.Tab tab = mTabLayout.getTabAt(i);
- if (tab != null && tab.getTag() == selectedTab && !tab.isSelected()) {
- tab.select();
- break;
- }
- }
-
- // Update the selected fragment in the viewpager if it does not agree with UiDataModel.
- for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
- final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
- if (fragment.isTabSelected() && mFragmentTabPager.getCurrentItem() != i) {
- mFragmentTabPager.setCurrentItem(i);
- break;
- }
- }
- }
-
- /**
- * @return the DeskClockFragment that is currently selected according to UiDataModel
- */
- private DeskClockFragment getSelectedDeskClockFragment() {
- for (int i = 0; i < mFragmentTabPagerAdapter.getCount(); i++) {
- final DeskClockFragment fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i);
- if (fragment.isTabSelected()) {
- return fragment;
- }
- }
- final UiDataModel.Tab selectedTab = UiDataModel.getUiDataModel().getSelectedTab();
- throw new IllegalStateException("Unable to locate selected fragment (" + selectedTab + ")");
- }
-
- /**
- * @return a Snackbar that displays the message with the given id for 5 seconds
- */
- private Snackbar createSnackbar(@StringRes int messageId) {
- return Snackbar.make(mSnackbarAnchor, messageId, 5000 /* duration */);
- }
-
- /**
- * As the view pager changes the selected page, update the model to record the new selected tab.
- */
- private final class PageChangeWatcher implements OnPageChangeListener {
-
- /** The last reported page scroll state; used to detect exotic state changes. */
- private int mPriorState = SCROLL_STATE_IDLE;
-
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- // Only hide the fab when a non-zero drag distance is detected. This prevents
- // over-scrolling from needlessly hiding the fab.
- if (mFabState == FabState.HIDE_ARMED && positionOffsetPixels != 0) {
- mFabState = FabState.HIDING;
- mHideAnimation.start();
- }
- }
-
- @Override
- public void onPageScrollStateChanged(int state) {
- if (mPriorState == SCROLL_STATE_IDLE && state == SCROLL_STATE_SETTLING) {
- // The user has tapped a tab button; play the hide and show animations linearly.
- mHideAnimation.addListener(mAutoStartShowListener);
- mHideAnimation.start();
- mFabState = FabState.HIDING;
- } else if (mPriorState == SCROLL_STATE_SETTLING && state == SCROLL_STATE_DRAGGING) {
- // The user has interrupted settling on a tab and the fab button must be re-hidden.
- if (mShowAnimation.isStarted()) {
- mShowAnimation.cancel();
- }
- if (mHideAnimation.isStarted()) {
- // Let the hide animation finish naturally; don't auto show when it ends.
- mHideAnimation.removeListener(mAutoStartShowListener);
- } else {
- // Start and immediately end the hide animation to jump to the hidden state.
- mHideAnimation.start();
- mHideAnimation.end();
- }
- mFabState = FabState.HIDING;
-
- } else if (state != SCROLL_STATE_DRAGGING && mFabState == FabState.HIDING) {
- // The user has lifted their finger; show the buttons now or after hide ends.
- if (mHideAnimation.isStarted()) {
- // Finish the hide animation and then start the show animation.
- mHideAnimation.addListener(mAutoStartShowListener);
- } else {
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
- mShowAnimation.start();
-
- // The animation to show the fab has begun; update the state to showing.
- mFabState = FabState.SHOWING;
- }
- } else if (state == SCROLL_STATE_DRAGGING) {
- // The user has started a drag so arm the hide animation.
- mFabState = FabState.HIDE_ARMED;
- }
-
- // Update the last known state.
- mPriorState = state;
- }
-
- @Override
- public void onPageSelected(int position) {
- mFragmentTabPagerAdapter.getDeskClockFragment(position).selectTab();
- }
- }
-
- /**
- * If this listener is attached to {@link #mHideAnimation} when it ends, the corresponding
- * {@link #mShowAnimation} is automatically started.
- */
- private final class AutoStartShowListener extends AnimatorListenerAdapter {
- @Override
- public void onAnimationEnd(Animator animation) {
- // Prepare the hide animation for its next use; by default do not auto-show after hide.
- mHideAnimation.removeListener(mAutoStartShowListener);
-
- // Update the buttons now that they are no longer visible.
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-
- // Automatically start the grow animation now that shrinking is complete.
- mShowAnimation.start();
-
- // The animation to show the fab has begun; update the state to showing.
- mFabState = FabState.SHOWING;
- }
- }
-
- /**
- * Shows/hides a snackbar as silencing settings are enabled/disabled.
- */
- private final class SilentSettingChangeWatcher implements OnSilentSettingsListener {
- @Override
- public void onSilentSettingsChange(SilentSetting before, SilentSetting after) {
- if (mShowSilentSettingSnackbarRunnable != null) {
- mSnackbarAnchor.removeCallbacks(mShowSilentSettingSnackbarRunnable);
- mShowSilentSettingSnackbarRunnable = null;
- }
-
- if (after == null) {
- SnackbarManager.dismiss();
- } else {
- mShowSilentSettingSnackbarRunnable = new ShowSilentSettingSnackbarRunnable(after);
- mSnackbarAnchor.postDelayed(mShowSilentSettingSnackbarRunnable, SECOND_IN_MILLIS);
- }
- }
- }
-
- /**
- * Displays a snackbar that indicates a system setting is currently silencing alarms.
- */
- private final class ShowSilentSettingSnackbarRunnable implements Runnable {
-
- private final SilentSetting mSilentSetting;
-
- private ShowSilentSettingSnackbarRunnable(SilentSetting silentSetting) {
- mSilentSetting = silentSetting;
- }
-
- public void run() {
- // Create a snackbar with a message explaining the setting that is silencing alarms.
- final Snackbar snackbar = createSnackbar(mSilentSetting.getLabelResId());
-
- // Set the associated corrective action if one exists.
- if (mSilentSetting.isActionEnabled(DeskClock.this)) {
- final int actionResId = mSilentSetting.getActionResId();
- snackbar.setAction(actionResId, mSilentSetting.getActionListener());
- }
-
- SnackbarManager.show(snackbar);
- }
- }
-
- /**
- * As the model reports changes to the selected tab, update the user interface.
- */
- private final class TabChangeWatcher implements TabListener {
- @Override
- public void selectedTabChanged(UiDataModel.Tab oldSelectedTab,
- UiDataModel.Tab newSelectedTab) {
- // Update the view pager and tab layout to agree with the model.
- updateCurrentTab();
-
- // Avoid sending events for the initial tab selection on launch and re-selecting a tab
- // after a configuration change.
- if (DataModel.getDataModel().isApplicationInForeground()) {
- switch (newSelectedTab) {
- case ALARMS:
- Events.sendAlarmEvent(R.string.action_show, R.string.label_deskclock);
- break;
- case CLOCKS:
- Events.sendClockEvent(R.string.action_show, R.string.label_deskclock);
- break;
- case TIMERS:
- Events.sendTimerEvent(R.string.action_show, R.string.label_deskclock);
- break;
- case STOPWATCH:
- Events.sendStopwatchEvent(R.string.action_show, R.string.label_deskclock);
- break;
- }
- }
-
- // If the hide animation has already completed, the buttons must be updated now when the
- // new tab is known. Otherwise they are updated at the end of the hide animation.
- if (!mHideAnimation.isStarted()) {
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClock.kt b/src/com/android/deskclock/DeskClock.kt
new file mode 100644
index 0000000..363a3da
--- /dev/null
+++ b/src/com/android/deskclock/DeskClock.kt
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.text.format.DateUtils
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.appcompat.app.ActionBar
+import androidx.appcompat.widget.Toolbar
+import androidx.fragment.app.Fragment
+import androidx.viewpager.widget.ViewPager
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener
+import androidx.viewpager.widget.ViewPager.SCROLL_STATE_DRAGGING
+import androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE
+import androidx.viewpager.widget.ViewPager.SCROLL_STATE_SETTLING
+
+import com.android.deskclock.FabContainer.UpdateFabFlag
+import com.android.deskclock.LabelDialogFragment.AlarmLabelDialogHandler
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory
+import com.android.deskclock.actionbarmenu.NightModeMenuItemController
+import com.android.deskclock.actionbarmenu.OptionsMenuManager
+import com.android.deskclock.actionbarmenu.SettingsMenuItemController
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.OnSilentSettingsListener
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.uidata.TabListener
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.widget.toast.SnackbarManager
+
+import com.google.android.material.snackbar.Snackbar
+import com.google.android.material.tabs.TabLayout
+
+/**
+ * The main activity of the application which displays 4 different tabs contains alarms, world
+ * clocks, timers and a stopwatch.
+ */
+class DeskClock : BaseActivity(), FabContainer, AlarmLabelDialogHandler {
+ /** Models the interesting state of display the [.mFab] button may inhabit. */
+ private enum class FabState {
+ SHOWING, HIDE_ARMED, HIDING
+ }
+
+ /** Coordinates handling of context menu items. */
+ private val mOptionsMenuManager = OptionsMenuManager()
+
+ /** Shrinks the [.mFab], [.mLeftButton] and [.mRightButton] to nothing. */
+ private val mHideAnimation = AnimatorSet()
+
+ /** Grows the [.mFab], [.mLeftButton] and [.mRightButton] to natural sizes. */
+ private val mShowAnimation = AnimatorSet()
+
+ /** Hides, updates, and shows only the [.mFab]; the buttons are untouched. */
+ private val mUpdateFabOnlyAnimation = AnimatorSet()
+
+ /** Hides, updates, and shows only the [.mLeftButton] and [.mRightButton]. */
+ private val mUpdateButtonsOnlyAnimation = AnimatorSet()
+
+ /** Automatically starts the [.mShowAnimation] after [.mHideAnimation] ends. */
+ private val mAutoStartShowListener: AnimatorListenerAdapter = AutoStartShowListener()
+
+ /** Updates the user interface to reflect the selected tab from the backing model. */
+ private val mTabChangeWatcher: TabListener = TabChangeWatcher()
+
+ /** Shows/hides a snackbar explaining which setting is suppressing alarms from firing. */
+ private val mSilentSettingChangeWatcher: OnSilentSettingsListener = SilentSettingChangeWatcher()
+
+ /** Displays a snackbar explaining why alarms may not fire or may fire silently. */
+ private var mShowSilentSettingSnackbarRunnable: Runnable? = null
+
+ /** The view to which snackbar items are anchored. */
+ private lateinit var mSnackbarAnchor: View
+
+ /** The current display state of the [.mFab]. */
+ private var mFabState = FabState.SHOWING
+
+ /** The single floating-action button shared across all tabs in the user interface. */
+ private lateinit var mFab: ImageView
+
+ /** The button left of the [.mFab] shared across all tabs in the user interface. */
+ private lateinit var mLeftButton: Button
+
+ /** The button right of the [.mFab] shared across all tabs in the user interface. */
+ private lateinit var mRightButton: Button
+
+ /** The controller that shows the drop shadow when content is not scrolled to the top. */
+ private var mDropShadowController: DropShadowController? = null
+
+ /** The ViewPager that pages through the fragments representing the content of the tabs. */
+ private lateinit var mFragmentTabPager: ViewPager
+
+ /** Generates the fragments that are displayed by the [.mFragmentTabPager]. */
+ private lateinit var mFragmentTabPagerAdapter: FragmentTabPagerAdapter
+
+ /** The container that stores the tab headers. */
+ private lateinit var mTabLayout: TabLayout
+
+ /** `true` when a settings change necessitates recreating this activity. */
+ private var mRecreateActivity = false
+
+ override fun onNewIntent(newIntent: Intent) {
+ super.onNewIntent(newIntent)
+
+ // Fragments may query the latest intent for information, so update the intent.
+ setIntent(newIntent)
+ }
+
+ protected override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.desk_clock)
+ mSnackbarAnchor = findViewById(R.id.content)
+
+ // Configure the toolbar.
+ val toolbar: Toolbar = findViewById(R.id.toolbar) as Toolbar
+ setSupportActionBar(toolbar)
+
+ val actionBar: ActionBar? = getSupportActionBar()
+ actionBar?.setDisplayShowTitleEnabled(false)
+
+ // Configure the menu item controllers add behavior to the toolbar.
+ mOptionsMenuManager.addMenuItemController(
+ NightModeMenuItemController(this), SettingsMenuItemController(this))
+ mOptionsMenuManager.addMenuItemController(
+ *MenuItemControllerFactory.buildMenuItemControllers(this))
+
+ // Inflate the menu during creation to avoid a double layout pass. Otherwise, the menu
+ // inflation occurs *after* the initial draw and a second layout pass adds in the menu.
+ onCreateOptionsMenu(toolbar.getMenu())
+
+ // Create the tabs that make up the user interface.
+ mTabLayout = findViewById(R.id.tabs) as TabLayout
+ val tabCount: Int = UiDataModel.uiDataModel.tabCount
+ val showTabLabel: Boolean = getResources().getBoolean(R.bool.showTabLabel)
+ val showTabHorizontally: Boolean = getResources().getBoolean(R.bool.showTabHorizontally)
+ for (i in 0 until tabCount) {
+ val tabModel: UiDataModel.Tab = UiDataModel.uiDataModel.getTab(i)
+ @StringRes val labelResId: Int = tabModel.labelResId
+
+ val tab: TabLayout.Tab = mTabLayout.newTab()
+ .setTag(tabModel)
+ .setIcon(tabModel.iconResId)
+ .setContentDescription(labelResId)
+
+ if (showTabLabel) {
+ tab.setText(labelResId)
+ tab.setCustomView(R.layout.tab_item)
+
+ val text = tab.getCustomView()!!.findViewById(android.R.id.text1) as TextView
+ text.setTextColor(mTabLayout.getTabTextColors())
+
+ // Bind the icon to the TextView.
+ val icon: Drawable? = tab.getIcon()
+ if (showTabHorizontally) {
+ // Remove the icon so it doesn't affect the minimum TabLayout height.
+ tab.setIcon(null)
+ text.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
+ } else {
+ text.setCompoundDrawablesRelativeWithIntrinsicBounds(null, icon, null, null)
+ }
+ }
+
+ mTabLayout.addTab(tab)
+ }
+
+ // Configure the buttons shared by the tabs.
+ mFab = findViewById(R.id.fab) as ImageView
+ mLeftButton = findViewById(R.id.left_button) as Button
+ mRightButton = findViewById(R.id.right_button) as Button
+
+ mFab.setOnClickListener { selectedDeskClockFragment.onFabClick(mFab) }
+ mLeftButton.setOnClickListener {
+ selectedDeskClockFragment.onLeftButtonClick(mLeftButton)
+ }
+ mRightButton.setOnClickListener {
+ selectedDeskClockFragment.onRightButtonClick(mRightButton)
+ }
+
+ val duration: Long = UiDataModel.uiDataModel.shortAnimationDuration
+
+ val hideFabAnimation = AnimatorUtils.getScaleAnimator(mFab, 1f, 0f)
+ val showFabAnimation = AnimatorUtils.getScaleAnimator(mFab, 0f, 1f)
+
+ val leftHideAnimation = AnimatorUtils.getScaleAnimator(mLeftButton, 1f, 0f)
+ val rightHideAnimation = AnimatorUtils.getScaleAnimator(mRightButton, 1f, 0f)
+ val leftShowAnimation = AnimatorUtils.getScaleAnimator(mLeftButton, 0f, 1f)
+ val rightShowAnimation = AnimatorUtils.getScaleAnimator(mRightButton, 0f, 1f)
+
+ hideFabAnimation.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ selectedDeskClockFragment.onUpdateFab(mFab)
+ }
+ })
+
+ leftHideAnimation.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ selectedDeskClockFragment.onUpdateFabButtons(mLeftButton, mRightButton)
+ }
+ })
+
+ // Build the reusable animations that hide and show the fab and left/right buttons.
+ // These may be used independently or be chained together.
+ mHideAnimation
+ .setDuration(duration)
+ .play(hideFabAnimation)
+ .with(leftHideAnimation)
+ .with(rightHideAnimation)
+
+ mShowAnimation
+ .setDuration(duration)
+ .play(showFabAnimation)
+ .with(leftShowAnimation)
+ .with(rightShowAnimation)
+
+ // Build the reusable animation that hides and shows only the fab.
+ mUpdateFabOnlyAnimation
+ .setDuration(duration)
+ .play(showFabAnimation)
+ .after(hideFabAnimation)
+
+ // Build the reusable animation that hides and shows only the buttons.
+ mUpdateButtonsOnlyAnimation
+ .setDuration(duration)
+ .play(leftShowAnimation)
+ .with(rightShowAnimation)
+ .after(leftHideAnimation)
+ .after(rightHideAnimation)
+
+ // Customize the view pager.
+ mFragmentTabPagerAdapter = FragmentTabPagerAdapter(this)
+ mFragmentTabPager = findViewById(R.id.desk_clock_pager) as ViewPager
+ // Keep all four tabs to minimize jank.
+ mFragmentTabPager.setOffscreenPageLimit(3)
+ // Set Accessibility Delegate to null so view pager doesn't intercept movements and
+ // prevent the fab from being selected.
+ mFragmentTabPager.setAccessibilityDelegate(null)
+ // Mirror changes made to the selected page of the view pager into UiDataModel.
+ mFragmentTabPager.addOnPageChangeListener(PageChangeWatcher())
+ mFragmentTabPager.setAdapter(mFragmentTabPagerAdapter)
+
+ // Mirror changes made to the selected tab into UiDataModel.
+ mTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
+ override fun onTabSelected(tab: TabLayout.Tab) {
+ UiDataModel.uiDataModel.selectedTab = tab.getTag() as UiDataModel.Tab
+ }
+
+ override fun onTabUnselected(tab: TabLayout.Tab) {
+ }
+
+ override fun onTabReselected(tab: TabLayout.Tab) {
+ }
+ })
+
+ // Honor changes to the selected tab from outside entities.
+ UiDataModel.uiDataModel.addTabListener(mTabChangeWatcher)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ DataModel.dataModel.addSilentSettingsListener(mSilentSettingChangeWatcher)
+ DataModel.dataModel.isApplicationInForeground = true
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ val dropShadow: View = findViewById(R.id.drop_shadow)
+ mDropShadowController = DropShadowController(dropShadow, UiDataModel.uiDataModel,
+ mSnackbarAnchor.findViewById(R.id.tab_hairline))
+
+ // ViewPager does not save state; this honors the selected tab in the user interface.
+ updateCurrentTab()
+ }
+
+ override fun onPostResume() {
+ super.onPostResume()
+
+ if (mRecreateActivity) {
+ mRecreateActivity = false
+
+ // A runnable must be posted here or the new DeskClock activity will be recreated in a
+ // paused state, even though it is the foreground activity.
+ mFragmentTabPager.post(Runnable { recreate() })
+ }
+ }
+
+ override fun onPause() {
+ if (mDropShadowController != null) {
+ mDropShadowController!!.stop()
+ mDropShadowController = null
+ }
+
+ super.onPause()
+ }
+
+ override fun onStop() {
+ DataModel.dataModel.removeSilentSettingsListener(mSilentSettingChangeWatcher)
+ if (!isChangingConfigurations()) {
+ DataModel.dataModel.isApplicationInForeground = false
+ }
+
+ super.onStop()
+ }
+
+ override fun onDestroy() {
+ UiDataModel.uiDataModel.removeTabListener(mTabChangeWatcher)
+ super.onDestroy()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ mOptionsMenuManager.onCreateOptionsMenu(menu)
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ super.onPrepareOptionsMenu(menu)
+ mOptionsMenuManager.onPrepareOptionsMenu(menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item)
+ }
+
+ /**
+ * Called by the LabelDialogFormat class after the dialog is finished.
+ */
+ override fun onDialogLabelSet(alarm: Alarm, label: String, tag: String) {
+ val frag: Fragment? = supportFragmentManager.findFragmentByTag(tag)
+ if (frag is AlarmClockFragment) {
+ frag.setLabel(alarm, label)
+ }
+ }
+
+ /**
+ * Listens for keyboard activity for the tab fragments to handle if necessary. A tab may want to
+ * respond to key presses even if they are not currently focused.
+ */
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ return (selectedDeskClockFragment.onKeyDown(keyCode, event) ||
+ super.onKeyDown(keyCode, event))
+ }
+
+ override fun updateFab(@UpdateFabFlag updateType: Int) {
+ val f = selectedDeskClockFragment
+
+ when (updateType and FabContainer.FAB_ANIMATION_MASK) {
+ FabContainer.FAB_SHRINK_AND_EXPAND -> mUpdateFabOnlyAnimation.start()
+ FabContainer.FAB_IMMEDIATE -> f.onUpdateFab(mFab)
+ FabContainer.FAB_MORPH -> f.onMorphFab(mFab)
+ }
+ when (updateType and FabContainer.FAB_REQUEST_FOCUS_MASK) {
+ FabContainer.FAB_REQUEST_FOCUS -> mFab.requestFocus()
+ }
+ when (updateType and FabContainer.BUTTONS_ANIMATION_MASK) {
+ FabContainer.BUTTONS_IMMEDIATE -> f.onUpdateFabButtons(mLeftButton, mRightButton)
+ FabContainer.BUTTONS_SHRINK_AND_EXPAND -> mUpdateButtonsOnlyAnimation.start()
+ }
+ when (updateType and FabContainer.BUTTONS_DISABLE_MASK) {
+ FabContainer.BUTTONS_DISABLE -> {
+ mLeftButton.isClickable = false
+ mRightButton.isClickable = false
+ }
+ }
+ when (updateType and FabContainer.FAB_AND_BUTTONS_SHRINK_EXPAND_MASK) {
+ FabContainer.FAB_AND_BUTTONS_SHRINK -> mHideAnimation.start()
+ FabContainer.FAB_AND_BUTTONS_EXPAND -> mShowAnimation.start()
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ // Recreate the activity if any settings have been changed
+ if (requestCode == SettingsMenuItemController.REQUEST_CHANGE_SETTINGS &&
+ resultCode == RESULT_OK) {
+ mRecreateActivity = true
+ }
+ }
+
+ /**
+ * Configure the [.mFragmentTabPager] and [.mTabLayout] to display UiDataModel's
+ * selected tab.
+ */
+ private fun updateCurrentTab() {
+ // Fetch the selected tab from the source of truth: UiDataModel.
+ val selectedTab: UiDataModel.Tab = UiDataModel.uiDataModel.selectedTab
+
+ // Update the selected tab in the tablayout if it does not agree with UiDataModel.
+ for (i in 0 until mTabLayout.getTabCount()) {
+ val tab: TabLayout.Tab? = mTabLayout.getTabAt(i)
+ if (tab?.getTag() == selectedTab && !tab.isSelected()) {
+ tab.select()
+ break
+ }
+ }
+
+ // Update the selected fragment in the viewpager if it does not agree with UiDataModel.
+ for (i in 0 until mFragmentTabPagerAdapter.count) {
+ val fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i)
+ if (fragment.isTabSelected && mFragmentTabPager.getCurrentItem() != i) {
+ mFragmentTabPager.setCurrentItem(i)
+ break
+ }
+ }
+ }
+
+ /**
+ * @return the DeskClockFragment that is currently selected according to UiDataModel
+ */
+ private val selectedDeskClockFragment: DeskClockFragment
+ get() {
+ for (i in 0 until mFragmentTabPagerAdapter.count) {
+ val fragment = mFragmentTabPagerAdapter.getDeskClockFragment(i)
+ if (fragment.isTabSelected) {
+ return fragment
+ }
+ }
+ val selectedTab: UiDataModel.Tab = UiDataModel.uiDataModel.selectedTab
+ throw IllegalStateException("Unable to locate selected fragment ($selectedTab)")
+ }
+
+ /**
+ * @return a Snackbar that displays the message with the given id for 5 seconds
+ */
+ private fun createSnackbar(@StringRes messageId: Int): Snackbar {
+ return Snackbar.make(mSnackbarAnchor, messageId, 5000)
+ }
+
+ /**
+ * As the view pager changes the selected page, update the model to record the new selected tab.
+ */
+ private inner class PageChangeWatcher : OnPageChangeListener {
+ /** The last reported page scroll state; used to detect exotic state changes. */
+ private var mPriorState: Int = SCROLL_STATE_IDLE
+
+ override fun onPageScrolled(
+ position: Int,
+ positionOffset: Float,
+ positionOffsetPixels: Int
+ ) {
+ // Only hide the fab when a non-zero drag distance is detected. This prevents
+ // over-scrolling from needlessly hiding the fab.
+ if (mFabState == FabState.HIDE_ARMED && positionOffsetPixels != 0) {
+ mFabState = FabState.HIDING
+ mHideAnimation.start()
+ }
+ }
+
+ override fun onPageScrollStateChanged(state: Int) {
+ if (mPriorState == SCROLL_STATE_IDLE && state == SCROLL_STATE_SETTLING) {
+ // The user has tapped a tab button; play the hide and show animations linearly.
+ mHideAnimation.addListener(mAutoStartShowListener)
+ mHideAnimation.start()
+ mFabState = FabState.HIDING
+ } else if (mPriorState == SCROLL_STATE_SETTLING && state == SCROLL_STATE_DRAGGING) {
+ // The user has interrupted settling on a tab and the fab button must be re-hidden.
+ if (mShowAnimation.isStarted) {
+ mShowAnimation.cancel()
+ }
+ if (mHideAnimation.isStarted) {
+ // Let the hide animation finish naturally; don't auto show when it ends.
+ mHideAnimation.removeListener(mAutoStartShowListener)
+ } else {
+ // Start and immediately end the hide animation to jump to the hidden state.
+ mHideAnimation.start()
+ mHideAnimation.end()
+ }
+ mFabState = FabState.HIDING
+ } else if (state != SCROLL_STATE_DRAGGING && mFabState == FabState.HIDING) {
+ // The user has lifted their finger; show the buttons now or after hide ends.
+ if (mHideAnimation.isStarted) {
+ // Finish the hide animation and then start the show animation.
+ mHideAnimation.addListener(mAutoStartShowListener)
+ } else {
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+ mShowAnimation.start()
+
+ // The animation to show the fab has begun; update the state to showing.
+ mFabState = FabState.SHOWING
+ }
+ } else if (state == SCROLL_STATE_DRAGGING) {
+ // The user has started a drag so arm the hide animation.
+ mFabState = FabState.HIDE_ARMED
+ }
+
+ // Update the last known state.
+ mPriorState = state
+ }
+
+ override fun onPageSelected(position: Int) {
+ mFragmentTabPagerAdapter.getDeskClockFragment(position).selectTab()
+ }
+ }
+
+ /**
+ * If this listener is attached to [.mHideAnimation] when it ends, the corresponding
+ * [.mShowAnimation] is automatically started.
+ */
+ private inner class AutoStartShowListener : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ // Prepare the hide animation for its next use; by default do not auto-show after hide.
+ mHideAnimation.removeListener(mAutoStartShowListener)
+
+ // Update the buttons now that they are no longer visible.
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+ // Automatically start the grow animation now that shrinking is complete.
+ mShowAnimation.start()
+
+ // The animation to show the fab has begun; update the state to showing.
+ mFabState = FabState.SHOWING
+ }
+ }
+
+ /**
+ * Shows/hides a snackbar as silencing settings are enabled/disabled.
+ */
+ private inner class SilentSettingChangeWatcher : OnSilentSettingsListener {
+ override fun onSilentSettingsChange(
+ before: DataModel.SilentSetting?,
+ after: DataModel.SilentSetting?
+ ) {
+ if (mShowSilentSettingSnackbarRunnable != null) {
+ mSnackbarAnchor.removeCallbacks(mShowSilentSettingSnackbarRunnable)
+ mShowSilentSettingSnackbarRunnable = null
+ }
+
+ if (after == null) {
+ SnackbarManager.dismiss()
+ } else {
+ mShowSilentSettingSnackbarRunnable = ShowSilentSettingSnackbarRunnable(after)
+ mSnackbarAnchor.postDelayed(mShowSilentSettingSnackbarRunnable,
+ DateUtils.SECOND_IN_MILLIS)
+ }
+ }
+ }
+
+ /**
+ * Displays a snackbar that indicates a system setting is currently silencing alarms.
+ */
+ private inner class ShowSilentSettingSnackbarRunnable(
+ private val mSilentSetting: DataModel.SilentSetting
+ ) : Runnable {
+ override fun run() {
+ // Create a snackbar with a message explaining the setting that is silencing alarms.
+ val snackbar: Snackbar = createSnackbar(mSilentSetting.labelResId)
+
+ // Set the associated corrective action if one exists.
+ if (mSilentSetting.isActionEnabled(this@DeskClock)) {
+ val actionResId: Int = mSilentSetting.actionResId
+ snackbar.setAction(actionResId, mSilentSetting.actionListener)
+ }
+
+ SnackbarManager.show(snackbar)
+ }
+ }
+
+ /**
+ * As the model reports changes to the selected tab, update the user interface.
+ */
+ private inner class TabChangeWatcher : TabListener {
+ override fun selectedTabChanged(
+ oldSelectedTab: UiDataModel.Tab,
+ newSelectedTab: UiDataModel.Tab
+ ) {
+ // Update the view pager and tab layout to agree with the model.
+ updateCurrentTab()
+
+ // Avoid sending events for the initial tab selection on launch and re-selecting a tab
+ // after a configuration change.
+ if (DataModel.dataModel.isApplicationInForeground) {
+ when (newSelectedTab) {
+ UiDataModel.Tab.ALARMS -> {
+ Events.sendAlarmEvent(R.string.action_show, R.string.label_deskclock)
+ }
+ UiDataModel.Tab.CLOCKS -> {
+ Events.sendClockEvent(R.string.action_show, R.string.label_deskclock)
+ }
+ UiDataModel.Tab.TIMERS -> {
+ Events.sendTimerEvent(R.string.action_show, R.string.label_deskclock)
+ }
+ UiDataModel.Tab.STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_show, R.string.label_deskclock)
+ }
+ }
+ }
+
+ // If the hide animation has already completed, the buttons must be updated now when the
+ // new tab is known. Otherwise they are updated at the end of the hide animation.
+ if (!mHideAnimation.isStarted) {
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockApplication.java b/src/com/android/deskclock/DeskClockApplication.java
deleted file mode 100644
index 395d385..0000000
--- a/src/com/android/deskclock/DeskClockApplication.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.annotation.TargetApi;
-import android.app.Application;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.preference.PreferenceManager;
-
-import com.android.deskclock.controller.Controller;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.LogEventTracker;
-import com.android.deskclock.uidata.UiDataModel;
-
-public class DeskClockApplication extends Application {
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- final Context applicationContext = getApplicationContext();
- final SharedPreferences prefs = getDefaultSharedPreferences(applicationContext);
-
- DataModel.getDataModel().init(applicationContext, prefs);
- UiDataModel.getUiDataModel().init(applicationContext, prefs);
- Controller.getController().setContext(applicationContext);
- Controller.getController().addEventTracker(new LogEventTracker(applicationContext));
- }
-
- /**
- * Returns the default {@link SharedPreferences} instance from the underlying storage context.
- */
- @TargetApi(Build.VERSION_CODES.N)
- private static SharedPreferences getDefaultSharedPreferences(Context context) {
- final Context storageContext;
- if (Utils.isNOrLater()) {
- // All N devices have split storage areas. Migrate the existing preferences into the new
- // device encrypted storage area if that has not yet occurred.
- final String name = PreferenceManager.getDefaultSharedPreferencesName(context);
- storageContext = context.createDeviceProtectedStorageContext();
- if (!storageContext.moveSharedPreferencesFrom(context, name)) {
- LogUtils.wtf("Failed to migrate shared preferences");
- }
- } else {
- storageContext = context;
- }
- return PreferenceManager.getDefaultSharedPreferences(storageContext);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockApplication.kt b/src/com/android/deskclock/DeskClockApplication.kt
new file mode 100644
index 0000000..7f0df93
--- /dev/null
+++ b/src/com/android/deskclock/DeskClockApplication.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.annotation.TargetApi
+import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Build
+import android.preference.PreferenceManager
+
+import com.android.deskclock.controller.Controller
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.LogEventTracker
+import com.android.deskclock.uidata.UiDataModel
+
+class DeskClockApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+
+ val applicationContext = applicationContext
+ val prefs = getDefaultSharedPreferences(applicationContext)
+
+ DataModel.dataModel.init(applicationContext, prefs)
+ UiDataModel.uiDataModel.init(applicationContext, prefs)
+ Controller.getController().setContext(applicationContext)
+ Controller.getController().addEventTracker(LogEventTracker(applicationContext))
+ }
+
+ companion object {
+ /**
+ * Returns the default [SharedPreferences] instance from the underlying storage context.
+ */
+ @TargetApi(Build.VERSION_CODES.N)
+ private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
+ val storageContext: Context
+ if (Utils.isNOrLater) {
+ // All N devices have split storage areas. Migrate the existing preferences
+ // into the new device encrypted storage area if that has not yet occurred.
+ val name = PreferenceManager.getDefaultSharedPreferencesName(context)
+ storageContext = context.createDeviceProtectedStorageContext()
+ if (!storageContext.moveSharedPreferencesFrom(context, name)) {
+ LogUtils.wtf("Failed to migrate shared preferences")
+ }
+ } else {
+ storageContext = context
+ }
+ return PreferenceManager.getDefaultSharedPreferences(storageContext)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockBackupAgent.java b/src/com/android/deskclock/DeskClockBackupAgent.java
deleted file mode 100644
index 4b66659..0000000
--- a/src/com/android/deskclock/DeskClockBackupAgent.java
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
-import android.app.backup.BackupAgent;
-import android.app.backup.BackupDataInput;
-import android.app.backup.BackupDataOutput;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.ParcelFileDescriptor;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-
-import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Calendar;
-import java.util.List;
-
-public class DeskClockBackupAgent extends BackupAgent {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("DeskClockBackupAgent");
-
- public static final String ACTION_COMPLETE_RESTORE =
- "com.android.deskclock.action.COMPLETE_RESTORE";
-
- @Override
- public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
- ParcelFileDescriptor newState) throws IOException { }
-
- @Override
- public void onRestore(BackupDataInput data, int appVersionCode,
- ParcelFileDescriptor newState) throws IOException { }
-
- @Override
- public void onRestoreFile(@NonNull ParcelFileDescriptor data, long size, File destination,
- int type, long mode, long mtime) throws IOException {
- // The preference file on the backup device may not be the same on the restore device.
- // Massage the file name here before writing it.
- if (destination.getName().endsWith("_preferences.xml")) {
- final String prefFileName = getPackageName() + "_preferences.xml";
- destination = new File(destination.getParentFile(), prefFileName);
- }
-
- super.onRestoreFile(data, size, destination, type, mode, mtime);
- }
-
- /**
- * When this method is called during backup/restore, the application is executing in a
- * "minimalist" state. Because of this, the application's ContentResolver cannot be used.
- * Consequently, the work of scheduling alarms on the restore device cannot be done here.
- * Instead, a future callback to DeskClock is used as a signal to reschedule the alarms. The
- * future callback may take the form of ACTION_BOOT_COMPLETED if the device is not yet fully
- * booted (i.e. the restore occurred as part of the setup wizard). If the device is booted, an
- * ACTION_COMPLETE_RESTORE broadcast is scheduled 10 seconds in the future to give
- * backup/restore enough time to kill the Clock process. Both of these future callbacks result
- * in the execution of {@link #processRestoredData(Context)}.
- */
- @Override
- public void onRestoreFinished() {
- if (Utils.isNOrLater()) {
- // TODO: migrate restored database and preferences over into
- // the device-encrypted storage area
- }
-
- // Indicate a data restore has been completed.
- DataModel.getDataModel().setRestoreBackupFinished(true);
-
- // Create an Intent to send into DeskClock indicating restore is complete.
- final PendingIntent restoreIntent = PendingIntent.getBroadcast(this, 0,
- new Intent(ACTION_COMPLETE_RESTORE).setClass(this, AlarmInitReceiver.class),
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT);
-
- // Deliver the Intent 10 seconds from now.
- final long triggerAtMillis = SystemClock.elapsedRealtime() + 10000;
-
- // Schedule the Intent delivery in AlarmManager.
- final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
- alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, restoreIntent);
-
- LOGGER.i("Waiting for %s to complete the data restore", ACTION_COMPLETE_RESTORE);
- }
-
- /**
- * @param context a context to access resources and services
- * @return {@code true} if restore data was processed; {@code false} otherwise.
- */
- public static boolean processRestoredData(Context context) {
- // If data was not recently restored, there is nothing to do.
- if (!DataModel.getDataModel().isRestoreBackupFinished()) {
- return false;
- }
-
- LOGGER.i("processRestoredData() started");
-
- // Now that alarms have been restored, schedule new instances in AlarmManager.
- final ContentResolver contentResolver = context.getContentResolver();
- final List<Alarm> alarms = Alarm.getAlarms(contentResolver, null);
-
- final Calendar now = Calendar.getInstance();
- for (Alarm alarm : alarms) {
- // Remove any instances that may currently exist for the alarm;
- // these aren't relevant on the restore device and we'll recreate them below.
- AlarmStateManager.deleteAllInstances(context, alarm.id);
-
- if (alarm.enabled) {
- // Create the next alarm instance to schedule.
- AlarmInstance alarmInstance = alarm.createInstanceAfter(now);
-
- // Add the next alarm instance to the database.
- alarmInstance = AlarmInstance.addInstance(contentResolver, alarmInstance);
-
- // Schedule the next alarm instance in AlarmManager.
- AlarmStateManager.registerInstance(context, alarmInstance, true);
- LOGGER.i("DeskClockBackupAgent scheduled alarm instance: %s", alarmInstance);
- }
- }
-
- // Remove the preference to avoid executing this logic multiple times.
- DataModel.getDataModel().setRestoreBackupFinished(false);
-
- LOGGER.i("processRestoredData() completed");
- return true;
- }
-}
diff --git a/src/com/android/deskclock/DeskClockBackupAgent.kt b/src/com/android/deskclock/DeskClockBackupAgent.kt
new file mode 100644
index 0000000..71ccf0e
--- /dev/null
+++ b/src/com/android/deskclock/DeskClockBackupAgent.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.AlarmManager
+import android.app.PendingIntent
+import android.app.backup.BackupAgent
+import android.app.backup.BackupDataInput
+import android.app.backup.BackupDataOutput
+import android.content.Context
+import android.content.Intent
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+
+import java.io.File
+import java.io.IOException
+import java.util.Calendar
+
+class DeskClockBackupAgent : BackupAgent() {
+ @Throws(IOException::class)
+ override fun onBackup(
+ oldState: ParcelFileDescriptor,
+ data: BackupDataOutput,
+ newState: ParcelFileDescriptor
+ ) {
+ }
+
+ @Throws(IOException::class)
+ override fun onRestore(
+ data: BackupDataInput,
+ appVersionCode: Int,
+ newState: ParcelFileDescriptor
+ ) {
+ }
+
+ @Throws(IOException::class)
+ override fun onRestoreFile(
+ data: ParcelFileDescriptor,
+ size: Long,
+ destination: File,
+ type: Int,
+ mode: Long,
+ mtime: Long
+ ) {
+ // The preference file on the backup device may not be the same on the restore device.
+ // Massage the file name here before writing it.
+ var variableDestination = destination
+ if (variableDestination.name.endsWith("_preferences.xml")) {
+ val prefFileName = packageName + "_preferences.xml"
+ variableDestination = File(variableDestination.parentFile, prefFileName)
+ }
+
+ super.onRestoreFile(data, size, variableDestination, type, mode, mtime)
+ }
+
+ /**
+ * When this method is called during backup/restore, the application is executing in a
+ * "minimalist" state. Because of this, the application's ContentResolver cannot be used.
+ * Consequently, the work of scheduling alarms on the restore device cannot be done here.
+ * Instead, a future callback to DeskClock is used as a signal to reschedule the alarms. The
+ * future callback may take the form of ACTION_BOOT_COMPLETED if the device is not yet fully
+ * booted (i.e. the restore occurred as part of the setup wizard). If the device is booted, an
+ * ACTION_COMPLETE_RESTORE broadcast is scheduled 10 seconds in the future to give
+ * backup/restore enough time to kill the Clock process. Both of these future callbacks result
+ * in the execution of [.processRestoredData].
+ */
+ override fun onRestoreFinished() {
+ if (Utils.isNOrLater) {
+ // TODO: migrate restored database and preferences over into
+ // the device-encrypted storage area
+ }
+
+ // Indicate a data restore has been completed.
+ DataModel.dataModel.isRestoreBackupFinished = true
+
+ // Create an Intent to send into DeskClock indicating restore is complete.
+ val restoreIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(ACTION_COMPLETE_RESTORE).setClass(this, AlarmInitReceiver::class.java),
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_CANCEL_CURRENT)
+
+ // Deliver the Intent 10 seconds from now.
+ val triggerAtMillis = SystemClock.elapsedRealtime() + 10000
+
+ // Schedule the Intent delivery in AlarmManager.
+ val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, restoreIntent)
+
+ LOGGER.i("Waiting for %s to complete the data restore", ACTION_COMPLETE_RESTORE)
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("DeskClockBackupAgent")
+
+ const val ACTION_COMPLETE_RESTORE = "com.android.deskclock.action.COMPLETE_RESTORE"
+
+ /**
+ * @param context a context to access resources and services
+ * @return `true` if restore data was processed; `false` otherwise.
+ */
+ @JvmStatic
+ fun processRestoredData(context: Context): Boolean {
+ // If data was not recently restored, there is nothing to do.
+ if (!DataModel.dataModel.isRestoreBackupFinished) {
+ return false
+ }
+
+ LOGGER.i("processRestoredData() started")
+
+ // Now that alarms have been restored, schedule new instances in AlarmManager.
+ val contentResolver = context.contentResolver
+ val alarms = Alarm.getAlarms(contentResolver, null)
+
+ val now = Calendar.getInstance()
+ for (alarm in alarms) {
+ // Remove any instances that may currently exist for the alarm;
+ // these aren't relevant on the restore device and we'll recreate them below.
+ AlarmStateManager.deleteAllInstances(context, alarm.id)
+
+ if (alarm.enabled) {
+ // Create the next alarm instance to schedule.
+ var alarmInstance = alarm.createInstanceAfter(now)
+
+ // Add the next alarm instance to the database.
+ alarmInstance = AlarmInstance.addInstance(contentResolver, alarmInstance)
+
+ // Schedule the next alarm instance in AlarmManager.
+ AlarmStateManager.registerInstance(context, alarmInstance, true)
+ LOGGER.i("DeskClockBackupAgent scheduled alarm instance: %s", alarmInstance)
+ }
+ }
+
+ // Remove the preference to avoid executing this logic multiple times.
+ DataModel.dataModel.isRestoreBackupFinished = false
+
+ LOGGER.i("processRestoredData() completed")
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockFragment.java b/src/com/android/deskclock/DeskClockFragment.java
deleted file mode 100644
index a9e3fc6..0000000
--- a/src/com/android/deskclock/DeskClockFragment.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.Fragment;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import android.view.KeyEvent;
-import android.widget.Button;
-import android.widget.ImageView;
-
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.uidata.UiDataModel.Tab;
-
-public abstract class DeskClockFragment extends Fragment implements FabContainer, FabController {
-
- /** The tab associated with this fragment. */
- private final Tab mTab;
-
- /** The container that houses the fab and its left and right buttons. */
- private FabContainer mFabContainer;
-
- public DeskClockFragment(Tab tab) {
- mTab = tab;
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- // Update the fab and buttons in case their state changed while the fragment was paused.
- if (isTabSelected()) {
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
- }
- }
-
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- // By default return false so event continues to propagate
- return false;
- }
-
- @Override
- public void onLeftButtonClick(@NonNull Button left) {
- // Do nothing here, only in derived classes
- }
-
- @Override
- public void onRightButtonClick(@NonNull Button right) {
- // Do nothing here, only in derived classes
- }
-
- @Override
- public void onMorphFab(@NonNull ImageView fab) {
- // Do nothing here, only in derived classes
- }
-
- /**
- * @param color the newly installed app window color
- */
- protected void onAppColorChanged(@ColorInt int color) {
- // Do nothing here, only in derived classes
- }
-
- /**
- * @param fabContainer the container that houses the fab and its left and right buttons
- */
- public final void setFabContainer(FabContainer fabContainer) {
- mFabContainer = fabContainer;
- }
-
- /**
- * Requests that the parent activity update the fab and buttons.
- *
- * @param updateTypes the manner in which the fab container should be updated
- */
- @Override
- public final void updateFab(@UpdateFabFlag int updateTypes) {
- if (mFabContainer != null) {
- mFabContainer.updateFab(updateTypes);
- }
- }
-
- /**
- * @return {@code true} iff the currently selected tab displays this fragment
- */
- public final boolean isTabSelected() {
- return UiDataModel.getUiDataModel().getSelectedTab() == mTab;
- }
-
- /**
- * Select the tab that displays this fragment.
- */
- public final void selectTab() {
- UiDataModel.getUiDataModel().setSelectedTab(mTab);
- }
-
- /**
- * Updates the scrolling state in the {@link UiDataModel} for this tab.
- *
- * @param scrolledToTop {@code true} iff the vertical scroll position of this tab is at the top
- */
- public final void setTabScrolledToTop(boolean scrolledToTop) {
- UiDataModel.getUiDataModel().setTabScrolledToTop(mTab, scrolledToTop);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DeskClockFragment.kt b/src/com/android/deskclock/DeskClockFragment.kt
new file mode 100644
index 0000000..b7f1fa9
--- /dev/null
+++ b/src/com/android/deskclock/DeskClockFragment.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.view.KeyEvent
+import android.widget.Button
+import android.widget.ImageView
+import androidx.annotation.ColorInt
+import androidx.fragment.app.Fragment
+
+import com.android.deskclock.FabContainer.UpdateFabFlag
+import com.android.deskclock.uidata.UiDataModel
+
+abstract class DeskClockFragment(
+ /** The tab associated with this fragment. */
+ private val mTab: UiDataModel.Tab
+) : Fragment(), FabContainer, FabController {
+
+ /** The container that houses the fab and its left and right buttons. */
+ private var mFabContainer: FabContainer? = null
+
+ override fun onResume() {
+ super.onResume()
+
+ // Update the fab and buttons in case their state changed while the fragment was paused.
+ if (isTabSelected) {
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+ }
+ }
+
+ open fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ // By default return false so event continues to propagate
+ return false
+ }
+
+ override fun onLeftButtonClick(left: Button) {
+ // Do nothing here, only in derived classes
+ }
+
+ override fun onRightButtonClick(right: Button) {
+ // Do nothing here, only in derived classes
+ }
+
+ override fun onMorphFab(fab: ImageView) {
+ // Do nothing here, only in derived classes
+ }
+
+ /**
+ * @param color the newly installed app window color
+ */
+ protected open fun onAppColorChanged(@ColorInt color: Int) {
+ // Do nothing here, only in derived classes
+ }
+
+ /**
+ * @param fabContainer the container that houses the fab and its left and right buttons
+ */
+ fun setFabContainer(fabContainer: FabContainer?) {
+ mFabContainer = fabContainer
+ }
+
+ /**
+ * Requests that the parent activity update the fab and buttons.
+ *
+ * @param updateTypes the manner in which the fab container should be updated
+ */
+ override fun updateFab(@UpdateFabFlag updateTypes: Int) {
+ mFabContainer?.updateFab(updateTypes)
+ }
+
+ /**
+ * @return `true` iff the currently selected tab displays this fragment
+ */
+ val isTabSelected: Boolean
+ get() = UiDataModel.uiDataModel.selectedTab == mTab
+
+ /**
+ * Select the tab that displays this fragment.
+ */
+ fun selectTab() {
+ UiDataModel.uiDataModel.selectedTab = mTab
+ }
+
+ /**
+ * Updates the scrolling state in the [UiDataModel] for this tab.
+ *
+ * @param scrolledToTop `true` iff the vertical scroll position of this tab is at the top
+ */
+ fun setTabScrolledToTop(scrolledToTop: Boolean) {
+ UiDataModel.uiDataModel.setTabScrolledToTop(mTab, scrolledToTop)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DropShadowController.java b/src/com/android/deskclock/DropShadowController.java
deleted file mode 100644
index 53edd77..0000000
--- a/src/com/android/deskclock/DropShadowController.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.animation.ValueAnimator;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.View;
-import android.widget.AbsListView;
-import android.widget.ListView;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.uidata.TabScrollListener;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.uidata.UiDataModel.Tab;
-
-import static com.android.deskclock.AnimatorUtils.getAlphaAnimator;
-
-/**
- * This controller encapsulates the logic that watches a model for changes to scroll state and
- * updates the display state of an associated drop shadow. The observable model may take many forms
- * including ListViews, RecyclerViews and this application's UiDataModel. Each of these models can
- * indicate when content is scrolled to its top. When the content is scrolled to the top the drop
- * shadow is hidden and the content appears flush with the app bar. When the content is scrolled
- * up the drop shadow is displayed making the content appear to scroll below the app bar.
- */
-public final class DropShadowController {
-
- /** Updates {@link #mDropShadowView} in response to changes in the backing scroll model. */
- private final ScrollChangeWatcher mScrollChangeWatcher = new ScrollChangeWatcher();
-
- /** Fades the {@link @mDropShadowView} in/out as scroll state changes. */
- private final ValueAnimator mDropShadowAnimator;
-
- /** The component that displays a drop shadow. */
- private final View mDropShadowView;
-
- /** Tab bar's hairline, which is hidden whenever the drop shadow is displayed. */
- private View mHairlineView;
-
- // Supported sources of scroll position include: ListView, RecyclerView and UiDataModel.
- private RecyclerView mRecyclerView;
- private UiDataModel mUiDataModel;
- private ListView mListView;
-
- /**
- * @param dropShadowView to be hidden/shown as {@code uiDataModel} reports scrolling changes
- * @param uiDataModel models the vertical scrolling state of the application's selected tab
- * @param hairlineView at the bottom of the tab bar to be hidden or shown when the drop shadow
- * is displayed or hidden, respectively.
- */
- public DropShadowController(View dropShadowView, UiDataModel uiDataModel, View hairlineView) {
- this(dropShadowView);
- mUiDataModel = uiDataModel;
- mUiDataModel.addTabScrollListener(mScrollChangeWatcher);
- mHairlineView = hairlineView;
- updateDropShadow(!uiDataModel.isSelectedTabScrolledToTop());
- }
-
- /**
- * @param dropShadowView to be hidden/shown as {@code listView} reports scrolling changes
- * @param listView a scrollable view that dictates the visibility of {@code dropShadowView}
- */
- public DropShadowController(View dropShadowView, ListView listView) {
- this(dropShadowView);
- mListView = listView;
- mListView.setOnScrollListener(mScrollChangeWatcher);
- updateDropShadow(!Utils.isScrolledToTop(listView));
- }
-
- /**
- * @param dropShadowView to be hidden/shown as {@code recyclerView} reports scrolling changes
- * @param recyclerView a scrollable view that dictates the visibility of {@code dropShadowView}
- */
- public DropShadowController(View dropShadowView, RecyclerView recyclerView) {
- this(dropShadowView);
- mRecyclerView = recyclerView;
- mRecyclerView.addOnScrollListener(mScrollChangeWatcher);
- updateDropShadow(!Utils.isScrolledToTop(recyclerView));
- }
-
- private DropShadowController(View dropShadowView) {
- mDropShadowView = dropShadowView;
- mDropShadowAnimator = getAlphaAnimator(mDropShadowView, 0f, 1f)
- .setDuration(UiDataModel.getUiDataModel().getShortAnimationDuration());
- }
-
- /**
- * Stop updating the drop shadow in response to scrolling changes. Stop listening to the backing
- * scrollable entity for changes. This is important to avoid memory leaks.
- */
- public void stop() {
- if (mRecyclerView != null) {
- mRecyclerView.removeOnScrollListener(mScrollChangeWatcher);
- } else if (mListView != null) {
- mListView.setOnScrollListener(null);
- } else if (mUiDataModel != null) {
- mUiDataModel.removeTabScrollListener(mScrollChangeWatcher);
- }
- }
-
- /**
- * @param shouldShowDropShadow {@code true} indicates the drop shadow should be displayed;
- * {@code false} indicates the drop shadow should be hidden
- */
- private void updateDropShadow(boolean shouldShowDropShadow) {
- if (!shouldShowDropShadow && mDropShadowView.getAlpha() != 0f) {
- if (DataModel.getDataModel().isApplicationInForeground()) {
- mDropShadowAnimator.reverse();
- } else {
- mDropShadowView.setAlpha(0f);
- }
- if (mHairlineView != null) {
- mHairlineView.setVisibility(View.VISIBLE);
- }
- }
-
- if (shouldShowDropShadow && mDropShadowView.getAlpha() != 1f) {
- if (DataModel.getDataModel().isApplicationInForeground()) {
- mDropShadowAnimator.start();
- } else {
- mDropShadowView.setAlpha(1f);
- }
- if (mHairlineView != null) {
- mHairlineView.setVisibility(View.INVISIBLE);
- }
- }
- }
-
- /**
- * Update the drop shadow as the scrollable entity is scrolled.
- */
- private final class ScrollChangeWatcher extends RecyclerView.OnScrollListener
- implements TabScrollListener, AbsListView.OnScrollListener {
-
- // RecyclerView scrolled.
- @Override
- public void onScrolled(RecyclerView view, int dx, int dy) {
- updateDropShadow(!Utils.isScrolledToTop(view));
- }
-
- // ListView scrolled.
- @Override
- public void onScrollStateChanged(AbsListView view, int scrollState) {}
-
- @Override
- public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
- int totalItemCount) {
- updateDropShadow(!Utils.isScrolledToTop(view));
- }
-
- // UiDataModel reports scroll change.
- public void selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop) {
- updateDropShadow(!scrolledToTop);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/DropShadowController.kt b/src/com/android/deskclock/DropShadowController.kt
new file mode 100644
index 0000000..4fa9101
--- /dev/null
+++ b/src/com/android/deskclock/DropShadowController.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.animation.ValueAnimator
+import android.view.View
+import android.widget.AbsListView
+import android.widget.ListView
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.uidata.TabScrollListener
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * This controller encapsulates the logic that watches a model for changes to scroll state and
+ * updates the display state of an associated drop shadow. The observable model may take many forms
+ * including ListViews, RecyclerViews and this application's UiDataModel. Each of these models can
+ * indicate when content is scrolled to its top. When the content is scrolled to the top the drop
+ * shadow is hidden and the content appears flush with the app bar. When the content is scrolled
+ * up the drop shadow is displayed making the content appear to scroll below the app bar.
+ */
+class DropShadowController private constructor(
+ /** The component that displays a drop shadow. */
+ private val mDropShadowView: View
+) {
+ /** Updates [.mDropShadowView] in response to changes in the backing scroll model. */
+ private val mScrollChangeWatcher = ScrollChangeWatcher()
+
+ /** Fades the [@mDropShadowView] in/out as scroll state changes. */
+ private val mDropShadowAnimator: ValueAnimator =
+ AnimatorUtils.getAlphaAnimator(mDropShadowView, 0f, 1f)
+ .setDuration(UiDataModel.uiDataModel.shortAnimationDuration)
+
+ /** Tab bar's hairline, which is hidden whenever the drop shadow is displayed. */
+ private var mHairlineView: View? = null
+
+ // Supported sources of scroll position include: ListView, RecyclerView and UiDataModel.
+ private var mRecyclerView: RecyclerView? = null
+ private var mUiDataModel: UiDataModel? = null
+ private var mListView: ListView? = null
+
+ /**
+ * @param dropShadowView to be hidden/shown as `uiDataModel` reports scrolling changes
+ * @param uiDataModel models the vertical scrolling state of the application's selected tab
+ * @param hairlineView at the bottom of the tab bar to be hidden or shown when the drop shadow
+ * is displayed or hidden, respectively.
+ */
+ constructor(
+ dropShadowView: View,
+ uiDataModel: UiDataModel,
+ hairlineView: View
+ ) : this(dropShadowView) {
+ mUiDataModel = uiDataModel
+ mUiDataModel?.addTabScrollListener(mScrollChangeWatcher)
+ mHairlineView = hairlineView
+ updateDropShadow(!uiDataModel.isSelectedTabScrolledToTop)
+ }
+
+ /**
+ * @param dropShadowView to be hidden/shown as `listView` reports scrolling changes
+ * @param listView a scrollable view that dictates the visibility of `dropShadowView`
+ */
+ constructor(dropShadowView: View, listView: ListView) : this(dropShadowView) {
+ mListView = listView
+ mListView?.setOnScrollListener(mScrollChangeWatcher)
+ updateDropShadow(!Utils.isScrolledToTop(listView))
+ }
+
+ /**
+ * @param dropShadowView to be hidden/shown as `recyclerView` reports scrolling changes
+ * @param recyclerView a scrollable view that dictates the visibility of `dropShadowView`
+ */
+ constructor(dropShadowView: View, recyclerView: RecyclerView) : this(dropShadowView) {
+ mRecyclerView = recyclerView
+ mRecyclerView?.addOnScrollListener(mScrollChangeWatcher)
+ updateDropShadow(!Utils.isScrolledToTop(recyclerView))
+ }
+
+ /**
+ * Stop updating the drop shadow in response to scrolling changes. Stop listening to the backing
+ * scrollable entity for changes. This is important to avoid memory leaks.
+ */
+ fun stop() {
+ when {
+ mRecyclerView != null -> mRecyclerView?.removeOnScrollListener(mScrollChangeWatcher)
+ mListView != null -> mListView?.setOnScrollListener(null)
+ mUiDataModel != null -> mUiDataModel?.removeTabScrollListener(mScrollChangeWatcher)
+ }
+ }
+
+ /**
+ * @param shouldShowDropShadow `true` indicates the drop shadow should be displayed;
+ * `false` indicates the drop shadow should be hidden
+ */
+ private fun updateDropShadow(shouldShowDropShadow: Boolean) {
+ if (!shouldShowDropShadow && mDropShadowView.alpha != 0f) {
+ if (DataModel.dataModel.isApplicationInForeground) {
+ mDropShadowAnimator.reverse()
+ } else {
+ mDropShadowView.alpha = 0f
+ }
+ mHairlineView?.visibility = View.VISIBLE
+ }
+ if (shouldShowDropShadow && mDropShadowView.alpha != 1f) {
+ if (DataModel.dataModel.isApplicationInForeground) {
+ mDropShadowAnimator.start()
+ } else {
+ mDropShadowView.alpha = 1f
+ }
+ mHairlineView?.visibility = View.INVISIBLE
+ }
+ }
+
+ /**
+ * Update the drop shadow as the scrollable entity is scrolled.
+ */
+ private inner class ScrollChangeWatcher
+ : RecyclerView.OnScrollListener(), TabScrollListener, AbsListView.OnScrollListener {
+ // RecyclerView scrolled.
+ override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
+ updateDropShadow(!Utils.isScrolledToTop(view))
+ }
+
+ // ListView scrolled.
+ override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
+ }
+
+ override fun onScroll(
+ view: AbsListView,
+ firstVisibleItem: Int,
+ visibleItemCount: Int,
+ totalItemCount: Int
+ ) {
+ updateDropShadow(!Utils.isScrolledToTop(view))
+ }
+
+ // UiDataModel reports scroll change.
+ override fun selectedTabScrollToTopChanged(
+ selectedTab: UiDataModel.Tab,
+ scrolledToTop: Boolean
+ ) {
+ updateDropShadow(!scrolledToTop)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabContainer.java b/src/com/android/deskclock/FabContainer.java
deleted file mode 100644
index 2813e70..0000000
--- a/src/com/android/deskclock/FabContainer.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.android.deskclock;
-
-import androidx.annotation.IntDef;
-
-/**
- * Implemented by containers that house the fab and its associated buttons. Also implemented by
- * containers that know how to contact the <strong>true</strong> fab container to ferry through
- * commands.
- */
-public interface FabContainer {
-
- /** Bit field for updates */
-
- /** Bit 0-1 */
- int FAB_ANIMATION_MASK = 0b11;
- /** Signals that the fab should be updated in place with no animation. */
- int FAB_IMMEDIATE = 0b1;
- /** Signals the fab should be "animated away", updated, and "animated back". */
- int FAB_SHRINK_AND_EXPAND = 0b10;
- /** Signals that the fab should morph into a new state in place. */
- int FAB_MORPH = 0b11;
-
- /** Bit 2 */
- int FAB_REQUEST_FOCUS_MASK = 0b100;
- /** Signals that the fab should request focus. */
- int FAB_REQUEST_FOCUS = 0b100;
-
- /** Bit 3-4 */
- int BUTTONS_ANIMATION_MASK = 0b11000;
- /** Signals that the buttons should be updated in place with no animation. */
- int BUTTONS_IMMEDIATE = 0b1000;
- /** Signals that the buttons should be "animated away", updated, and "animated back". */
- int BUTTONS_SHRINK_AND_EXPAND = 0b10000;
-
- /** Bit 5 */
- int BUTTONS_DISABLE_MASK = 0b100000;
- /** Disable the buttons of the fab so they do not respond to clicks. */
- int BUTTONS_DISABLE = 0b100000;
-
- /** Bit 6-7 */
- int FAB_AND_BUTTONS_SHRINK_EXPAND_MASK = 0b11000000;
- /** Signals the fab and buttons should be "animated away". */
- int FAB_AND_BUTTONS_SHRINK = 0b10000000;
- /** Signals the fab and buttons should be "animated back". */
- int FAB_AND_BUTTONS_EXPAND = 0b01000000;
-
- /** Convenience flags */
- int FAB_AND_BUTTONS_IMMEDIATE = FAB_IMMEDIATE | BUTTONS_IMMEDIATE;
- int FAB_AND_BUTTONS_SHRINK_AND_EXPAND = FAB_SHRINK_AND_EXPAND | BUTTONS_SHRINK_AND_EXPAND;
-
- @IntDef(
- flag = true,
- value = { FAB_IMMEDIATE, FAB_SHRINK_AND_EXPAND, FAB_MORPH, FAB_REQUEST_FOCUS,
- BUTTONS_IMMEDIATE, BUTTONS_SHRINK_AND_EXPAND, BUTTONS_DISABLE,
- FAB_AND_BUTTONS_IMMEDIATE, FAB_AND_BUTTONS_SHRINK_AND_EXPAND,
- FAB_AND_BUTTONS_SHRINK, FAB_AND_BUTTONS_EXPAND }
- )
- @interface UpdateFabFlag {}
-
- /**
- * Requests that this container update the fab and/or its buttons because their state has
- * changed. The update may be immediate or it may be animated depending on the choice of
- * {@code updateTypes}.
- *
- * @param updateTypes indicates the types of update to apply to the fab and its buttons
- */
- void updateFab(@UpdateFabFlag int updateTypes);
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabContainer.kt b/src/com/android/deskclock/FabContainer.kt
new file mode 100644
index 0000000..58738f3
--- /dev/null
+++ b/src/com/android/deskclock/FabContainer.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import androidx.annotation.IntDef
+
+/**
+ * Implemented by containers that house the fab and its associated buttons. Also implemented by
+ * containers that know how to contact the **true** fab container to ferry through
+ * commands.
+ */
+interface FabContainer {
+ @IntDef(flag = true, value = [
+ FAB_IMMEDIATE,
+ FAB_SHRINK_AND_EXPAND,
+ FAB_MORPH,
+ FAB_REQUEST_FOCUS,
+ BUTTONS_IMMEDIATE,
+ BUTTONS_SHRINK_AND_EXPAND,
+ BUTTONS_DISABLE,
+ FAB_AND_BUTTONS_IMMEDIATE,
+ FAB_AND_BUTTONS_SHRINK_AND_EXPAND,
+ FAB_AND_BUTTONS_SHRINK,
+ FAB_AND_BUTTONS_EXPAND
+ ])
+ annotation class UpdateFabFlag
+
+ /**
+ * Requests that this container update the fab and/or its buttons because their state has
+ * changed. The update may be immediate or it may be animated depending on the choice of
+ * `updateTypes`.
+ *
+ * @param updateTypes indicates the types of update to apply to the fab and its buttons
+ */
+ fun updateFab(@UpdateFabFlag updateTypes: Int)
+
+ companion object {
+ /** Bit field for updates */
+ /** Bit 0-1 */
+ const val FAB_ANIMATION_MASK = 3
+
+ /** Signals that the fab should be updated in place with no animation. */
+ const val FAB_IMMEDIATE = 1
+
+ /** Signals the fab should be "animated away", updated, and "animated back". */
+ const val FAB_SHRINK_AND_EXPAND = 2
+
+ /** Signals that the fab should morph into a new state in place. */
+ const val FAB_MORPH = 3
+
+ /** Bit 2 */
+ const val FAB_REQUEST_FOCUS_MASK = 4
+
+ /** Signals that the fab should request focus. */
+ const val FAB_REQUEST_FOCUS = 4
+
+ /** Bit 3-4 */
+ const val BUTTONS_ANIMATION_MASK = 24
+
+ /** Signals that the buttons should be updated in place with no animation. */
+ const val BUTTONS_IMMEDIATE = 8
+
+ /** Signals that the buttons should be "animated away", updated, and "animated back". */
+ const val BUTTONS_SHRINK_AND_EXPAND = 16
+
+ /** Bit 5 */
+ const val BUTTONS_DISABLE_MASK = 32
+
+ /** Disable the buttons of the fab so they do not respond to clicks. */
+ const val BUTTONS_DISABLE = 32
+
+ /** Bit 6-7 */
+ const val FAB_AND_BUTTONS_SHRINK_EXPAND_MASK = 192
+
+ /** Signals the fab and buttons should be "animated away". */
+ const val FAB_AND_BUTTONS_SHRINK = 128
+
+ /** Signals the fab and buttons should be "animated back". */
+ const val FAB_AND_BUTTONS_EXPAND = 64
+
+ /** Convenience flags */
+ const val FAB_AND_BUTTONS_IMMEDIATE = FAB_IMMEDIATE or BUTTONS_IMMEDIATE
+ const val FAB_AND_BUTTONS_SHRINK_AND_EXPAND =
+ FAB_SHRINK_AND_EXPAND or BUTTONS_SHRINK_AND_EXPAND
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabController.java b/src/com/android/deskclock/FabController.java
deleted file mode 100644
index bab7f46..0000000
--- a/src/com/android/deskclock/FabController.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.android.deskclock;
-
-import androidx.annotation.NonNull;
-import android.view.View;
-import android.widget.Button;
-import android.widget.ImageView;
-
-/**
- * Implementers of this interface are able to {@link #onUpdateFab configure the fab} and associated
- * {@link #onUpdateFabButtons left/right buttons} including setting them {@link View#INVISIBLE} if
- * they are unnecessary. Implementers also attach click handler logic to the
- * {@link #onFabClick fab}, {@link #onLeftButtonClick left button} and
- * {@link #onRightButtonClick right button}.
- */
-public interface FabController {
-
- /**
- * Configures the display of the fab component to match the current state of this controller.
- *
- * @param fab the fab component to be configured based on current state
- */
- void onUpdateFab(@NonNull ImageView fab);
-
- /**
- * Called before onUpdateFab when the fab should be animated.
- *
- * @param fab the fab component to be configured based on current state
- */
- void onMorphFab(@NonNull ImageView fab);
-
- /**
- * Configures the display of the buttons to the left and right of the fab to match the current
- * state of this controller.
- *
- * @param left button to the left of the fab to configure based on current state
- * @param right button to the right of the fab to configure based on current state
- */
- void onUpdateFabButtons(@NonNull Button left, @NonNull Button right);
-
- /**
- * Handles a click on the fab.
- *
- * @param fab the fab component on which the click occurred
- */
- void onFabClick(@NonNull ImageView fab);
-
- /**
- * Handles a click on the button to the left of the fab component.
- *
- * @param left the button to the left of the fab component
- */
- void onLeftButtonClick(@NonNull Button left);
-
- /**
- * Handles a click on the button to the right of the fab component.
- *
- * @param right the button to the right of the fab component
- */
- void onRightButtonClick(@NonNull Button right);
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FabController.kt b/src/com/android/deskclock/FabController.kt
new file mode 100644
index 0000000..cc1096a
--- /dev/null
+++ b/src/com/android/deskclock/FabController.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+
+/**
+ * Implementers of this interface are able to [configure the fab][.onUpdateFab] and associated
+ * [left/right buttons][.onUpdateFabButtons] including setting them [View.INVISIBLE] if
+ * they are unnecessary. Implementers also attach click handler logic to the
+ * [fab][.onFabClick], [left button][.onLeftButtonClick] and
+ * [right button][.onRightButtonClick].
+ */
+interface FabController {
+ /**
+ * Configures the display of the fab component to match the current state of this controller.
+ *
+ * @param fab the fab component to be configured based on current state
+ */
+ fun onUpdateFab(fab: ImageView)
+
+ /**
+ * Called before onUpdateFab when the fab should be animated.
+ *
+ * @param fab the fab component to be configured based on current state
+ */
+ fun onMorphFab(fab: ImageView)
+
+ /**
+ * Configures the display of the buttons to the left and right of the fab to match the current
+ * state of this controller.
+ *
+ * @param left button to the left of the fab to configure based on current state
+ * @param right button to the right of the fab to configure based on current state
+ */
+ fun onUpdateFabButtons(left: Button, right: Button)
+
+ /**
+ * Handles a click on the fab.
+ *
+ * @param fab the fab component on which the click occurred
+ */
+ fun onFabClick(fab: ImageView)
+
+ /**
+ * Handles a click on the button to the left of the fab component.
+ *
+ * @param left the button to the left of the fab component
+ */
+ fun onLeftButtonClick(left: Button)
+
+ /**
+ * Handles a click on the button to the right of the fab component.
+ *
+ * @param right the button to the right of the fab component
+ */
+ fun onRightButtonClick(right: Button)
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FetchMatchingAlarmsAction.java b/src/com/android/deskclock/FetchMatchingAlarmsAction.java
deleted file mode 100644
index 5575348..0000000
--- a/src/com/android/deskclock/FetchMatchingAlarmsAction.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Looper;
-import android.provider.AlarmClock;
-
-import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.controller.Controller;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-import java.text.DateFormatSymbols;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.List;
-
-/**
- * Returns a list of alarms that are specified by the intent
- * processed by HandleDeskClockApiCalls
- * if there are more than 1 matching alarms and the SEARCH_MODE is not ALL
- * we show a picker UI dialog
- */
-class FetchMatchingAlarmsAction implements Runnable {
-
- private final Context mContext;
- private final List<Alarm> mAlarms;
- private final Intent mIntent;
- private final List<Alarm> mMatchingAlarms = new ArrayList<>();
- private final Activity mActivity;
-
- public FetchMatchingAlarmsAction(Context context, List<Alarm> alarms, Intent intent,
- Activity activity) {
- mContext = context;
- // only enabled alarms are passed
- mAlarms = alarms;
- mIntent = intent;
- mActivity = activity;
- }
-
- @Override
- public void run() {
- Utils.enforceNotMainLooper();
-
- final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE);
- // if search mode isn't specified show all alarms in the UI picker
- if (searchMode == null) {
- mMatchingAlarms.addAll(mAlarms);
- return;
- }
-
- final ContentResolver cr = mContext.getContentResolver();
- switch (searchMode) {
- case AlarmClock.ALARM_SEARCH_MODE_TIME:
- // at least one of these has to be specified in this search mode.
- final int hour = mIntent.getIntExtra(AlarmClock.EXTRA_HOUR, -1);
- // if minutes weren't specified default to 0
- final int minutes = mIntent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
- final Boolean isPm = (Boolean) mIntent.getExtras().get(AlarmClock.EXTRA_IS_PM);
- boolean badInput = isPm != null && hour > 12 && isPm;
- badInput |= hour < 0 || hour > 23;
- badInput |= minutes < 0 || minutes > 59;
-
- if (badInput) {
- final String[] ampm = new DateFormatSymbols().getAmPmStrings();
- final String amPm = isPm == null ? "" : (isPm ? ampm[1] : ampm[0]);
- final String reason = mContext.getString(R.string.invalid_time, hour, minutes,
- amPm);
- notifyFailureAndLog(reason, mActivity);
- return;
- }
-
- final int hour24 = Boolean.TRUE.equals(isPm) && hour < 12 ? (hour + 12) : hour;
-
- // there might me multiple alarms at the same time
- for (Alarm alarm : mAlarms) {
- if (alarm.hour == hour24 && alarm.minutes == minutes) {
- mMatchingAlarms.add(alarm);
- }
- }
- if (mMatchingAlarms.isEmpty()) {
- final String reason = mContext.getString(R.string.no_alarm_at, hour24, minutes);
- notifyFailureAndLog(reason, mActivity);
- return;
- }
- break;
- case AlarmClock.ALARM_SEARCH_MODE_NEXT:
- // Match currently firing alarms before scheduled alarms.
- for (Alarm alarm : mAlarms) {
- final AlarmInstance alarmInstance =
- AlarmInstance.getNextUpcomingInstanceByAlarmId(cr, alarm.id);
- if (alarmInstance != null
- && alarmInstance.mAlarmState == AlarmInstance.FIRED_STATE) {
- mMatchingAlarms.add(alarm);
- }
- }
- if (!mMatchingAlarms.isEmpty()) {
- // return the matched firing alarms
- return;
- }
-
- final AlarmInstance nextAlarm = AlarmStateManager.getNextFiringAlarm(mContext);
- if (nextAlarm == null) {
- final String reason = mContext.getString(R.string.no_scheduled_alarms);
- notifyFailureAndLog(reason, mActivity);
- return;
- }
-
- // get time from nextAlarm and see if there are any other alarms matching this time
- final Calendar nextTime = nextAlarm.getAlarmTime();
- final List<Alarm> alarmsFiringAtSameTime = getAlarmsByHourMinutes(
- nextTime.get(Calendar.HOUR_OF_DAY), nextTime.get(Calendar.MINUTE), cr);
- // there might me multiple alarms firing next
- mMatchingAlarms.addAll(alarmsFiringAtSameTime);
- break;
- case AlarmClock.ALARM_SEARCH_MODE_ALL:
- mMatchingAlarms.addAll(mAlarms);
- break;
- case AlarmClock.ALARM_SEARCH_MODE_LABEL:
- // EXTRA_MESSAGE has to be set in this mode
- final String label = mIntent.getStringExtra(AlarmClock.EXTRA_MESSAGE);
- if (label == null) {
- final String reason = mContext.getString(R.string.no_label_specified);
- notifyFailureAndLog(reason, mActivity);
- return;
- }
-
- // there might me multiple alarms with this label
- for (Alarm alarm : mAlarms) {
- if (alarm.label.contains(label)) {
- mMatchingAlarms.add(alarm);
- }
- }
-
- if (mMatchingAlarms.isEmpty()) {
- final String reason = mContext.getString(R.string.no_alarms_with_label);
- notifyFailureAndLog(reason, mActivity);
- return;
- }
- break;
- }
- }
-
- private List<Alarm> getAlarmsByHourMinutes(int hour24, int minutes, ContentResolver cr) {
- // if we want to dismiss we should only add enabled alarms
- final String selection = String.format("%s=? AND %s=? AND %s=?",
- Alarm.HOUR, Alarm.MINUTES, Alarm.ENABLED);
- final String[] args = { String.valueOf(hour24), String.valueOf(minutes), "1" };
- return Alarm.getAlarms(cr, selection, args);
- }
-
- public List<Alarm> getMatchingAlarms() {
- return mMatchingAlarms;
- }
-
- private void notifyFailureAndLog(String reason, Activity activity) {
- LogUtils.e(reason);
- Controller.getController().notifyVoiceFailure(activity, reason);
- }
-}
diff --git a/src/com/android/deskclock/FetchMatchingAlarmsAction.kt b/src/com/android/deskclock/FetchMatchingAlarmsAction.kt
new file mode 100644
index 0000000..a706dda
--- /dev/null
+++ b/src/com/android/deskclock/FetchMatchingAlarmsAction.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.Activity
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.provider.AlarmClock
+
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.controller.Controller
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.text.DateFormatSymbols
+import java.util.Calendar
+
+/**
+ * Returns a list of alarms that are specified by the intent
+ * processed by HandleDeskClockApiCalls
+ * if there are more than 1 matching alarms and the SEARCH_MODE is not ALL
+ * we show a picker UI dialog
+ */
+internal class FetchMatchingAlarmsAction(
+ private val mContext: Context,
+ private val mAlarms: List<Alarm>,
+ private val mIntent: Intent,
+ private val mActivity: Activity
+) : Runnable {
+ private val mMatchingAlarms: MutableList<Alarm> = ArrayList()
+
+ override fun run() {
+ Utils.enforceNotMainLooper()
+
+ val searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE)
+ // if search mode isn't specified show all alarms in the UI picker
+ if (searchMode == null) {
+ mMatchingAlarms.addAll(mAlarms)
+ return
+ }
+
+ val cr = mContext.contentResolver
+ when (searchMode) {
+ AlarmClock.ALARM_SEARCH_MODE_TIME -> {
+ // at least one of these has to be specified in this search mode.
+ val hour = mIntent.getIntExtra(AlarmClock.EXTRA_HOUR, -1)
+ // if minutes weren't specified default to 0
+ val minutes = mIntent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0)
+ val isPm = mIntent.extras!![AlarmClock.EXTRA_IS_PM] as Boolean?
+ var badInput = isPm != null && hour > 12 && isPm
+ badInput = badInput or (hour < 0 || hour > 23)
+ badInput = badInput or (minutes < 0 || minutes > 59)
+
+ if (badInput) {
+ val ampm = DateFormatSymbols().amPmStrings
+ val amPm = if (isPm == null) "" else (if (isPm) ampm[1] else ampm[0])
+ val reason = mContext.getString(R.string.invalid_time, hour, minutes, amPm)
+ notifyFailureAndLog(reason, mActivity)
+ return
+ }
+
+ val hour24 = if (java.lang.Boolean.TRUE == isPm && hour < 12) hour + 12 else hour
+
+ // there might me multiple alarms at the same time
+ for (alarm in mAlarms) {
+ if (alarm.hour == hour24 && alarm.minutes == minutes) {
+ mMatchingAlarms.add(alarm)
+ }
+ }
+ if (mMatchingAlarms.isEmpty()) {
+ val reason = mContext.getString(R.string.no_alarm_at, hour24, minutes)
+ notifyFailureAndLog(reason, mActivity)
+ return
+ }
+ }
+ AlarmClock.ALARM_SEARCH_MODE_NEXT -> {
+ // Match currently firing alarms before scheduled alarms.
+ for (alarm in mAlarms) {
+ val alarmInstance = AlarmInstance.getNextUpcomingInstanceByAlarmId(cr, alarm.id)
+ if (alarmInstance != null &&
+ alarmInstance.mAlarmState == InstancesColumns.FIRED_STATE) {
+ mMatchingAlarms.add(alarm)
+ }
+ }
+ if (mMatchingAlarms.isNotEmpty()) {
+ // return the matched firing alarms
+ return
+ }
+ val nextAlarm = AlarmStateManager.getNextFiringAlarm(mContext)
+ if (nextAlarm == null) {
+ val reason = mContext.getString(R.string.no_scheduled_alarms)
+ notifyFailureAndLog(reason, mActivity)
+ return
+ }
+
+ // get time from nextAlarm and see if there are any other alarms matching this time
+ val nextTime: Calendar = nextAlarm.alarmTime
+ val alarmsFiringAtSameTime = getAlarmsByHourMinutes(
+ nextTime[Calendar.HOUR_OF_DAY], nextTime[Calendar.MINUTE], cr)
+ // there might me multiple alarms firing next
+ mMatchingAlarms.addAll(alarmsFiringAtSameTime)
+ }
+ AlarmClock.ALARM_SEARCH_MODE_ALL -> mMatchingAlarms.addAll(mAlarms)
+ AlarmClock.ALARM_SEARCH_MODE_LABEL -> {
+ // EXTRA_MESSAGE has to be set in this mode
+ val label = mIntent.getStringExtra(AlarmClock.EXTRA_MESSAGE)
+ if (label == null) {
+ val reason = mContext.getString(R.string.no_label_specified)
+ notifyFailureAndLog(reason, mActivity)
+ return
+ }
+
+ // there might me multiple alarms with this label
+ for (alarm in mAlarms) {
+ if (alarm.label!!.contains(label)) {
+ mMatchingAlarms.add(alarm)
+ }
+ }
+
+ if (mMatchingAlarms.isEmpty()) {
+ val reason = mContext.getString(R.string.no_alarms_with_label)
+ notifyFailureAndLog(reason, mActivity)
+ return
+ }
+ }
+ }
+ }
+
+ private fun getAlarmsByHourMinutes(
+ hour24: Int,
+ minutes: Int,
+ cr: ContentResolver
+ ): List<Alarm> {
+ // if we want to dismiss we should only add enabled alarms
+ val selection = String.format("%s=? AND %s=? AND %s=?",
+ AlarmsColumns.HOUR, AlarmsColumns.MINUTES, AlarmsColumns.ENABLED)
+ val args = arrayOf(hour24.toString(), minutes.toString(), "1")
+ return Alarm.getAlarms(cr, selection, *args)
+ }
+
+ val matchingAlarms: List<Alarm>
+ get() = mMatchingAlarms
+
+ private fun notifyFailureAndLog(reason: String, activity: Activity) {
+ LogUtils.e(reason)
+ Controller.getController().notifyVoiceFailure(activity, reason)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FormattedTextUtils.java b/src/com/android/deskclock/FormattedTextUtils.java
deleted file mode 100644
index 80833ee..0000000
--- a/src/com/android/deskclock/FormattedTextUtils.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.text.Spannable;
-import android.text.SpannableString;
-
-/**
- * Utilities for formatting strings using spans.
- */
-public class FormattedTextUtils {
-
- private FormattedTextUtils() {
- }
-
- /**
- * Applies a span over the length of the given text.
- *
- * @param text the {@link CharSequence} to be formatted
- * @param span the span to apply
- * @return the text with the span applied
- */
- public static CharSequence formatText(CharSequence text, Object span) {
- if (text == null) {
- return null;
- }
-
- final SpannableString formattedText = SpannableString.valueOf(text);
- formattedText.setSpan(span, 0, formattedText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- return formattedText;
- }
-}
diff --git a/src/com/android/deskclock/FormattedTextUtils.kt b/src/com/android/deskclock/FormattedTextUtils.kt
new file mode 100644
index 0000000..bd11690
--- /dev/null
+++ b/src/com/android/deskclock/FormattedTextUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.text.Spannable
+import android.text.SpannableString
+
+/**
+ * Utilities for formatting strings using spans.
+ */
+object FormattedTextUtils {
+ /**
+ * Applies a span over the length of the given text.
+ *
+ * @param text the [CharSequence] to be formatted
+ * @param span the span to apply
+ * @return the text with the span applied
+ */
+ @JvmStatic
+ fun formatText(text: CharSequence?, span: Any?): CharSequence? {
+ if (text == null) {
+ return null
+ }
+
+ val formattedText = SpannableString.valueOf(text)
+ formattedText.setSpan(span, 0, formattedText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ return formattedText
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FragmentTabPagerAdapter.java b/src/com/android/deskclock/FragmentTabPagerAdapter.java
deleted file mode 100644
index 3682c86..0000000
--- a/src/com/android/deskclock/FragmentTabPagerAdapter.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import androidx.legacy.app.FragmentCompat;
-import androidx.viewpager.widget.PagerAdapter;
-import android.util.ArrayMap;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.Map;
-
-/**
- * This adapter produces the DeskClockFragments that are the content of the DeskClock tabs. The
- * adapter presents the tabs in LTR and RTL order depending on the text layout direction for the
- * current locale. To prevent issues when switching between LTR and RTL, fragments are registered
- * with the manager using position-independent tags, which is an important departure from
- * FragmentPagerAdapter.
- */
-final class FragmentTabPagerAdapter extends PagerAdapter {
-
- private final DeskClock mDeskClock;
-
- /** The manager into which fragments are added. */
- private final FragmentManager mFragmentManager;
-
- /** A fragment cache that can be accessed before {@link #instantiateItem} is called. */
- private final Map<UiDataModel.Tab, DeskClockFragment> mFragmentCache;
-
- /** The active fragment transaction if one exists. */
- private FragmentTransaction mCurrentTransaction;
-
- /** The current fragment displayed to the user. */
- private Fragment mCurrentPrimaryItem;
-
- FragmentTabPagerAdapter(DeskClock deskClock) {
- mDeskClock = deskClock;
- mFragmentCache = new ArrayMap<>(getCount());
- mFragmentManager = deskClock.getFragmentManager();
- }
-
- @Override
- public int getCount() {
- return UiDataModel.getUiDataModel().getTabCount();
- }
-
- /**
- * @param position the left-to-right index of the fragment to be returned
- * @return the fragment displayed at the given {@code position}
- */
- DeskClockFragment getDeskClockFragment(int position) {
- // Fetch the tab the UiDataModel reports for the position.
- final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
-
- // First check the local cache for the fragment.
- DeskClockFragment fragment = mFragmentCache.get(tab);
- if (fragment != null) {
- return fragment;
- }
-
- // Next check the fragment manager; relevant when app is rebuilt after locale changes
- // because this adapter will be new and mFragmentCache will be empty, but the fragment
- // manager will retain the Fragments built on original application launch.
- fragment = (DeskClockFragment) mFragmentManager.findFragmentByTag(tab.name());
- if (fragment != null) {
- fragment.setFabContainer(mDeskClock);
- mFragmentCache.put(tab, fragment);
- return fragment;
- }
-
- // Otherwise, build the fragment from scratch.
- final String fragmentClassName = tab.getFragmentClassName();
- fragment = (DeskClockFragment) Fragment.instantiate(mDeskClock, fragmentClassName);
- fragment.setFabContainer(mDeskClock);
- mFragmentCache.put(tab, fragment);
- return fragment;
- }
-
- @Override
- public void startUpdate(ViewGroup container) {
- if (container.getId() == View.NO_ID) {
- throw new IllegalStateException("ViewPager with adapter " + this + " has no id");
- }
- }
-
- @Override
- public Object instantiateItem(ViewGroup container, int position) {
- if (mCurrentTransaction == null) {
- mCurrentTransaction = mFragmentManager.beginTransaction();
- }
-
- // Use the fragment located in the fragment manager if one exists.
- final UiDataModel.Tab tab = UiDataModel.getUiDataModel().getTabAt(position);
- Fragment fragment = mFragmentManager.findFragmentByTag(tab.name());
- if (fragment != null) {
- mCurrentTransaction.attach(fragment);
- } else {
- fragment = getDeskClockFragment(position);
- mCurrentTransaction.add(container.getId(), fragment, tab.name());
- }
-
- if (fragment != mCurrentPrimaryItem) {
- FragmentCompat.setMenuVisibility(fragment, false);
- FragmentCompat.setUserVisibleHint(fragment, false);
- }
-
- return fragment;
- }
-
- @Override
- public void destroyItem(ViewGroup container, int position, Object object) {
- if (mCurrentTransaction == null) {
- mCurrentTransaction = mFragmentManager.beginTransaction();
- }
- final DeskClockFragment fragment = (DeskClockFragment) object;
- fragment.setFabContainer(null);
- mCurrentTransaction.detach(fragment);
- }
-
- @Override
- public void setPrimaryItem(ViewGroup container, int position, Object object) {
- final Fragment fragment = (Fragment) object;
- if (fragment != mCurrentPrimaryItem) {
- if (mCurrentPrimaryItem != null) {
- FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false);
- FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false);
- }
- if (fragment != null) {
- FragmentCompat.setMenuVisibility(fragment, true);
- FragmentCompat.setUserVisibleHint(fragment, true);
- }
- mCurrentPrimaryItem = fragment;
- }
- }
-
- @Override
- public void finishUpdate(ViewGroup container) {
- if (mCurrentTransaction != null) {
- mCurrentTransaction.commitAllowingStateLoss();
- mCurrentTransaction = null;
- mFragmentManager.executePendingTransactions();
- }
- }
-
- @Override
- public boolean isViewFromObject(View view, Object object) {
- return ((Fragment) object).getView() == view;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/FragmentTabPagerAdapter.kt b/src/com/android/deskclock/FragmentTabPagerAdapter.kt
new file mode 100644
index 0000000..1c920f5
--- /dev/null
+++ b/src/com/android/deskclock/FragmentTabPagerAdapter.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.util.ArrayMap
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.viewpager.widget.PagerAdapter
+
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * This adapter produces the DeskClockFragments that are the content of the DeskClock tabs. The
+ * adapter presents the tabs in LTR and RTL order depending on the text layout direction for the
+ * current locale. To prevent issues when switching between LTR and RTL, fragments are registered
+ * with the manager using position-independent tags, which is an important departure from
+ * FragmentPagerAdapter.
+ */
+internal class FragmentTabPagerAdapter(private val mDeskClock: DeskClock) : PagerAdapter() {
+
+ /** The manager into which fragments are added. */
+ private val mFragmentManager: FragmentManager = mDeskClock.supportFragmentManager
+
+ /** A fragment cache that can be accessed before [.instantiateItem] is called. */
+ private val mFragmentCache: MutableMap<UiDataModel.Tab, DeskClockFragment?> =
+ ArrayMap(getCount())
+
+ /** The active fragment transaction if one exists. */
+ private var mCurrentTransaction: FragmentTransaction? = null
+
+ /** The current fragment displayed to the user. */
+ private var mCurrentPrimaryItem: Fragment? = null
+
+ override fun getCount(): Int = UiDataModel.uiDataModel.tabCount
+
+ /**
+ * @param position the left-to-right index of the fragment to be returned
+ * @return the fragment displayed at the given `position`
+ */
+ fun getDeskClockFragment(position: Int): DeskClockFragment {
+ // Fetch the tab the UiDataModel reports for the position.
+ val tab: UiDataModel.Tab = UiDataModel.uiDataModel.getTabAt(position)
+
+ // First check the local cache for the fragment.
+ var fragment = mFragmentCache[tab]
+ if (fragment != null) {
+ return fragment
+ }
+
+ // Next check the fragment manager; relevant when app is rebuilt after locale changes
+ // because this adapter will be new and mFragmentCache will be empty, but the fragment
+ // manager will retain the Fragments built on original application launch.
+ fragment = mFragmentManager.findFragmentByTag(tab.name) as DeskClockFragment?
+ if (fragment != null) {
+ fragment.setFabContainer(mDeskClock)
+ mFragmentCache[tab] = fragment
+ return fragment
+ }
+
+ // Otherwise, build the fragment from scratch.
+ val fragmentClassName: String = tab.fragmentClassName
+ fragment = Fragment.instantiate(mDeskClock, fragmentClassName) as DeskClockFragment
+ fragment.setFabContainer(mDeskClock)
+ mFragmentCache[tab] = fragment
+ return fragment
+ }
+
+ override fun startUpdate(container: ViewGroup) {
+ check(container.id != View.NO_ID) { "ViewPager with adapter $this has no id" }
+ }
+
+ override fun instantiateItem(container: ViewGroup, position: Int): Any {
+ if (mCurrentTransaction == null) {
+ mCurrentTransaction = mFragmentManager.beginTransaction()
+ }
+
+ // Use the fragment located in the fragment manager if one exists.
+ val tab: UiDataModel.Tab = UiDataModel.uiDataModel.getTabAt(position)
+ var fragment = mFragmentManager.findFragmentByTag(tab.name)
+ if (fragment != null) {
+ mCurrentTransaction!!.attach(fragment)
+ } else {
+ fragment = getDeskClockFragment(position)
+ mCurrentTransaction!!.add(container.id, fragment, tab.name)
+ }
+
+ if (fragment !== mCurrentPrimaryItem) {
+ fragment.setMenuVisibility(false)
+ fragment.setUserVisibleHint(false)
+ }
+
+ return fragment
+ }
+
+ override fun destroyItem(container: ViewGroup, position: Int, any: Any) {
+ if (mCurrentTransaction == null) {
+ mCurrentTransaction = mFragmentManager.beginTransaction()
+ }
+ val fragment = any as DeskClockFragment
+ fragment.setFabContainer(null)
+ mCurrentTransaction!!.detach(fragment)
+ }
+
+ override fun setPrimaryItem(container: ViewGroup, position: Int, any: Any) {
+ val fragment = any as Fragment
+ if (fragment !== mCurrentPrimaryItem) {
+ mCurrentPrimaryItem?.let {
+ it.setMenuVisibility(false)
+ it.setUserVisibleHint(false)
+ }
+ fragment.setMenuVisibility(true)
+ fragment.setUserVisibleHint(true)
+ mCurrentPrimaryItem = fragment
+ }
+ }
+
+ override fun finishUpdate(container: ViewGroup) {
+ if (mCurrentTransaction != null) {
+ mCurrentTransaction!!.commitAllowingStateLoss()
+ mCurrentTransaction = null
+ mFragmentManager.executePendingTransactions()
+ }
+ }
+
+ override fun isViewFromObject(view: View, any: Any): Boolean = (any as Fragment).view === view
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/HandleApiCalls.java b/src/com/android/deskclock/HandleApiCalls.java
deleted file mode 100644
index a4a7012..0000000
--- a/src/com/android/deskclock/HandleApiCalls.java
+++ /dev/null
@@ -1,633 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.Activity;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.provider.AlarmClock;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-
-import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.controller.Controller;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.data.Weekdays;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.timer.TimerFragment;
-import com.android.deskclock.timer.TimerService;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static com.android.deskclock.AlarmSelectionActivity.ACTION_DISMISS;
-import static com.android.deskclock.AlarmSelectionActivity.EXTRA_ACTION;
-import static com.android.deskclock.AlarmSelectionActivity.EXTRA_ALARMS;
-import static com.android.deskclock.provider.AlarmInstance.FIRED_STATE;
-import static com.android.deskclock.provider.AlarmInstance.SNOOZE_STATE;
-import static com.android.deskclock.uidata.UiDataModel.Tab.ALARMS;
-import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
-
-/**
- * This activity is never visible. It processes all public intents defined by {@link AlarmClock}
- * that apply to alarms and timers. Its definition in AndroidManifest.xml requires callers to hold
- * the com.android.alarm.permission.SET_ALARM permission to complete the requested action.
- */
-public class HandleApiCalls extends Activity {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("HandleApiCalls");
-
- private Context mAppContext;
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- mAppContext = getApplicationContext();
-
- try {
- final Intent intent = getIntent();
- final String action = intent == null ? null : intent.getAction();
- if (action == null) {
- return;
- }
- LOGGER.i("onCreate: " + intent);
-
- switch (action) {
-
- case AlarmClock.ACTION_SET_ALARM:
- handleSetAlarm(intent);
- break;
- case AlarmClock.ACTION_SHOW_ALARMS:
- handleShowAlarms();
- break;
- case AlarmClock.ACTION_SET_TIMER:
- handleSetTimer(intent);
- break;
- case AlarmClock.ACTION_SHOW_TIMERS:
- handleShowTimers(intent);
- break;
- case AlarmClock.ACTION_DISMISS_ALARM:
- handleDismissAlarm(intent);
- break;
- case AlarmClock.ACTION_SNOOZE_ALARM:
- handleSnoozeAlarm(intent);
- break;
- case AlarmClock.ACTION_DISMISS_TIMER:
- handleDismissTimer(intent);
- break;
- }
- } catch (Exception e) {
- LOGGER.wtf(e);
- } finally {
- finish();
- }
- }
-
-
- private void handleDismissAlarm(Intent intent) {
- // Change to the alarms tab.
- UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
- // Open DeskClock which is now positioned on the alarms tab.
- startActivity(new Intent(mAppContext, DeskClock.class));
-
- new DismissAlarmAsync(mAppContext, intent, this).execute();
- }
-
- public static void dismissAlarm(Alarm alarm, Activity activity) {
- final Context context = activity.getApplicationContext();
- final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
- context.getContentResolver(), alarm.id);
- if (instance == null) {
- final String reason = context.getString(R.string.no_alarm_scheduled_for_this_time);
- Controller.getController().notifyVoiceFailure(activity, reason);
- LOGGER.i("No alarm instance to dismiss");
- return;
- }
-
- dismissAlarmInstance(instance, activity);
- }
-
- public static void dismissAlarmInstance(AlarmInstance instance, Activity activity) {
- Utils.enforceNotMainLooper();
-
- final Context context = activity.getApplicationContext();
- final Date alarmTime = instance.getAlarmTime().getTime();
- final String time = DateFormat.getTimeFormat(context).format(alarmTime);
-
- if (instance.mAlarmState == FIRED_STATE || instance.mAlarmState == SNOOZE_STATE) {
- // Always dismiss alarms that are fired or snoozed.
- AlarmStateManager.deleteInstanceAndUpdateParent(context, instance);
- } else if (Utils.isAlarmWithin24Hours(instance)) {
- // Upcoming alarms are always predismissed.
- AlarmStateManager.setPreDismissState(context, instance);
- } else {
- // Otherwise the alarm cannot be dismissed at this time.
- final String reason = context.getString(
- R.string.alarm_cant_be_dismissed_still_more_than_24_hours_away, time);
- Controller.getController().notifyVoiceFailure(activity, reason);
- LOGGER.i("Can't dismiss alarm more than 24 hours in advance");
- }
-
- // Log the successful dismissal.
- final String reason = context.getString(R.string.alarm_is_dismissed, time);
- Controller.getController().notifyVoiceSuccess(activity, reason);
- LOGGER.i("Alarm dismissed: " + instance);
- Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
- }
-
- private static class DismissAlarmAsync extends AsyncTask<Void, Void, Void> {
-
- private final Context mContext;
- private final Intent mIntent;
- private final Activity mActivity;
-
- public DismissAlarmAsync(Context context, Intent intent, Activity activity) {
- mContext = context;
- mIntent = intent;
- mActivity = activity;
- }
-
- @Override
- protected Void doInBackground(Void... parameters) {
- final ContentResolver cr = mContext.getContentResolver();
- final List<Alarm> alarms = getEnabledAlarms(mContext);
- if (alarms.isEmpty()) {
- final String reason = mContext.getString(R.string.no_scheduled_alarms);
- Controller.getController().notifyVoiceFailure(mActivity, reason);
- LOGGER.i("No scheduled alarms");
- return null;
- }
-
- // remove Alarms in MISSED, DISMISSED, and PREDISMISSED states
- for (Iterator<Alarm> i = alarms.iterator(); i.hasNext();) {
- final AlarmInstance instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
- cr, i.next().id);
- if (instance == null || instance.mAlarmState > FIRED_STATE) {
- i.remove();
- }
- }
-
- final String searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE);
- if (searchMode == null && alarms.size() > 1) {
- // shows the UI where user picks which alarm they want to DISMISS
- final Intent pickSelectionIntent = new Intent(mContext,
- AlarmSelectionActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(EXTRA_ACTION, ACTION_DISMISS)
- .putExtra(EXTRA_ALARMS, alarms.toArray(new Parcelable[alarms.size()]));
- mContext.startActivity(pickSelectionIntent);
- final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
- Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
- return null;
- }
-
- // fetch the alarms that are specified by the intent
- final FetchMatchingAlarmsAction fmaa =
- new FetchMatchingAlarmsAction(mContext, alarms, mIntent, mActivity);
- fmaa.run();
- final List<Alarm> matchingAlarms = fmaa.getMatchingAlarms();
-
- // If there are multiple matching alarms and it wasn't expected
- // disambiguate what the user meant
- if (!AlarmClock.ALARM_SEARCH_MODE_ALL.equals(searchMode) && matchingAlarms.size() > 1) {
- final Intent pickSelectionIntent = new Intent(mContext, AlarmSelectionActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(EXTRA_ACTION, ACTION_DISMISS)
- .putExtra(EXTRA_ALARMS,
- matchingAlarms.toArray(new Parcelable[matchingAlarms.size()]));
- mContext.startActivity(pickSelectionIntent);
- final String voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss);
- Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage);
- return null;
- }
-
- // Apply the action to the matching alarms
- for (Alarm alarm : matchingAlarms) {
- dismissAlarm(alarm, mActivity);
- LOGGER.i("Alarm dismissed: " + alarm);
- }
- return null;
- }
-
- private static List<Alarm> getEnabledAlarms(Context context) {
- final String selection = String.format("%s=?", Alarm.ENABLED);
- final String[] args = { "1" };
- return Alarm.getAlarms(context.getContentResolver(), selection, args);
- }
- }
-
- private void handleSnoozeAlarm(Intent intent) {
- new SnoozeAlarmAsync(intent, this).execute();
- }
-
- private static class SnoozeAlarmAsync extends AsyncTask<Void, Void, Void> {
-
- private final Context mContext;
- private final Intent mIntent;
- private final Activity mActivity;
-
- public SnoozeAlarmAsync(Intent intent, Activity activity) {
- mContext = activity.getApplicationContext();
- mIntent = intent;
- mActivity = activity;
- }
-
- @Override
- protected Void doInBackground(Void... parameters) {
- final ContentResolver cr = mContext.getContentResolver();
- final List<AlarmInstance> alarmInstances = AlarmInstance.getInstancesByState(
- cr, FIRED_STATE);
- if (alarmInstances.isEmpty()) {
- final String reason = mContext.getString(R.string.no_firing_alarms);
- Controller.getController().notifyVoiceFailure(mActivity, reason);
- LOGGER.i("No firing alarms");
- return null;
- }
-
- for (AlarmInstance firingAlarmInstance : alarmInstances) {
- snoozeAlarm(firingAlarmInstance, mContext, mActivity);
- }
- return null;
- }
- }
-
- static void snoozeAlarm(AlarmInstance alarmInstance, Context context, Activity activity) {
- Utils.enforceNotMainLooper();
-
- final String time = DateFormat.getTimeFormat(context).format(
- alarmInstance.getAlarmTime().getTime());
- final String reason = context.getString(R.string.alarm_is_snoozed, time);
- AlarmStateManager.setSnoozeState(context, alarmInstance, true);
-
- Controller.getController().notifyVoiceSuccess(activity, reason);
- LOGGER.i("Alarm snoozed: " + alarmInstance);
- Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
- }
-
- /***
- * Processes the SET_ALARM intent
- * @param intent Intent passed to the app
- */
- private void handleSetAlarm(Intent intent) {
- // Validate the hour, if one was given.
- int hour = -1;
- if (intent.hasExtra(AlarmClock.EXTRA_HOUR)) {
- hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, hour);
- if (hour < 0 || hour > 23) {
- final int mins = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
- final String voiceMessage = getString(R.string.invalid_time, hour, mins, " ");
- Controller.getController().notifyVoiceFailure(this, voiceMessage);
- LOGGER.i("Illegal hour: " + hour);
- return;
- }
- }
-
- // Validate the minute, if one was given.
- final int minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0);
- if (minutes < 0 || minutes > 59) {
- final String voiceMessage = getString(R.string.invalid_time, hour, minutes, " ");
- Controller.getController().notifyVoiceFailure(this, voiceMessage);
- LOGGER.i("Illegal minute: " + minutes);
- return;
- }
-
- final boolean skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false);
- final ContentResolver cr = getContentResolver();
-
- // If time information was not provided an existing alarm cannot be located and a new one
- // cannot be created so show the UI for creating the alarm from scratch per spec.
- if (hour == -1) {
- // Change to the alarms tab.
- UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
- // Intent has no time or an invalid time, open the alarm creation UI.
- final Intent createAlarm = Alarm.createIntent(this, DeskClock.class, Alarm.INVALID_ID)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(AlarmClockFragment.ALARM_CREATE_NEW_INTENT_EXTRA, true);
-
- // Open DeskClock which is now positioned on the alarms tab.
- startActivity(createAlarm);
- final String voiceMessage = getString(R.string.invalid_time, hour, minutes, " ");
- Controller.getController().notifyVoiceFailure(this, voiceMessage);
- LOGGER.i("Missing alarm time; opening UI");
- return;
- }
-
- final StringBuilder selection = new StringBuilder();
- final List<String> argsList = new ArrayList<>();
- setSelectionFromIntent(intent, hour, minutes, selection, argsList);
-
- // Try to locate an existing alarm using the intent data.
- final String[] args = argsList.toArray(new String[argsList.size()]);
- final List<Alarm> alarms = Alarm.getAlarms(cr, selection.toString(), args);
-
- final Alarm alarm;
- if (!alarms.isEmpty()) {
- // Enable the first matching alarm.
- alarm = alarms.get(0);
- alarm.enabled = true;
- Alarm.updateAlarm(cr, alarm);
-
- // Delete all old instances.
- AlarmStateManager.deleteAllInstances(this, alarm.id);
-
- Events.sendAlarmEvent(R.string.action_update, R.string.label_intent);
- LOGGER.i("Updated alarm: " + alarm);
- } else {
- // No existing alarm could be located; create one using the intent data.
- alarm = new Alarm();
- updateAlarmFromIntent(alarm, intent);
- alarm.deleteAfterUse = !alarm.daysOfWeek.isRepeating() && skipUi;
-
- // Save the new alarm.
- Alarm.addAlarm(cr, alarm);
-
- Events.sendAlarmEvent(R.string.action_create, R.string.label_intent);
- LOGGER.i("Created new alarm: " + alarm);
- }
-
- // Schedule the next instance.
- final Calendar now = DataModel.getDataModel().getCalendar();
- final AlarmInstance alarmInstance = alarm.createInstanceAfter(now);
- setupInstance(alarmInstance, skipUi);
-
- final String time = DateFormat.getTimeFormat(this)
- .format(alarmInstance.getAlarmTime().getTime());
- Controller.getController().notifyVoiceSuccess(this, getString(R.string.alarm_is_set, time));
- }
-
- private void handleDismissTimer(Intent intent) {
- final Uri dataUri = intent.getData();
- if (dataUri != null) {
- final Timer selectedTimer = getSelectedTimer(dataUri);
- if (selectedTimer != null) {
- DataModel.getDataModel().resetOrDeleteTimer(selectedTimer, R.string.label_intent);
- Controller.getController().notifyVoiceSuccess(this,
- getResources().getQuantityString(R.plurals.expired_timers_dismissed, 1));
- LOGGER.i("Timer dismissed: " + selectedTimer);
- } else {
- Controller.getController().notifyVoiceFailure(this,
- getString(R.string.invalid_timer));
- LOGGER.e("Could not dismiss timer: invalid URI");
- }
- } else {
- final List<Timer> expiredTimers = DataModel.getDataModel().getExpiredTimers();
- if (!expiredTimers.isEmpty()) {
- for (Timer timer : expiredTimers) {
- DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_intent);
- }
- final int numberOfTimers = expiredTimers.size();
- final String timersDismissedMessage = getResources().getQuantityString(
- R.plurals.expired_timers_dismissed, numberOfTimers, numberOfTimers);
- Controller.getController().notifyVoiceSuccess(this, timersDismissedMessage);
- LOGGER.i(timersDismissedMessage);
- } else {
- Controller.getController().notifyVoiceFailure(this,
- getString(R.string.no_expired_timers));
- LOGGER.e("Could not dismiss timer: no expired timers");
- }
- }
- }
-
- private Timer getSelectedTimer(Uri dataUri) {
- try {
- final int timerId = (int) ContentUris.parseId(dataUri);
- return DataModel.getDataModel().getTimer(timerId);
- } catch (NumberFormatException e) {
- return null;
- }
- }
-
- private void handleShowAlarms() {
- Events.sendAlarmEvent(R.string.action_show, R.string.label_intent);
-
- // Open DeskClock positioned on the alarms tab.
- UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
- startActivity(new Intent(this, DeskClock.class));
- }
-
- private void handleShowTimers(Intent intent) {
- Events.sendTimerEvent(R.string.action_show, R.string.label_intent);
-
- final Intent showTimersIntent = new Intent(this, DeskClock.class);
-
- final List<Timer> timers = DataModel.getDataModel().getTimers();
- if (!timers.isEmpty()) {
- final Timer newestTimer = timers.get(timers.size() - 1);
- showTimersIntent.putExtra(TimerService.EXTRA_TIMER_ID, newestTimer.getId());
- }
-
- // Open DeskClock positioned on the timers tab.
- UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
- startActivity(showTimersIntent);
- }
-
- private void handleSetTimer(Intent intent) {
- // If no length is supplied, show the timer setup view.
- if (!intent.hasExtra(AlarmClock.EXTRA_LENGTH)) {
- // Change to the timers tab.
- UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
-
- // Open DeskClock which is now positioned on the timers tab and show the timer setup.
- startActivity(TimerFragment.createTimerSetupIntent(this));
- LOGGER.i("Showing timer setup");
- return;
- }
-
- // Verify that the timer length is between one second and one day.
- final long lengthMillis = SECOND_IN_MILLIS * intent.getIntExtra(AlarmClock.EXTRA_LENGTH, 0);
- if (lengthMillis < Timer.MIN_LENGTH) {
- final String voiceMessage = getString(R.string.invalid_timer_length);
- Controller.getController().notifyVoiceFailure(this, voiceMessage);
- LOGGER.i("Invalid timer length requested: " + lengthMillis);
- return;
- }
-
- final String label = getLabelFromIntent(intent, "");
- final boolean skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false);
-
- // Attempt to reuse an existing timer that is Reset with the same length and label.
- Timer timer = null;
- for (Timer t : DataModel.getDataModel().getTimers()) {
- if (!t.isReset()) { continue; }
- if (t.getLength() != lengthMillis) { continue; }
- if (!TextUtils.equals(label, t.getLabel())) { continue; }
-
- timer = t;
- break;
- }
-
- // Create a new timer if one could not be reused.
- if (timer == null) {
- timer = DataModel.getDataModel().addTimer(lengthMillis, label, skipUi);
- Events.sendTimerEvent(R.string.action_create, R.string.label_intent);
- }
-
- // Start the selected timer.
- DataModel.getDataModel().startTimer(timer);
- Events.sendTimerEvent(R.string.action_start, R.string.label_intent);
- Controller.getController().notifyVoiceSuccess(this, getString(R.string.timer_created));
-
- // If not instructed to skip the UI, display the running timer.
- if (!skipUi) {
- // Change to the timers tab.
- UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
-
- // Open DeskClock which is now positioned on the timers tab.
- startActivity(new Intent(this, DeskClock.class)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId()));
- }
- }
-
- private void setupInstance(AlarmInstance instance, boolean skipUi) {
- instance = AlarmInstance.addInstance(this.getContentResolver(), instance);
- AlarmStateManager.registerInstance(this, instance, true);
- AlarmUtils.popAlarmSetToast(this, instance.getAlarmTime().getTimeInMillis());
- if (!skipUi) {
- // Change to the alarms tab.
- UiDataModel.getUiDataModel().setSelectedTab(ALARMS);
-
- // Open DeskClock which is now positioned on the alarms tab.
- final Intent showAlarm = Alarm.createIntent(this, DeskClock.class, instance.mAlarmId)
- .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, instance.mAlarmId)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(showAlarm);
- }
- }
-
- /**
- * @param alarm the alarm to be updated
- * @param intent the intent containing new alarm field values to merge into the {@code alarm}
- */
- private static void updateAlarmFromIntent(Alarm alarm, Intent intent) {
- alarm.enabled = true;
- alarm.hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, alarm.hour);
- alarm.minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, alarm.minutes);
- alarm.vibrate = intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, alarm.vibrate);
- alarm.alert = getAlertFromIntent(intent, alarm.alert);
- alarm.label = getLabelFromIntent(intent, alarm.label);
- alarm.daysOfWeek = getDaysFromIntent(intent, alarm.daysOfWeek);
- }
-
- private static String getLabelFromIntent(Intent intent, String defaultLabel) {
- final String message = intent.getExtras().getString(AlarmClock.EXTRA_MESSAGE, defaultLabel);
- return message == null ? "" : message;
- }
-
- private static Weekdays getDaysFromIntent(Intent intent, Weekdays defaultWeekdays) {
- if (!intent.hasExtra(AlarmClock.EXTRA_DAYS)) {
- return defaultWeekdays;
- }
-
- final List<Integer> days = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS);
- if (days != null) {
- final int[] daysArray = new int[days.size()];
- for (int i = 0; i < days.size(); i++) {
- daysArray[i] = days.get(i);
- }
- return Weekdays.fromCalendarDays(daysArray);
- } else {
- // API says to use an ArrayList<Integer> but we allow the user to use a int[] too.
- final int[] daysArray = intent.getIntArrayExtra(AlarmClock.EXTRA_DAYS);
- if (daysArray != null) {
- return Weekdays.fromCalendarDays(daysArray);
- }
- }
- return defaultWeekdays;
- }
-
- private static Uri getAlertFromIntent(Intent intent, Uri defaultUri) {
- final String alert = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE);
- if (alert == null) {
- return defaultUri;
- } else if (AlarmClock.VALUE_RINGTONE_SILENT.equals(alert) || alert.isEmpty()) {
- return Alarm.NO_RINGTONE_URI;
- }
-
- return Uri.parse(alert);
- }
-
- /**
- * Assemble a database where clause to search for an alarm matching the given {@code hour} and
- * {@code minutes} as well as all of the optional information within the {@code intent}
- * including:
- *
- * <ul>
- * <li>alarm message</li>
- * <li>repeat days</li>
- * <li>vibration setting</li>
- * <li>ringtone uri</li>
- * </ul>
- *
- * @param intent contains details of the alarm to be located
- * @param hour the hour of the day of the alarm
- * @param minutes the minute of the hour of the alarm
- * @param selection an out parameter containing a SQL where clause
- * @param args an out parameter containing the values to substitute into the {@code selection}
- */
- private void setSelectionFromIntent(
- Intent intent,
- int hour,
- int minutes,
- StringBuilder selection,
- List<String> args) {
- selection.append(Alarm.HOUR).append("=?");
- args.add(String.valueOf(hour));
- selection.append(" AND ").append(Alarm.MINUTES).append("=?");
- args.add(String.valueOf(minutes));
-
- if (intent.hasExtra(AlarmClock.EXTRA_MESSAGE)) {
- selection.append(" AND ").append(Alarm.LABEL).append("=?");
- args.add(getLabelFromIntent(intent, ""));
- }
-
- // Days is treated differently than other fields because if days is not specified, it
- // explicitly means "not recurring".
- selection.append(" AND ").append(Alarm.DAYS_OF_WEEK).append("=?");
- args.add(String.valueOf(getDaysFromIntent(intent, Weekdays.NONE).getBits()));
-
- if (intent.hasExtra(AlarmClock.EXTRA_VIBRATE)) {
- selection.append(" AND ").append(Alarm.VIBRATE).append("=?");
- args.add(intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, false) ? "1" : "0");
- }
-
- if (intent.hasExtra(AlarmClock.EXTRA_RINGTONE)) {
- selection.append(" AND ").append(Alarm.RINGTONE).append("=?");
-
- // If the intent explicitly specified a NULL ringtone, treat it as the default ringtone.
- final Uri defaultRingtone = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
- final Uri ringtone = getAlertFromIntent(intent, defaultRingtone);
- args.add(ringtone.toString());
- }
- }
-}
diff --git a/src/com/android/deskclock/HandleApiCalls.kt b/src/com/android/deskclock/HandleApiCalls.kt
new file mode 100644
index 0000000..4e59cc1
--- /dev/null
+++ b/src/com/android/deskclock/HandleApiCalls.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.Activity
+import android.content.ContentUris
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Bundle
+import android.os.Parcelable
+import android.provider.AlarmClock
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+
+import com.android.deskclock.AlarmUtils.popAlarmSetToast
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.controller.Controller
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.data.Weekdays
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.timer.TimerFragment
+import com.android.deskclock.timer.TimerService
+import com.android.deskclock.uidata.UiDataModel
+
+import java.util.Calendar
+import java.util.Date
+
+/**
+ * This activity is never visible. It processes all public intents defined by [AlarmClock]
+ * that apply to alarms and timers. Its definition in AndroidManifest.xml requires callers to hold
+ * the com.android.alarm.permission.SET_ALARM permission to complete the requested action.
+ */
+// TODO(b/165664115) Replace deprecated AsyncTask calls
+class HandleApiCalls : Activity() {
+ private lateinit var mAppContext: Context
+
+ override fun onCreate(icicle: Bundle?) {
+ super.onCreate(icicle)
+
+ mAppContext = applicationContext
+
+ try {
+ val intent = intent
+ val action = intent?.action ?: return
+ LOGGER.i("onCreate: $intent")
+
+ when (action) {
+ AlarmClock.ACTION_SET_ALARM -> handleSetAlarm(intent)
+ AlarmClock.ACTION_SHOW_ALARMS -> handleShowAlarms()
+ AlarmClock.ACTION_SET_TIMER -> handleSetTimer(intent)
+ AlarmClock.ACTION_SHOW_TIMERS -> handleShowTimers(intent)
+ AlarmClock.ACTION_DISMISS_ALARM -> handleDismissAlarm(intent)
+ AlarmClock.ACTION_SNOOZE_ALARM -> handleSnoozeAlarm(intent)
+ AlarmClock.ACTION_DISMISS_TIMER -> handleDismissTimer(intent)
+ }
+ } catch (e: Exception) {
+ LOGGER.wtf(e)
+ } finally {
+ finish()
+ }
+ }
+
+ private fun handleDismissAlarm(intent: Intent) {
+ // Change to the alarms tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+ // Open DeskClock which is now positioned on the alarms tab.
+ startActivity(Intent(mAppContext, DeskClock::class.java))
+
+ DismissAlarmAsync(mAppContext, intent, this).execute()
+ }
+
+ private class DismissAlarmAsync(
+ private val mContext: Context,
+ private val mIntent: Intent,
+ private val mActivity: Activity
+ ) : AsyncTask<Void?, Void?, Void?>() {
+ override fun doInBackground(vararg parameters: Void?): Void? {
+ val cr = mContext.contentResolver
+ val alarms = getEnabledAlarms(mContext)
+ if (alarms.isEmpty()) {
+ val reason = mContext.getString(R.string.no_scheduled_alarms)
+ Controller.getController().notifyVoiceFailure(mActivity, reason)
+ LOGGER.i("No scheduled alarms")
+ return null
+ }
+
+ // remove Alarms in MISSED, DISMISSED, and PREDISMISSED states
+ val i: MutableIterator<Alarm> = alarms.toMutableList().listIterator()
+ while (i.hasNext()) {
+ val instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(cr, i.next().id)
+ if (instance == null ||
+ instance.mAlarmState > ClockContract.InstancesColumns.FIRED_STATE) {
+ i.remove()
+ }
+ }
+
+ val searchMode = mIntent.getStringExtra(AlarmClock.EXTRA_ALARM_SEARCH_MODE)
+ if (searchMode == null && alarms.size > 1) {
+ // shows the UI where user picks which alarm they want to DISMISS
+ val pickSelectionIntent = Intent(mContext,
+ AlarmSelectionActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(AlarmSelectionActivity.EXTRA_ACTION,
+ AlarmSelectionActivity.ACTION_DISMISS)
+ .putExtra(AlarmSelectionActivity.EXTRA_ALARMS,
+ alarms.toTypedArray<Parcelable>())
+ mContext.startActivity(pickSelectionIntent)
+ val voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss)
+ Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage)
+ return null
+ }
+
+ // fetch the alarms that are specified by the intent
+ val fmaa = FetchMatchingAlarmsAction(mContext, alarms, mIntent, mActivity)
+ fmaa.run()
+ val matchingAlarms: List<Alarm> = fmaa.matchingAlarms
+
+ // If there are multiple matching alarms and it wasn't expected
+ // disambiguate what the user meant
+ if (AlarmClock.ALARM_SEARCH_MODE_ALL != searchMode && matchingAlarms.size > 1) {
+ val pickSelectionIntent = Intent(mContext, AlarmSelectionActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(AlarmSelectionActivity.EXTRA_ACTION,
+ AlarmSelectionActivity.ACTION_DISMISS)
+ .putExtra(AlarmSelectionActivity.EXTRA_ALARMS,
+ matchingAlarms.toTypedArray<Parcelable>())
+ mContext.startActivity(pickSelectionIntent)
+ val voiceMessage = mContext.getString(R.string.pick_alarm_to_dismiss)
+ Controller.getController().notifyVoiceSuccess(mActivity, voiceMessage)
+ return null
+ }
+
+ // Apply the action to the matching alarms
+ for (alarm in matchingAlarms) {
+ dismissAlarm(alarm, mActivity)
+ LOGGER.i("Alarm dismissed: $alarm")
+ }
+ return null
+ }
+
+ companion object {
+ private fun getEnabledAlarms(context: Context): List<Alarm> {
+ val selection = String.format("%s=?", AlarmsColumns.ENABLED)
+ val args = arrayOf("1")
+ return Alarm.getAlarms(context.contentResolver, selection, *args)
+ }
+ }
+ }
+
+ private fun handleSnoozeAlarm(intent: Intent) {
+ SnoozeAlarmAsync(intent, this).execute()
+ }
+
+ private class SnoozeAlarmAsync(
+ private val mIntent: Intent,
+ private val mActivity: Activity
+ ) : AsyncTask<Void?, Void?, Void?>() {
+ private val mContext: Context = mActivity.applicationContext
+
+ override fun doInBackground(vararg parameters: Void?): Void? {
+ val cr = mContext.contentResolver
+ val alarmInstances = AlarmInstance.getInstancesByState(
+ cr, ClockContract.InstancesColumns.FIRED_STATE)
+ if (alarmInstances.isEmpty()) {
+ val reason = mContext.getString(R.string.no_firing_alarms)
+ Controller.getController().notifyVoiceFailure(mActivity, reason)
+ LOGGER.i("No firing alarms")
+ return null
+ }
+
+ for (firingAlarmInstance in alarmInstances) {
+ snoozeAlarm(firingAlarmInstance, mContext, mActivity)
+ }
+ return null
+ }
+ }
+
+ /**
+ * Processes the SET_ALARM intent
+ * @param intent Intent passed to the app
+ */
+ private fun handleSetAlarm(intent: Intent) {
+ // Validate the hour, if one was given.
+ var hour = -1
+ if (intent.hasExtra(AlarmClock.EXTRA_HOUR)) {
+ hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, hour)
+ if (hour < 0 || hour > 23) {
+ val mins = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0)
+ val voiceMessage = getString(R.string.invalid_time, hour, mins, " ")
+ Controller.getController().notifyVoiceFailure(this, voiceMessage)
+ LOGGER.i("Illegal hour: $hour")
+ return
+ }
+ }
+
+ // Validate the minute, if one was given.
+ val minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, 0)
+ if (minutes < 0 || minutes > 59) {
+ val voiceMessage = getString(R.string.invalid_time, hour, minutes, " ")
+ Controller.getController().notifyVoiceFailure(this, voiceMessage)
+ LOGGER.i("Illegal minute: $minutes")
+ return
+ }
+
+ val skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false)
+ val cr = contentResolver
+
+ // If time information was not provided an existing alarm cannot be located and a new one
+ // cannot be created so show the UI for creating the alarm from scratch per spec.
+ if (hour == -1) {
+ // Change to the alarms tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+ // Intent has no time or an invalid time, open the alarm creation UI.
+ val createAlarm = Alarm.createIntent(this, DeskClock::class.java, Alarm.INVALID_ID)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(AlarmClockFragment.ALARM_CREATE_NEW_INTENT_EXTRA, true)
+
+ // Open DeskClock which is now positioned on the alarms tab.
+ startActivity(createAlarm)
+ val voiceMessage = getString(R.string.invalid_time, hour, minutes, " ")
+ Controller.getController().notifyVoiceFailure(this, voiceMessage)
+ LOGGER.i("Missing alarm time; opening UI")
+ return
+ }
+
+ val selection = StringBuilder()
+ val argsList: MutableList<String> = ArrayList()
+ setSelectionFromIntent(intent, hour, minutes, selection, argsList)
+
+ // Try to locate an existing alarm using the intent data.
+ val args = argsList.toTypedArray()
+ val alarms = Alarm.getAlarms(cr, selection.toString(), *args)
+
+ val alarm: Alarm
+ if (alarms.isNotEmpty()) {
+ // Enable the first matching alarm.
+ alarm = alarms[0]
+ alarm.enabled = true
+ Alarm.updateAlarm(cr, alarm)
+
+ // Delete all old instances.
+ AlarmStateManager.deleteAllInstances(this, alarm.id)
+
+ Events.sendAlarmEvent(R.string.action_update, R.string.label_intent)
+ LOGGER.i("Updated alarm: $alarm")
+ } else {
+ // No existing alarm could be located; create one using the intent data.
+ alarm = Alarm()
+ updateAlarmFromIntent(alarm, intent)
+ alarm.deleteAfterUse = !alarm.daysOfWeek.isRepeating && skipUi
+
+ // Save the new alarm.
+ Alarm.addAlarm(cr, alarm)
+
+ Events.sendAlarmEvent(R.string.action_create, R.string.label_intent)
+ LOGGER.i("Created new alarm: $alarm")
+ }
+
+ // Schedule the next instance.
+ val now: Calendar = DataModel.dataModel.calendar
+ val alarmInstance = alarm.createInstanceAfter(now)
+ setupInstance(alarmInstance, skipUi)
+
+ val time = DateFormat.getTimeFormat(this).format(alarmInstance.alarmTime.time)
+ Controller.getController().notifyVoiceSuccess(this, getString(R.string.alarm_is_set, time))
+ }
+
+ private fun handleDismissTimer(intent: Intent) {
+ val dataUri = intent.data
+ if (dataUri != null) {
+ val selectedTimer = getSelectedTimer(dataUri)
+ if (selectedTimer != null) {
+ DataModel.dataModel.resetOrDeleteTimer(selectedTimer, R.string.label_intent)
+ Controller.getController().notifyVoiceSuccess(this,
+ resources.getQuantityString(R.plurals.expired_timers_dismissed, 1))
+ LOGGER.i("Timer dismissed: $selectedTimer")
+ } else {
+ Controller.getController().notifyVoiceFailure(this,
+ getString(R.string.invalid_timer))
+ LOGGER.e("Could not dismiss timer: invalid URI")
+ }
+ } else {
+ val expiredTimers: List<Timer> = DataModel.dataModel.expiredTimers
+ if (expiredTimers.isNotEmpty()) {
+ for (timer in expiredTimers) {
+ DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_intent)
+ }
+ val numberOfTimers = expiredTimers.size
+ val timersDismissedMessage = resources.getQuantityString(
+ R.plurals.expired_timers_dismissed, numberOfTimers, numberOfTimers)
+ Controller.getController().notifyVoiceSuccess(this, timersDismissedMessage)
+ LOGGER.i(timersDismissedMessage)
+ } else {
+ Controller.getController().notifyVoiceFailure(this,
+ getString(R.string.no_expired_timers))
+ LOGGER.e("Could not dismiss timer: no expired timers")
+ }
+ }
+ }
+
+ private fun getSelectedTimer(dataUri: Uri): Timer? {
+ return try {
+ val timerId = ContentUris.parseId(dataUri).toInt()
+ DataModel.dataModel.getTimer(timerId)
+ } catch (e: NumberFormatException) {
+ null
+ }
+ }
+
+ private fun handleShowAlarms() {
+ Events.sendAlarmEvent(R.string.action_show, R.string.label_intent)
+
+ // Open DeskClock positioned on the alarms tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+ startActivity(Intent(this, DeskClock::class.java))
+ }
+
+ private fun handleShowTimers(intent: Intent) {
+ Events.sendTimerEvent(R.string.action_show, R.string.label_intent)
+
+ val showTimersIntent = Intent(this, DeskClock::class.java)
+
+ val timers: List<Timer> = DataModel.dataModel.timers
+ if (timers.isNotEmpty()) {
+ val newestTimer = timers[timers.size - 1]
+ showTimersIntent.putExtra(TimerService.EXTRA_TIMER_ID, newestTimer.id)
+ }
+
+ // Open DeskClock positioned on the timers tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+ startActivity(showTimersIntent)
+ }
+
+ private fun handleSetTimer(intent: Intent) {
+ // If no length is supplied, show the timer setup view.
+ if (!intent.hasExtra(AlarmClock.EXTRA_LENGTH)) {
+ // Change to the timers tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+
+ // Open DeskClock which is now positioned on the timers tab and show the timer setup.
+ startActivity(TimerFragment.createTimerSetupIntent(this))
+ LOGGER.i("Showing timer setup")
+ return
+ }
+
+ // Verify that the timer length is between one second and one day.
+ val lengthMillis =
+ DateUtils.SECOND_IN_MILLIS * intent.getIntExtra(AlarmClock.EXTRA_LENGTH, 0)
+ if (lengthMillis < Timer.MIN_LENGTH) {
+ val voiceMessage = getString(R.string.invalid_timer_length)
+ Controller.getController().notifyVoiceFailure(this, voiceMessage)
+ LOGGER.i("Invalid timer length requested: $lengthMillis")
+ return
+ }
+
+ val label = getLabelFromIntent(intent, "")
+ val skipUi = intent.getBooleanExtra(AlarmClock.EXTRA_SKIP_UI, false)
+
+ // Attempt to reuse an existing timer that is Reset with the same length and label.
+ var timer: Timer? = null
+ for (t in DataModel.dataModel.timers) {
+ if (!t.isReset) {
+ continue
+ }
+ if (t.length != lengthMillis) {
+ continue
+ }
+ if (!TextUtils.equals(label, t.label)) {
+ continue
+ }
+
+ timer = t
+ break
+ }
+
+ // Create a new timer if one could not be reused.
+ if (timer == null) {
+ timer = DataModel.dataModel.addTimer(lengthMillis, label, skipUi)
+ Events.sendTimerEvent(R.string.action_create, R.string.label_intent)
+ }
+
+ // Start the selected timer.
+ DataModel.dataModel.startTimer(timer)
+ Events.sendTimerEvent(R.string.action_start, R.string.label_intent)
+ Controller.getController().notifyVoiceSuccess(this, getString(R.string.timer_created))
+
+ // If not instructed to skip the UI, display the running timer.
+ if (!skipUi) {
+ // Change to the timers tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+
+ // Open DeskClock which is now positioned on the timers tab.
+ startActivity(Intent(this, DeskClock::class.java)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id))
+ }
+ }
+
+ private fun setupInstance(instance: AlarmInstance, skipUi: Boolean) {
+ var variableInstance = instance
+ variableInstance = AlarmInstance.addInstance(this.contentResolver, variableInstance)
+ AlarmStateManager.registerInstance(this, variableInstance, true)
+ popAlarmSetToast(this, variableInstance.alarmTime.timeInMillis)
+ if (!skipUi) {
+ // Change to the alarms tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.ALARMS
+
+ // Open DeskClock which is now positioned on the alarms tab.
+ val showAlarm =
+ Alarm.createIntent(this, DeskClock::class.java, variableInstance.mAlarmId!!)
+ .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA,
+ variableInstance.mAlarmId!!)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(showAlarm)
+ }
+ }
+
+ /**
+ * Assemble a database where clause to search for an alarm matching the given `hour` and
+ * `minutes` as well as all of the optional information within the `intent`
+ * including:
+ * <ul>
+ * <li>alarm message</li>
+ * <li>repeat days</li>
+ * <li>vibration setting</li>
+ * <li>ringtone uri</li>
+ * </ul>
+ *
+ * @param intent contains details of the alarm to be located
+ * @param hour the hour of the day of the alarm
+ * @param minutes the minute of the hour of the alarm
+ * @param selection an out parameter containing a SQL where clause
+ * @param args an out parameter containing the values to substitute into the `selection`
+ */
+ private fun setSelectionFromIntent(
+ intent: Intent,
+ hour: Int,
+ minutes: Int,
+ selection: StringBuilder,
+ args: MutableList<String>
+ ) {
+ selection.append(AlarmsColumns.HOUR).append("=?")
+ args.add(hour.toString())
+ selection.append(" AND ").append(AlarmsColumns.MINUTES).append("=?")
+ args.add(minutes.toString())
+ if (intent.hasExtra(AlarmClock.EXTRA_MESSAGE)) {
+ selection.append(" AND ").append(AlarmSettingColumns.LABEL).append("=?")
+ args.add(getLabelFromIntent(intent, ""))
+ }
+
+ // Days is treated differently than other fields because if days is not specified, it
+ // explicitly means "not recurring".
+ selection.append(" AND ").append(AlarmsColumns.DAYS_OF_WEEK).append("=?")
+ args.add(getDaysFromIntent(intent, Weekdays.NONE).bits.toString())
+ if (intent.hasExtra(AlarmClock.EXTRA_VIBRATE)) {
+ selection.append(" AND ").append(AlarmSettingColumns.VIBRATE).append("=?")
+ args.add(if (intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, false)) "1" else "0")
+ }
+ if (intent.hasExtra(AlarmClock.EXTRA_RINGTONE)) {
+ selection.append(" AND ").append(AlarmSettingColumns.RINGTONE).append("=?")
+
+ // If the intent explicitly specified a NULL ringtone, treat it as the default ringtone.
+ val defaultRingtone: Uri = DataModel.dataModel.defaultAlarmRingtoneUri
+ val ringtone = getAlertFromIntent(intent, defaultRingtone)
+ args.add(ringtone.toString())
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("HandleApiCalls")
+
+ fun dismissAlarm(alarm: Alarm, activity: Activity) {
+ val context = activity.applicationContext
+ val instance = AlarmInstance.getNextUpcomingInstanceByAlarmId(
+ context.contentResolver, alarm.id)
+ if (instance == null) {
+ val reason = context.getString(R.string.no_alarm_scheduled_for_this_time)
+ Controller.getController().notifyVoiceFailure(activity, reason)
+ LOGGER.i("No alarm instance to dismiss")
+ return
+ }
+
+ dismissAlarmInstance(instance, activity)
+ }
+
+ private fun dismissAlarmInstance(instance: AlarmInstance, activity: Activity) {
+ Utils.enforceNotMainLooper()
+
+ val context = activity.applicationContext
+ val alarmTime: Date = instance.alarmTime.time
+ val time = DateFormat.getTimeFormat(context).format(alarmTime)
+
+ if (instance.mAlarmState == ClockContract.InstancesColumns.FIRED_STATE ||
+ instance.mAlarmState == ClockContract.InstancesColumns.SNOOZE_STATE) {
+ // Always dismiss alarms that are fired or snoozed.
+ AlarmStateManager.deleteInstanceAndUpdateParent(context, instance)
+ } else if (Utils.isAlarmWithin24Hours(instance)) {
+ // Upcoming alarms are always predismissed.
+ AlarmStateManager.setPreDismissState(context, instance)
+ } else {
+ // Otherwise the alarm cannot be dismissed at this time.
+ val reason = context.getString(
+ R.string.alarm_cant_be_dismissed_still_more_than_24_hours_away, time)
+ Controller.getController().notifyVoiceFailure(activity, reason)
+ LOGGER.i("Can't dismiss alarm more than 24 hours in advance")
+ }
+
+ // Log the successful dismissal.
+ val reason = context.getString(R.string.alarm_is_dismissed, time)
+ Controller.getController().notifyVoiceSuccess(activity, reason)
+ LOGGER.i("Alarm dismissed: $instance")
+ Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent)
+ }
+
+ fun snoozeAlarm(alarmInstance: AlarmInstance, context: Context, activity: Activity) {
+ Utils.enforceNotMainLooper()
+
+ val time = DateFormat.getTimeFormat(context).format(
+ alarmInstance.alarmTime.time)
+ val reason = context.getString(R.string.alarm_is_snoozed, time)
+ AlarmStateManager.setSnoozeState(context, alarmInstance, true)
+
+ Controller.getController().notifyVoiceSuccess(activity, reason)
+ LOGGER.i("Alarm snoozed: $alarmInstance")
+ Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent)
+ }
+
+ /**
+ * @param alarm the alarm to be updated
+ * @param intent the intent containing new alarm field values to merge into the `alarm`
+ */
+ private fun updateAlarmFromIntent(alarm: Alarm, intent: Intent) {
+ alarm.enabled = true
+ alarm.hour = intent.getIntExtra(AlarmClock.EXTRA_HOUR, alarm.hour)
+ alarm.minutes = intent.getIntExtra(AlarmClock.EXTRA_MINUTES, alarm.minutes)
+ alarm.vibrate = intent.getBooleanExtra(AlarmClock.EXTRA_VIBRATE, alarm.vibrate)
+ alarm.alert = getAlertFromIntent(intent, alarm.alert!!)
+ alarm.label = getLabelFromIntent(intent, alarm.label)
+ alarm.daysOfWeek = getDaysFromIntent(intent, alarm.daysOfWeek)
+ }
+
+ private fun getLabelFromIntent(intent: Intent?, defaultLabel: String?): String {
+ val message = intent!!.extras!!.getString(AlarmClock.EXTRA_MESSAGE, defaultLabel)
+ return message ?: ""
+ }
+
+ private fun getDaysFromIntent(intent: Intent, defaultWeekdays: Weekdays): Weekdays {
+ if (!intent.hasExtra(AlarmClock.EXTRA_DAYS)) {
+ return defaultWeekdays
+ }
+
+ val days: List<Int>? = intent.getIntegerArrayListExtra(AlarmClock.EXTRA_DAYS)
+ if (days != null) {
+ val daysArray = IntArray(days.size)
+ for (i in days.indices) {
+ daysArray[i] = days[i]
+ }
+ return Weekdays.fromCalendarDays(*daysArray)
+ } else {
+ // API says to use an ArrayList<Integer> but we allow the user to use a int[] too.
+ val daysArray = intent.getIntArrayExtra(AlarmClock.EXTRA_DAYS)
+ if (daysArray != null) {
+ return Weekdays.fromCalendarDays(*daysArray)
+ }
+ }
+ return defaultWeekdays
+ }
+
+ private fun getAlertFromIntent(intent: Intent, defaultUri: Uri): Uri {
+ val alert = intent.getStringExtra(AlarmClock.EXTRA_RINGTONE)
+ if (alert == null) {
+ return defaultUri
+ } else if (AlarmClock.VALUE_RINGTONE_SILENT == alert || alert.isEmpty()) {
+ return AlarmSettingColumns.NO_RINGTONE_URI
+ }
+
+ return Uri.parse(alert)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/HandleShortcuts.java b/src/com/android/deskclock/HandleShortcuts.java
deleted file mode 100644
index 68bc42e..0000000
--- a/src/com/android/deskclock/HandleShortcuts.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-import com.android.deskclock.events.Events;
-import com.android.deskclock.stopwatch.StopwatchService;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
-
-public class HandleShortcuts extends Activity {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("HandleShortcuts");
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final Intent intent = getIntent();
-
- try {
- final String action = intent.getAction();
- switch (action) {
- case StopwatchService.ACTION_PAUSE_STOPWATCH:
- Events.sendStopwatchEvent(R.string.action_pause, R.string.label_shortcut);
-
- // Open DeskClock positioned on the stopwatch tab.
- UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
- startActivity(new Intent(this, DeskClock.class)
- .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH));
- setResult(RESULT_OK);
- break;
- case StopwatchService.ACTION_START_STOPWATCH:
- Events.sendStopwatchEvent(R.string.action_start, R.string.label_shortcut);
-
- // Open DeskClock positioned on the stopwatch tab.
- UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
- startActivity(new Intent(this, DeskClock.class)
- .setAction(StopwatchService.ACTION_START_STOPWATCH));
- setResult(RESULT_OK);
- break;
- default:
- throw new IllegalArgumentException("Unsupported action: " + action);
- }
- } catch (Exception e) {
- LOGGER.e("Error handling intent: " + intent, e);
- setResult(RESULT_CANCELED);
- } finally {
- finish();
- }
- }
-}
diff --git a/src/com/android/deskclock/HandleShortcuts.kt b/src/com/android/deskclock/HandleShortcuts.kt
new file mode 100644
index 0000000..11f2cee
--- /dev/null
+++ b/src/com/android/deskclock/HandleShortcuts.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Bundle
+
+import com.android.deskclock.events.Events
+import com.android.deskclock.stopwatch.StopwatchService
+import com.android.deskclock.uidata.UiDataModel
+
+class HandleShortcuts : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val intent = intent
+
+ try {
+ when (val action = intent.action) {
+ StopwatchService.ACTION_PAUSE_STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_pause, R.string.label_shortcut)
+
+ // Open DeskClock positioned on the stopwatch tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.STOPWATCH
+ startActivity(Intent(this, DeskClock::class.java)
+ .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH))
+ setResult(RESULT_OK)
+ }
+ StopwatchService.ACTION_START_STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_start, R.string.label_shortcut)
+
+ // Open DeskClock positioned on the stopwatch tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.STOPWATCH
+ startActivity(Intent(this, DeskClock::class.java)
+ .setAction(StopwatchService.ACTION_START_STOPWATCH))
+ setResult(RESULT_OK)
+ }
+ else -> throw IllegalArgumentException("Unsupported action: $action")
+ }
+ } catch (e: Exception) {
+ LOGGER.e("Error handling intent: $intent", e)
+ setResult(RESULT_CANCELED)
+ } finally {
+ finish()
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("HandleShortcuts")
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAdapter.java b/src/com/android/deskclock/ItemAdapter.java
deleted file mode 100644
index a417636..0000000
--- a/src/com/android/deskclock/ItemAdapter.java
+++ /dev/null
@@ -1,544 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import android.util.SparseArray;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static androidx.recyclerview.widget.RecyclerView.NO_ID;
-
-/**
- * Base adapter class for displaying a collection of items. Provides functionality for handling
- * changing items, persistent item state, item click events, and re-usable item views.
- */
-public class ItemAdapter<T extends ItemAdapter.ItemHolder>
- extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
-
- /**
- * Finds the position of the changed item holder and invokes {@link #notifyItemChanged(int)} or
- * {@link #notifyItemChanged(int, Object)} if payloads are present (in order to do in-place
- * change animations).
- */
- private final OnItemChangedListener mItemChangedNotifier = new OnItemChangedListener() {
- @Override
- public void onItemChanged(ItemHolder<?> itemHolder) {
- if (mOnItemChangedListener != null) {
- mOnItemChangedListener.onItemChanged(itemHolder);
- }
- final int position = mItemHolders.indexOf(itemHolder);
- if (position != RecyclerView.NO_POSITION) {
- notifyItemChanged(position);
- }
- }
-
- @Override
- public void onItemChanged(ItemHolder<?> itemHolder, Object payload) {
- if (mOnItemChangedListener != null) {
- mOnItemChangedListener.onItemChanged(itemHolder, payload);
- }
- final int position = mItemHolders.indexOf(itemHolder);
- if (position != RecyclerView.NO_POSITION) {
- notifyItemChanged(position, payload);
- }
- }
- };
-
- /**
- * Invokes the {@link OnItemClickedListener} in {@link #mListenersByViewType} corresponding
- * to {@link ItemViewHolder#getItemViewType()}
- */
- private final OnItemClickedListener mOnItemClickedListener = new OnItemClickedListener() {
- @Override
- public void onItemClicked(ItemViewHolder<?> viewHolder, int id) {
- final OnItemClickedListener listener =
- mListenersByViewType.get(viewHolder.getItemViewType());
- if (listener != null) {
- listener.onItemClicked(viewHolder, id);
- }
- }
- };
-
- /**
- * Invoked when any item changes.
- */
- private OnItemChangedListener mOnItemChangedListener;
-
- /**
- * Factories for creating new {@link ItemViewHolder} entities.
- */
- private final SparseArray<ItemViewHolder.Factory> mFactoriesByViewType = new SparseArray<>();
-
- /**
- * Listeners to invoke in {@link #mOnItemClickedListener}.
- */
- private final SparseArray<OnItemClickedListener> mListenersByViewType = new SparseArray<>();
-
- /**
- * List of current item holders represented by this adapter.
- */
- private List<T> mItemHolders;
-
- /**
- * Convenience for calling {@link #setHasStableIds(boolean)} with {@code true}.
- *
- * @return this object, allowing calls to methods in this class to be chained
- */
- public ItemAdapter setHasStableIds() {
- setHasStableIds(true);
- return this;
- }
-
- /**
- * Sets the {@link ItemViewHolder.Factory} and {@link OnItemClickedListener} used to create
- * new item view holders in {@link #onCreateViewHolder(ViewGroup, int)}.
- *
- * @param factory the {@link ItemViewHolder.Factory} used to create new item view holders
- * @param listener the {@link OnItemClickedListener} to be invoked by
- * {@link #mItemChangedNotifier}
- * @param viewTypes the unique identifier for the view types to be created
- * @return this object, allowing calls to methods in this class to be chained
- */
- public ItemAdapter withViewTypes(ItemViewHolder.Factory factory,
- OnItemClickedListener listener, int... viewTypes) {
- for (int viewType : viewTypes) {
- mFactoriesByViewType.put(viewType, factory);
- mListenersByViewType.put(viewType, listener);
- }
- return this;
- }
-
- /**
- * @return the current list of item holders represented by this adapter
- */
- public final List<T> getItems() {
- return mItemHolders;
- }
-
- /**
- * Sets the list of item holders to serve as the dataset for this adapter and invokes
- * {@link #notifyDataSetChanged()} to update the UI.
- * <p/>
- * If {@link #hasStableIds()} returns {@code true}, then the instance state will preserved
- * between new and old holders that have matching {@link ItemHolder#itemId} values.
- *
- * @param itemHolders the new list of item holders
- * @return this object, allowing calls to methods in this class to be chained
- */
- public ItemAdapter setItems(List<T> itemHolders) {
- final List<T> oldItemHolders = mItemHolders;
- if (oldItemHolders != itemHolders) {
- if (oldItemHolders != null) {
- // remove the item change listener from the old item holders
- for (T oldItemHolder : oldItemHolders) {
- oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier);
- }
- }
-
- if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
- // transfer instance state from old to new item holders based on item id,
- // we use a simple O(N^2) implementation since we assume the number of items is
- // relatively small and generating a temporary map would be more expensive
- final Bundle bundle = new Bundle();
- for (ItemHolder newItemHolder : itemHolders) {
- for (ItemHolder oldItemHolder : oldItemHolders) {
- if (newItemHolder.itemId == oldItemHolder.itemId
- && newItemHolder != oldItemHolder) {
- // clear any existing state from the bundle
- bundle.clear();
-
- // transfer instance state from old to new item holder
- oldItemHolder.onSaveInstanceState(bundle);
- newItemHolder.onRestoreInstanceState(bundle);
-
- break;
- }
- }
- }
- }
-
- if (itemHolders != null) {
- // add the item change listener to the new item holders
- for (ItemHolder newItemHolder : itemHolders) {
- newItemHolder.addOnItemChangedListener(mItemChangedNotifier);
- }
- }
-
- // finally update the current list of item holders and inform the RV to update the UI
- mItemHolders = itemHolders;
- notifyDataSetChanged();
- }
-
- return this;
- }
-
- /**
- * Inserts the specified item holder at the specified position. Invokes
- * {@link #notifyItemInserted} to update the UI.
- *
- * @param position the index to which to add the item holder
- * @param itemHolder the item holder to add
- * @return this object, allowing calls to methods in this class to be chained
- */
- public ItemAdapter addItem(int position, @NonNull T itemHolder) {
- itemHolder.addOnItemChangedListener(mItemChangedNotifier);
- position = Math.min(position, mItemHolders.size());
- mItemHolders.add(position, itemHolder);
- notifyItemInserted(position);
- return this;
- }
-
- /**
- * Removes the first occurrence of the specified element from this list, if it is present
- * (optional operation). If this list does not contain the element, it is unchanged. Invokes
- * {@link #notifyItemRemoved} to update the UI.
- *
- * @param itemHolder the item holder to remove
- * @return this object, allowing calls to methods in this class to be chained
- */
- public ItemAdapter removeItem(@NonNull T itemHolder) {
- final int index = mItemHolders.indexOf(itemHolder);
- if (index >= 0) {
- itemHolder = mItemHolders.remove(index);
- itemHolder.removeOnItemChangedListener(mItemChangedNotifier);
- notifyItemRemoved(index);
- }
- return this;
- }
-
- /**
- * Sets the listener to be invoked whenever any item changes.
- */
- public void setOnItemChangedListener(OnItemChangedListener listener) {
- mOnItemChangedListener = listener;
- }
-
- @Override
- public int getItemCount() {
- return mItemHolders == null ? 0 : mItemHolders.size();
- }
-
- @Override
- public long getItemId(int position) {
- return hasStableIds() ? mItemHolders.get(position).itemId : NO_ID;
- }
-
- public T findItemById(long id) {
- for (T holder : mItemHolders) {
- if (holder.itemId == id) {
- return holder;
- }
- }
- return null;
- }
-
- @Override
- public int getItemViewType(int position) {
- return mItemHolders.get(position).getItemViewType();
- }
-
- @Override
- public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- final ItemViewHolder.Factory factory = mFactoriesByViewType.get(viewType);
- if (factory != null) {
- return factory.createViewHolder(parent, viewType);
- }
- throw new IllegalArgumentException("Unsupported view type: " + viewType);
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public void onBindViewHolder(ItemViewHolder viewHolder, int position) {
- // suppress any unchecked warnings since it is up to the subclass to guarantee
- // compatibility of their view holders with the item holder at the corresponding position
- viewHolder.bindItemView(mItemHolders.get(position));
- viewHolder.setOnItemClickedListener(mOnItemClickedListener);
- }
-
- @Override
- public void onViewRecycled(ItemViewHolder viewHolder) {
- viewHolder.setOnItemClickedListener(null);
- viewHolder.recycleItemView();
- }
-
- /**
- * Base class for wrapping an item for compatibility with an {@link ItemHolder}.
- * <p/>
- * An {@link ItemHolder} serves as bridge between the model and view layer; subclassers should
- * implement properties that fall beyond the scope of their model layer but are necessary for
- * the view layer. Properties that should be persisted across dataset changes can be
- * preserved via the {@link #onSaveInstanceState(Bundle)} and
- * {@link #onRestoreInstanceState(Bundle)} methods.
- * <p/>
- * Note: An {@link ItemHolder} can be used by multiple {@link ItemHolder} and any state changes
- * should simultaneously be reflected in both UIs. It is not thread-safe however and should
- * only be used on a single thread at a given time.
- *
- * @param <T> the item type wrapped by the holder
- */
- public static abstract class ItemHolder<T> {
-
- /**
- * The item held by this holder.
- */
- public final T item;
-
- /**
- * Globally unique id corresponding to the item.
- */
- public final long itemId;
-
- /**
- * Listeners to be invoked by {@link #notifyItemChanged()}.
- */
- private final List<OnItemChangedListener> mOnItemChangedListeners = new ArrayList<>();
-
- /**
- * Designated constructor.
- *
- * @param item the {@link T} item to be held by this holder
- * @param itemId the globally unique id corresponding to the item
- */
- public ItemHolder(T item, long itemId) {
- this.item = item;
- this.itemId = itemId;
- }
-
- /**
- * @return the unique identifier for the view that should be used to represent the item,
- * e.g. the layout resource id.
- */
- public abstract int getItemViewType();
-
- /**
- * Adds the listener to the current list of registered listeners if it is not already
- * registered.
- *
- * @param listener the listener to add
- */
- public final void addOnItemChangedListener(OnItemChangedListener listener) {
- if (!mOnItemChangedListeners.contains(listener)) {
- mOnItemChangedListeners.add(listener);
- }
- }
-
- /**
- * Removes the listener from the current list of registered listeners.
- *
- * @param listener the listener to remove
- */
- public final void removeOnItemChangedListener(OnItemChangedListener listener) {
- mOnItemChangedListeners.remove(listener);
- }
-
- /**
- * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder)} for all listeners added
- * via {@link #addOnItemChangedListener(OnItemChangedListener)}.
- */
- public final void notifyItemChanged() {
- for (OnItemChangedListener listener : mOnItemChangedListeners) {
- listener.onItemChanged(this);
- }
- }
-
- /**
- * Invokes {@link OnItemChangedListener#onItemChanged(ItemHolder, Object)} for all
- * listeners added via {@link #addOnItemChangedListener(OnItemChangedListener)}.
- */
- public final void notifyItemChanged(Object payload) {
- for (OnItemChangedListener listener : mOnItemChangedListeners) {
- listener.onItemChanged(this, payload);
- }
- }
-
- /**
- * Called to retrieve per-instance state when the item may disappear or change so that
- * state can be restored in {@link #onRestoreInstanceState(Bundle)}.
- * <p/>
- * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
- * reused for other items in the {@link ItemHolder}.
- *
- * @param bundle the {@link Bundle} in which to place saved state
- */
- public void onSaveInstanceState(Bundle bundle) {
- // for subclassers
- }
-
- /**
- * Called to restore any per-instance state which was previously saved in
- * {@link #onSaveInstanceState(Bundle)} for an item with a matching {@link #itemId}.
- * <p/>
- * Note: Subclasses must not maintain a reference to the {@link Bundle} as it may be
- * reused for other items in the {@link ItemHolder}.
- *
- * @param bundle the {@link Bundle} in which to retrieve saved state
- */
- public void onRestoreInstanceState(Bundle bundle) {
- // for subclassers
- }
- }
-
- /**
- * Base class for a reusable {@link RecyclerView.ViewHolder} compatible with an
- * {@link ItemViewHolder}. Provides an interface for binding to an {@link ItemHolder} and later
- * being recycled.
- */
- public static class ItemViewHolder<T extends ItemHolder> extends RecyclerView.ViewHolder {
-
- /**
- * The current {@link ItemHolder} bound to this holder.
- */
- private T mItemHolder;
-
- /**
- * The current {@link OnItemClickedListener} associated with this holder.
- */
- private OnItemClickedListener mOnItemClickedListener;
-
- /**
- * Designated constructor.
- *
- * @param itemView the item {@link View} to associate with this holder
- */
- public ItemViewHolder(View itemView) {
- super(itemView);
- }
-
- /**
- * @return the current {@link ItemHolder} bound to this holder, or {@code null} if unbound
- */
- public final T getItemHolder() {
- return mItemHolder;
- }
-
- /**
- * Binds the holder's {@link #itemView} to a particular item.
- *
- * @param itemHolder the {@link ItemHolder} to bind
- */
- public final void bindItemView(T itemHolder) {
- mItemHolder = itemHolder;
- onBindItemView(itemHolder);
- }
-
- /**
- * Called when a new item is bound to the holder. Subclassers should override to bind any
- * relevant data to their {@link #itemView} in this method.
- *
- * @param itemHolder the {@link ItemHolder} to bind
- */
- protected void onBindItemView(T itemHolder) {
- // for subclassers
- }
-
- /**
- * Recycles the current item view, unbinding the current item holder and state.
- */
- public final void recycleItemView() {
- mItemHolder = null;
- mOnItemClickedListener = null;
-
- onRecycleItemView();
- }
-
- /**
- * Called when the current item view is recycled. Subclassers should override to release
- * any bound item state and prepare their {@link #itemView} for reuse.
- */
- protected void onRecycleItemView() {
- // for subclassers
- }
-
- /**
- * Sets the current {@link OnItemClickedListener} to be invoked via
- * {@link #notifyItemClicked}.
- *
- * @param listener the new {@link OnItemClickedListener}, or {@code null} to clear
- */
- public final void setOnItemClickedListener(OnItemClickedListener listener) {
- mOnItemClickedListener = listener;
- }
-
- /**
- * Called by subclasses to invoke the current {@link OnItemClickedListener} for a
- * particular click event so it can be handled at a higher level.
- *
- * @param id the unique identifier for the click action that has occurred
- */
- public final void notifyItemClicked(int id) {
- if (mOnItemClickedListener != null) {
- mOnItemClickedListener.onItemClicked(this, id);
- }
- }
-
- /**
- * Factory interface used by {@link ItemAdapter} for creating new {@link ItemViewHolder}.
- */
- public interface Factory {
- /**
- * Used by {@link ItemAdapter#createViewHolder(ViewGroup, int)} to make new
- * {@link ItemViewHolder} for a given view type.
- *
- * @param parent the {@code ViewGroup} that the {@link ItemViewHolder#itemView} will
- * be attached
- * @param viewType the unique id of the item view to create
- * @return a new initialized {@link ItemViewHolder}
- */
- public ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType);
- }
- }
-
- /**
- * Callback interface for when an item changes and should be re-bound.
- */
- public interface OnItemChangedListener {
- /**
- * Invoked by {@link ItemHolder#notifyItemChanged()}.
- *
- * @param itemHolder the item holder that has changed
- */
- void onItemChanged(ItemHolder<?> itemHolder);
-
-
- /**
- * Invoked by {@link ItemHolder#notifyItemChanged(Object payload)}.
- *
- * @param itemHolder the item holder that has changed
- * @param payload the payload object
- */
- void onItemChanged(ItemAdapter.ItemHolder<?> itemHolder, Object payload);
- }
-
- /**
- * Callback interface for handling when an item is clicked.
- */
- public interface OnItemClickedListener {
- /**
- * Invoked by {@link ItemViewHolder#notifyItemClicked(int)}
- *
- * @param viewHolder the {@link ItemViewHolder} containing the view that was clicked
- * @param id the unique identifier for the click action that has occurred
- */
- void onItemClicked(ItemViewHolder<?> viewHolder, int id);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAdapter.kt b/src/com/android/deskclock/ItemAdapter.kt
new file mode 100644
index 0000000..585ea45
--- /dev/null
+++ b/src/com/android/deskclock/ItemAdapter.kt
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.os.Bundle
+import android.util.SparseArray
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+
+import kotlin.math.min
+
+/**
+ * Base adapter class for displaying a collection of items. Provides functionality for handling
+ * changing items, persistent item state, item click events, and re-usable item views.
+ */
+class ItemAdapter<T : ItemHolder<*>> : RecyclerView.Adapter<ItemViewHolder<T>>() {
+ /**
+ * Finds the position of the changed item holder and invokes [.notifyItemChanged] or
+ * [.notifyItemChanged] if payloads are present (in order to do in-place
+ * change animations).
+ */
+ private val mItemChangedNotifier: OnItemChangedListener = object : OnItemChangedListener {
+ override fun onItemChanged(itemHolder: ItemHolder<*>) {
+ mOnItemChangedListener?.onItemChanged(itemHolder)
+ val position = items!!.indexOf(itemHolder)
+ if (position != RecyclerView.NO_POSITION) {
+ notifyItemChanged(position)
+ }
+ }
+
+ override fun onItemChanged(itemHolder: ItemHolder<*>, payload: Any) {
+ mOnItemChangedListener?.onItemChanged(itemHolder, payload)
+ val position = items!!.indexOf(itemHolder)
+ if (position != RecyclerView.NO_POSITION) {
+ notifyItemChanged(position, payload)
+ }
+ }
+ }
+
+ /**
+ * Invokes the [OnItemClickedListener] in [.mListenersByViewType] corresponding
+ * to [ItemViewHolder.getItemViewType]
+ */
+ private val mOnItemClickedListener: OnItemClickedListener = object : OnItemClickedListener {
+ override fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int) {
+ val listener = mListenersByViewType[viewHolder.getItemViewType()]
+ listener?.onItemClicked(viewHolder, id)
+ }
+ }
+
+ /**
+ * Invoked when any item changes.
+ */
+ private var mOnItemChangedListener: OnItemChangedListener? = null
+
+ /**
+ * Factories for creating new [ItemViewHolder] entities.
+ */
+ private val mFactoriesByViewType: SparseArray<ItemViewHolder.Factory> = SparseArray()
+
+ /**
+ * Listeners to invoke in [.mOnItemClickedListener].
+ */
+ private val mListenersByViewType: SparseArray<OnItemClickedListener?> = SparseArray()
+
+ /**
+ * List of current item holders represented by this adapter.
+ */
+ var items: MutableList<T>? = null
+ private set
+
+ /**
+ * Convenience for calling [.setHasStableIds] with `true`.
+ *
+ * @return this object, allowing calls to methods in this class to be chained
+ */
+ fun setHasStableIds(): ItemAdapter<T> {
+ setHasStableIds(true)
+ return this
+ }
+
+ /**
+ * Sets the [ItemViewHolder.Factory] and [OnItemClickedListener] used to create
+ * new item view holders in [.onCreateViewHolder].
+ *
+ * @param factory the [ItemViewHolder.Factory] used to create new item view holders
+ * @param listener the [OnItemClickedListener] to be invoked by [.mItemChangedNotifier]
+ * @param viewTypes the unique identifier for the view types to be created
+ * @return this object, allowing calls to methods in this class to be chained
+ */
+ fun withViewTypes(
+ factory: ItemViewHolder.Factory,
+ listener: OnItemClickedListener?,
+ vararg viewTypes: Int
+ ): ItemAdapter<T> {
+ for (viewType in viewTypes) {
+ mFactoriesByViewType.put(viewType, factory)
+ mListenersByViewType.put(viewType, listener)
+ }
+ return this
+ }
+
+ /**
+ * Sets the list of item holders to serve as the dataset for this adapter and invokes
+ * [.notifyDataSetChanged] to update the UI.
+ *
+ * If [.hasStableIds] returns `true`, then the instance state will preserved
+ * between new and old holders that have matching [itemId] values.
+ *
+ * @param itemHolders the new list of item holders
+ * @return this object, allowing calls to methods in this class to be chained
+ */
+ fun setItems(itemHolders: List<T>?): ItemAdapter<T> {
+ val oldItemHolders = items
+ if (oldItemHolders !== itemHolders) {
+ if (oldItemHolders != null) {
+ // remove the item change listener from the old item holders
+ for (oldItemHolder in oldItemHolders) {
+ oldItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
+ }
+ }
+
+ if (oldItemHolders != null && itemHolders != null && hasStableIds()) {
+ // transfer instance state from old to new item holders based on item id,
+ // we use a simple O(N^2) implementation since we assume the number of items is
+ // relatively small and generating a temporary map would be more expensive
+ val bundle = Bundle()
+ for (newItemHolder in itemHolders) {
+ for (oldItemHolder in oldItemHolders) {
+ if (newItemHolder.itemId == oldItemHolder.itemId &&
+ newItemHolder !== oldItemHolder) {
+ // clear any existing state from the bundle
+ bundle.clear()
+
+ // transfer instance state from old to new item holder
+ oldItemHolder.onSaveInstanceState(bundle)
+ newItemHolder.onRestoreInstanceState(bundle)
+ break
+ }
+ }
+ }
+ }
+
+ if (itemHolders != null) {
+ // add the item change listener to the new item holders
+ for (newItemHolder in itemHolders) {
+ newItemHolder.addOnItemChangedListener(mItemChangedNotifier)
+ }
+ }
+
+ // finally update the current list of item holders and inform the RV to update the UI
+ items = itemHolders?.toMutableList()
+ notifyDataSetChanged()
+ }
+
+ return this
+ }
+
+ /**
+ * Inserts the specified item holder at the specified position. Invokes
+ * [.notifyItemInserted] to update the UI.
+ *
+ * @param position the index to which to add the item holder
+ * @param itemHolder the item holder to add
+ * @return this object, allowing calls to methods in this class to be chained
+ */
+ fun addItem(position: Int, itemHolder: T): ItemAdapter<T> {
+ var variablePosition = position
+ itemHolder.addOnItemChangedListener(mItemChangedNotifier)
+ variablePosition = min(variablePosition, items!!.size)
+ items!!.add(variablePosition, itemHolder)
+ notifyItemInserted(variablePosition)
+ return this
+ }
+
+ /**
+ * Removes the first occurrence of the specified element from this list, if it is present
+ * (optional operation). If this list does not contain the element, it is unchanged. Invokes
+ * [.notifyItemRemoved] to update the UI.
+ *
+ * @param itemHolder the item holder to remove
+ * @return this object, allowing calls to methods in this class to be chained
+ */
+ fun removeItem(itemHolder: T): ItemAdapter<T> {
+ var variableItemHolder = itemHolder
+ val index = items!!.indexOf(variableItemHolder)
+ if (index >= 0) {
+ variableItemHolder = items!!.removeAt(index)
+ variableItemHolder.removeOnItemChangedListener(mItemChangedNotifier)
+ notifyItemRemoved(index)
+ }
+ return this
+ }
+
+ /**
+ * Sets the listener to be invoked whenever any item changes.
+ */
+ fun setOnItemChangedListener(listener: OnItemChangedListener) {
+ mOnItemChangedListener = listener
+ }
+
+ override fun getItemCount(): Int = items?.size ?: 0
+
+ override fun getItemId(position: Int): Long {
+ return if (hasStableIds()) items!![position].itemId else NO_ID
+ }
+
+ fun findItemById(id: Long): T? {
+ for (holder in items!!) {
+ if (holder.itemId == id) {
+ return holder
+ }
+ }
+ return null
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return items!![position].getItemViewType()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<T> {
+ val factory = mFactoriesByViewType[viewType]
+ if (factory != null) {
+ return factory.createViewHolder(parent, viewType) as ItemViewHolder<T>
+ }
+ throw IllegalArgumentException("Unsupported view type: $viewType")
+ }
+
+ override fun onBindViewHolder(viewHolder: ItemViewHolder<T>, position: Int) {
+ // suppress any unchecked warnings since it is up to the subclass to guarantee
+ // compatibility of their view holders with the item holder at the corresponding position
+ viewHolder.bindItemView(items!![position])
+ viewHolder.setOnItemClickedListener(mOnItemClickedListener)
+ }
+
+ override fun onViewRecycled(viewHolder: ItemViewHolder<T>) {
+ viewHolder.setOnItemClickedListener(null)
+ viewHolder.recycleItemView()
+ }
+
+ /**
+ * Base class for wrapping an item for compatibility with an [ItemHolder].
+ *
+ * An [ItemHolder] serves as bridge between the model and view layer; subclassers should
+ * implement properties that fall beyond the scope of their model layer but are necessary for
+ * the view layer. Properties that should be persisted across dataset changes can be
+ * preserved via the [.onSaveInstanceState] and
+ * [.onRestoreInstanceState] methods.
+ *
+ * Note: An [ItemHolder] can be used by multiple [ItemHolder] and any state changes
+ * should simultaneously be reflected in both UIs. It is not thread-safe however and should
+ * only be used on a single thread at a given time.
+ *
+ * @param <T> the item type wrapped by the holder
+ </T> */
+ abstract class ItemHolder<T>(
+ /** The item held by this holder. */
+ val item: T,
+ /** Globally unique id corresponding to the item. */
+ val itemId: Long
+ ) {
+ /** Listeners to be invoked by [.notifyItemChanged]. */
+ private val mOnItemChangedListeners: MutableList<OnItemChangedListener> = ArrayList()
+
+ /**
+ * @return the unique identifier for the view that should be used to represent the item,
+ * e.g. the layout resource id.
+ */
+ abstract fun getItemViewType(): Int
+
+ /**
+ * Adds the listener to the current list of registered listeners if it is not already
+ * registered.
+ *
+ * @param listener the listener to add
+ */
+ fun addOnItemChangedListener(listener: OnItemChangedListener) {
+ if (!mOnItemChangedListeners.contains(listener)) {
+ mOnItemChangedListeners.add(listener)
+ }
+ }
+
+ /**
+ * Removes the listener from the current list of registered listeners.
+ *
+ * @param listener the listener to remove
+ */
+ fun removeOnItemChangedListener(listener: OnItemChangedListener) {
+ mOnItemChangedListeners.remove(listener)
+ }
+
+ /**
+ * Invokes [OnItemChangedListener.onItemChanged] for all listeners added
+ * via [.addOnItemChangedListener].
+ */
+ fun notifyItemChanged() {
+ for (listener in mOnItemChangedListeners) {
+ listener.onItemChanged(this)
+ }
+ }
+
+ /**
+ * Invokes [OnItemChangedListener.onItemChanged] for all
+ * listeners added via [.addOnItemChangedListener].
+ */
+ fun notifyItemChanged(payload: Any) {
+ for (listener in mOnItemChangedListeners) {
+ listener.onItemChanged(this, payload)
+ }
+ }
+
+ /**
+ * Called to retrieve per-instance state when the item may disappear or change so that
+ * state can be restored in [.onRestoreInstanceState].
+ *
+ * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
+ * reused for other items in the [ItemHolder].
+ *
+ * @param bundle the [Bundle] in which to place saved state
+ */
+ open fun onSaveInstanceState(bundle: Bundle) {
+ // for subclassers
+ }
+
+ /**
+ * Called to restore any per-instance state which was previously saved in
+ * [.onSaveInstanceState] for an item with a matching [.itemId].
+ *
+ * Note: Subclasses must not maintain a reference to the [Bundle] as it may be
+ * reused for other items in the [ItemHolder].
+ *
+ * @param bundle the [Bundle] in which to retrieve saved state
+ */
+ open fun onRestoreInstanceState(bundle: Bundle) {
+ // for subclassers
+ }
+ }
+
+ /**
+ * Base class for a reusable [RecyclerView.ViewHolder] compatible with an
+ * [ItemViewHolder]. Provides an interface for binding to an [ItemHolder] and later
+ * being recycled.
+ */
+ open class ItemViewHolder<T : ItemHolder<*>>(itemView: View)
+ : RecyclerView.ViewHolder(itemView) {
+ /**
+ * The current [ItemHolder] bound to this holder, or `null` if unbound.
+ */
+ var itemHolder: T? = null
+ private set
+
+ /**
+ * The current [OnItemClickedListener] associated with this holder.
+ */
+ private var mOnItemClickedListener: OnItemClickedListener? = null
+
+ /**
+ * Binds the holder's [.itemView] to a particular item.
+ *
+ * @param itemHolder the [ItemHolder] to bind
+ */
+ fun bindItemView(itemHolder: T) {
+ this.itemHolder = itemHolder
+ onBindItemView(itemHolder)
+ }
+
+ /**
+ * Called when a new item is bound to the holder. Subclassers should override to bind any
+ * relevant data to their [.itemView] in this method.
+ *
+ * @param itemHolder the [ItemHolder] to bind
+ */
+ protected open fun onBindItemView(itemHolder: T) {
+ // for subclassers
+ }
+
+ /**
+ * Recycles the current item view, unbinding the current item holder and state.
+ */
+ fun recycleItemView() {
+ itemHolder = null
+ mOnItemClickedListener = null
+
+ onRecycleItemView()
+ }
+
+ /**
+ * Called when the current item view is recycled. Subclassers should override to release
+ * any bound item state and prepare their [.itemView] for reuse.
+ */
+ protected fun onRecycleItemView() {
+ // for subclassers
+ }
+
+ /**
+ * Sets the current [OnItemClickedListener] to be invoked via
+ * [.notifyItemClicked].
+ *
+ * @param listener the new [OnItemClickedListener], or `null` to clear
+ */
+ fun setOnItemClickedListener(listener: OnItemClickedListener?) {
+ mOnItemClickedListener = listener
+ }
+
+ /**
+ * Called by subclasses to invoke the current [OnItemClickedListener] for a
+ * particular click event so it can be handled at a higher level.
+ *
+ * @param id the unique identifier for the click action that has occurred
+ */
+ fun notifyItemClicked(id: Int) {
+ mOnItemClickedListener?.onItemClicked(this, id)
+ }
+
+ /**
+ * Factory interface used by [ItemAdapter] for creating new [ItemViewHolder].
+ */
+ interface Factory {
+ /**
+ * Used by [ItemAdapter.createViewHolder] to make new
+ * [ItemViewHolder] for a given view type.
+ *
+ * @param parent the `ViewGroup` that the [ItemViewHolder.itemView] will be attached
+ * @param viewType the unique id of the item view to create
+ * @return a new initialized [ItemViewHolder]
+ */
+ fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*>
+ }
+ }
+
+ /**
+ * Callback interface for when an item changes and should be re-bound.
+ */
+ interface OnItemChangedListener {
+ /**
+ * Invoked by [ItemHolder.notifyItemChanged].
+ *
+ * @param itemHolder the item holder that has changed
+ */
+ fun onItemChanged(itemHolder: ItemHolder<*>)
+
+ /**
+ * Invoked by [ItemHolder.notifyItemChanged].
+ *
+ * @param itemHolder the item holder that has changed
+ * @param payload the payload object
+ */
+ fun onItemChanged(itemHolder: ItemHolder<*>, payload: Any)
+ }
+
+ /**
+ * Callback interface for handling when an item is clicked.
+ */
+ interface OnItemClickedListener {
+ /**
+ * Invoked by [ItemViewHolder.notifyItemClicked]
+ *
+ * @param viewHolder the [ItemViewHolder] containing the view that was clicked
+ * @param id the unique identifier for the click action that has occurred
+ */
+ fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAnimator.java b/src/com/android/deskclock/ItemAnimator.java
deleted file mode 100644
index 756d4e9..0000000
--- a/src/com/android/deskclock/ItemAnimator.java
+++ /dev/null
@@ -1,371 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import androidx.annotation.NonNull;
-import androidx.collection.ArrayMap;
-import androidx.recyclerview.widget.RecyclerView.State;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-import androidx.recyclerview.widget.SimpleItemAnimator;
-import android.view.View;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import static android.view.View.TRANSLATION_Y;
-import static android.view.View.TRANSLATION_X;
-
-public class ItemAnimator extends SimpleItemAnimator {
-
- private final List<Animator> mAddAnimatorsList = new ArrayList<>();
- private final List<Animator> mRemoveAnimatorsList = new ArrayList<>();
- private final List<Animator> mChangeAnimatorsList = new ArrayList<>();
- private final List<Animator> mMoveAnimatorsList = new ArrayList<>();
-
- private final Map<ViewHolder, Animator> mAnimators = new ArrayMap<>();
-
- @Override
- public boolean animateRemove(final ViewHolder holder) {
- endAnimation(holder);
-
- final float prevAlpha = holder.itemView.getAlpha();
-
- final Animator removeAnimator = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 0f);
- removeAnimator.setDuration(getRemoveDuration());
- removeAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- dispatchRemoveStarting(holder);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- animator.removeAllListeners();
- mAnimators.remove(holder);
- holder.itemView.setAlpha(prevAlpha);
- dispatchRemoveFinished(holder);
- }
- });
- mRemoveAnimatorsList.add(removeAnimator);
- mAnimators.put(holder, removeAnimator);
- return true;
- }
-
- @Override
- public boolean animateAdd(final ViewHolder holder) {
- endAnimation(holder);
-
- final float prevAlpha = holder.itemView.getAlpha();
- holder.itemView.setAlpha(0f);
-
- final Animator addAnimator = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 1f)
- .setDuration(getAddDuration());
- addAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- dispatchAddStarting(holder);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- animator.removeAllListeners();
- mAnimators.remove(holder);
- holder.itemView.setAlpha(prevAlpha);
- dispatchAddFinished(holder);
- }
- });
- mAddAnimatorsList.add(addAnimator);
- mAnimators.put(holder, addAnimator);
- return true;
- }
-
- @Override
- public boolean animateMove(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
- endAnimation(holder);
-
- final int deltaX = toX - fromX;
- final int deltaY = toY - fromY;
- final long moveDuration = getMoveDuration();
-
- if (deltaX == 0 && deltaY == 0) {
- dispatchMoveFinished(holder);
- return false;
- }
-
- final View view = holder.itemView;
- final float prevTranslationX = view.getTranslationX();
- final float prevTranslationY = view.getTranslationY();
- view.setTranslationX(-deltaX);
- view.setTranslationY(-deltaY);
-
- final ObjectAnimator moveAnimator;
- if (deltaX != 0 && deltaY != 0) {
- final PropertyValuesHolder moveX = PropertyValuesHolder.ofFloat(TRANSLATION_X, 0f);
- final PropertyValuesHolder moveY = PropertyValuesHolder.ofFloat(TRANSLATION_Y, 0f);
- moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX, moveY);
- } else if (deltaX != 0) {
- final PropertyValuesHolder moveX = PropertyValuesHolder.ofFloat(TRANSLATION_X, 0f);
- moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX);
- } else {
- final PropertyValuesHolder moveY = PropertyValuesHolder.ofFloat(TRANSLATION_Y, 0f);
- moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveY);
- }
-
- moveAnimator.setDuration(moveDuration);
- moveAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
- moveAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- dispatchMoveStarting(holder);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- animator.removeAllListeners();
- mAnimators.remove(holder);
- view.setTranslationX(prevTranslationX);
- view.setTranslationY(prevTranslationY);
- dispatchMoveFinished(holder);
- }
- });
- mMoveAnimatorsList.add(moveAnimator);
- mAnimators.put(holder, moveAnimator);
-
- return true;
- }
-
- @Override
- public boolean animateChange(@NonNull final ViewHolder oldHolder,
- @NonNull final ViewHolder newHolder, @NonNull ItemHolderInfo preInfo,
- @NonNull ItemHolderInfo postInfo) {
- endAnimation(oldHolder);
- endAnimation(newHolder);
-
- final long changeDuration = getChangeDuration();
- List<Object> payloads = preInfo instanceof PayloadItemHolderInfo
- ? ((PayloadItemHolderInfo) preInfo).getPayloads() : null;
-
- if (oldHolder == newHolder) {
- final Animator animator = ((OnAnimateChangeListener) newHolder)
- .onAnimateChange(payloads, preInfo.left, preInfo.top, preInfo.right,
- preInfo.bottom, changeDuration);
- if (animator == null) {
- dispatchChangeFinished(newHolder, false);
- return false;
- }
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- dispatchChangeStarting(newHolder, false);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- animator.removeAllListeners();
- mAnimators.remove(newHolder);
- dispatchChangeFinished(newHolder, false);
- }
- });
- mChangeAnimatorsList.add(animator);
- mAnimators.put(newHolder, animator);
- return true;
- } else if (!(oldHolder instanceof OnAnimateChangeListener) ||
- !(newHolder instanceof OnAnimateChangeListener)) {
- // Both holders must implement OnAnimateChangeListener in order to animate.
- dispatchChangeFinished(oldHolder, true);
- dispatchChangeFinished(newHolder, true);
- return false;
- }
-
- final Animator oldChangeAnimator = ((OnAnimateChangeListener) oldHolder)
- .onAnimateChange(oldHolder, newHolder, changeDuration);
- if (oldChangeAnimator != null) {
- oldChangeAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- dispatchChangeStarting(oldHolder, true);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- animator.removeAllListeners();
- mAnimators.remove(oldHolder);
- dispatchChangeFinished(oldHolder, true);
- }
- });
- mAnimators.put(oldHolder, oldChangeAnimator);
- mChangeAnimatorsList.add(oldChangeAnimator);
- } else {
- dispatchChangeFinished(oldHolder, true);
- }
-
- final Animator newChangeAnimator = ((OnAnimateChangeListener) newHolder)
- .onAnimateChange(oldHolder, newHolder, changeDuration);
- if (newChangeAnimator != null) {
- newChangeAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- dispatchChangeStarting(newHolder, false);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- animator.removeAllListeners();
- mAnimators.remove(newHolder);
- dispatchChangeFinished(newHolder, false);
- }
- });
- mAnimators.put(newHolder, newChangeAnimator);
- mChangeAnimatorsList.add(newChangeAnimator);
- } else {
- dispatchChangeFinished(newHolder, false);
- }
-
- return true;
- }
-
- @Override
- public boolean animateChange(ViewHolder oldHolder,
- ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
- /* Unused */
- throw new IllegalStateException("This method should not be used");
- }
-
- @Override
- public void runPendingAnimations() {
- final AnimatorSet removeAnimatorSet = new AnimatorSet();
- removeAnimatorSet.playTogether(mRemoveAnimatorsList);
- mRemoveAnimatorsList.clear();
-
- final AnimatorSet addAnimatorSet = new AnimatorSet();
- addAnimatorSet.playTogether(mAddAnimatorsList);
- mAddAnimatorsList.clear();
-
- final AnimatorSet changeAnimatorSet = new AnimatorSet();
- changeAnimatorSet.playTogether(mChangeAnimatorsList);
- mChangeAnimatorsList.clear();
-
- final AnimatorSet moveAnimatorSet = new AnimatorSet();
- moveAnimatorSet.playTogether(mMoveAnimatorsList);
- mMoveAnimatorsList.clear();
-
- final AnimatorSet pendingAnimatorSet = new AnimatorSet();
- pendingAnimatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- animator.removeAllListeners();
- dispatchFinishedWhenDone();
- }
- });
- // Required order: removes, then changes & moves simultaneously, then additions. There are
- // redundant edges because changes or moves may be empty, causing the removes to incorrectly
- // play immediately.
- pendingAnimatorSet.play(removeAnimatorSet).before(changeAnimatorSet);
- pendingAnimatorSet.play(removeAnimatorSet).before(moveAnimatorSet);
- pendingAnimatorSet.play(changeAnimatorSet).with(moveAnimatorSet);
- pendingAnimatorSet.play(addAnimatorSet).after(changeAnimatorSet);
- pendingAnimatorSet.play(addAnimatorSet).after(moveAnimatorSet);
- pendingAnimatorSet.start();
- }
-
- @Override
- public void endAnimation(ViewHolder holder) {
- final Animator animator = mAnimators.get(holder);
-
- mAnimators.remove(holder);
- mAddAnimatorsList.remove(animator);
- mRemoveAnimatorsList.remove(animator);
- mChangeAnimatorsList.remove(animator);
- mMoveAnimatorsList.remove(animator);
-
- if (animator != null) {
- animator.end();
- }
-
- dispatchFinishedWhenDone();
- }
-
- @Override
- public void endAnimations() {
- final List<Animator> animatorList = new ArrayList<>(mAnimators.values());
- for (Animator animator : animatorList) {
- animator.end();
- }
- dispatchFinishedWhenDone();
- }
-
- @Override
- public boolean isRunning() {
- return !mAnimators.isEmpty();
- }
-
- private void dispatchFinishedWhenDone() {
- if (!isRunning()) {
- dispatchAnimationsFinished();
- }
- }
-
- @Override
- public @NonNull ItemHolderInfo recordPreLayoutInformation(@NonNull State state,
- @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags,
- @NonNull List<Object> payloads) {
- final ItemHolderInfo itemHolderInfo = super.recordPreLayoutInformation(state, viewHolder,
- changeFlags, payloads);
- if (itemHolderInfo instanceof PayloadItemHolderInfo) {
- ((PayloadItemHolderInfo) itemHolderInfo).setPayloads(payloads);
- }
- return itemHolderInfo;
- }
-
- @Override
- public ItemHolderInfo obtainHolderInfo() {
- return new PayloadItemHolderInfo();
- }
-
- @Override
- public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
- @NonNull List<Object> payloads) {
- final boolean defaultReusePolicy = super.canReuseUpdatedViewHolder(viewHolder, payloads);
- // Whenever we have a payload, this is an in-place animation.
- return !payloads.isEmpty() || defaultReusePolicy;
- }
-
- private static final class PayloadItemHolderInfo extends ItemHolderInfo {
- private final List<Object> mPayloads = new ArrayList<>();
-
- void setPayloads(List<Object> payloads) {
- mPayloads.clear();
- mPayloads.addAll(payloads);
- }
-
- List<Object> getPayloads() {
- return mPayloads;
- }
- }
-
- public interface OnAnimateChangeListener {
- Animator onAnimateChange(ViewHolder oldHolder, ViewHolder newHolder, long duration);
- Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
- int fromBottom, long duration);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ItemAnimator.kt b/src/com/android/deskclock/ItemAnimator.kt
new file mode 100644
index 0000000..a1791b5
--- /dev/null
+++ b/src/com/android/deskclock/ItemAnimator.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.view.View
+import androidx.collection.ArrayMap
+import androidx.recyclerview.widget.RecyclerView.State
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.recyclerview.widget.RecyclerView.ItemAnimator
+import androidx.recyclerview.widget.SimpleItemAnimator
+
+class ItemAnimator : SimpleItemAnimator() {
+ private val mAddAnimatorsList: MutableList<Animator> = ArrayList()
+ private val mRemoveAnimatorsList: MutableList<Animator> = ArrayList()
+ private val mChangeAnimatorsList: MutableList<Animator> = ArrayList()
+ private val mMoveAnimatorsList: MutableList<Animator> = ArrayList()
+
+ private val mAnimators: MutableMap<ViewHolder, Animator> = ArrayMap()
+
+ override fun animateRemove(holder: ViewHolder): Boolean {
+ endAnimation(holder)
+
+ val prevAlpha: Float = holder.itemView.getAlpha()
+
+ val removeAnimator: Animator? = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 0f)
+ removeAnimator!!.duration = getRemoveDuration()
+ removeAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ dispatchRemoveStarting(holder)
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ animator.removeAllListeners()
+ mAnimators.remove(holder)
+ holder.itemView.setAlpha(prevAlpha)
+ dispatchRemoveFinished(holder)
+ }
+ })
+ mRemoveAnimatorsList.add(removeAnimator)
+ mAnimators[holder] = removeAnimator
+ return true
+ }
+
+ override fun animateAdd(holder: ViewHolder): Boolean {
+ endAnimation(holder)
+
+ val prevAlpha: Float = holder.itemView.getAlpha()
+ holder.itemView.setAlpha(0f)
+
+ val addAnimator: Animator = ObjectAnimator.ofFloat(holder.itemView, View.ALPHA, 1f)
+ .setDuration(getAddDuration())
+ addAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ dispatchAddStarting(holder)
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ animator.removeAllListeners()
+ mAnimators.remove(holder)
+ holder.itemView.setAlpha(prevAlpha)
+ dispatchAddFinished(holder)
+ }
+ })
+ mAddAnimatorsList.add(addAnimator)
+ mAnimators[holder] = addAnimator
+ return true
+ }
+
+ override fun animateMove(
+ holder: ViewHolder,
+ fromX: Int,
+ fromY: Int,
+ toX: Int,
+ toY: Int
+ ): Boolean {
+ endAnimation(holder)
+
+ val deltaX = toX - fromX
+ val deltaY = toY - fromY
+ val moveDuration: Long = getMoveDuration()
+
+ if (deltaX == 0 && deltaY == 0) {
+ dispatchMoveFinished(holder)
+ return false
+ }
+
+ val view: View = holder.itemView
+ val prevTranslationX = view.translationX
+ val prevTranslationY = view.translationY
+ view.translationX = -deltaX.toFloat()
+ view.translationY = -deltaY.toFloat()
+
+ val moveAnimator: ObjectAnimator?
+ if (deltaX != 0 && deltaY != 0) {
+ val moveX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0f)
+ val moveY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f)
+ moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX, moveY)
+ } else if (deltaX != 0) {
+ val moveX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0f)
+ moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveX)
+ } else {
+ val moveY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f)
+ moveAnimator = ObjectAnimator.ofPropertyValuesHolder(holder.itemView, moveY)
+ }
+
+ moveAnimator?.duration = moveDuration
+ moveAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+ moveAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator?) {
+ dispatchMoveStarting(holder)
+ }
+
+ override fun onAnimationEnd(animator: Animator?) {
+ animator?.removeAllListeners()
+ mAnimators.remove(holder)
+ view.translationX = prevTranslationX
+ view.translationY = prevTranslationY
+ dispatchMoveFinished(holder)
+ }
+ })
+ mMoveAnimatorsList.add(moveAnimator)
+ mAnimators[holder] = moveAnimator
+
+ return true
+ }
+
+ override fun animateChange(
+ oldHolder: ViewHolder,
+ newHolder: ViewHolder,
+ preInfo: ItemHolderInfo,
+ postInfo: ItemHolderInfo
+ ): Boolean {
+ endAnimation(oldHolder)
+ endAnimation(newHolder)
+
+ val changeDuration: Long = getChangeDuration()
+ val payloads = if (preInfo is PayloadItemHolderInfo) preInfo.payloads else null
+
+ if (oldHolder === newHolder) {
+ val animator = (newHolder as OnAnimateChangeListener)
+ .onAnimateChange(payloads, preInfo.left, preInfo.top, preInfo.right,
+ preInfo.bottom, changeDuration)
+ if (animator == null) {
+ dispatchChangeFinished(newHolder, false)
+ return false
+ }
+ animator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ dispatchChangeStarting(newHolder, false)
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ animator.removeAllListeners()
+ mAnimators.remove(newHolder)
+ dispatchChangeFinished(newHolder, false)
+ }
+ })
+ mChangeAnimatorsList.add(animator)
+ mAnimators[newHolder] = animator
+ return true
+ } else if (oldHolder !is OnAnimateChangeListener ||
+ newHolder !is OnAnimateChangeListener) {
+ // Both holders must implement OnAnimateChangeListener in order to animate.
+ dispatchChangeFinished(oldHolder, true)
+ dispatchChangeFinished(newHolder, true)
+ return false
+ }
+
+ val oldChangeAnimator = (oldHolder as OnAnimateChangeListener)
+ .onAnimateChange(oldHolder, newHolder, changeDuration)
+ if (oldChangeAnimator != null) {
+ oldChangeAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ dispatchChangeStarting(oldHolder, true)
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ animator.removeAllListeners()
+ mAnimators.remove(oldHolder)
+ dispatchChangeFinished(oldHolder, true)
+ }
+ })
+ mAnimators[oldHolder] = oldChangeAnimator
+ mChangeAnimatorsList.add(oldChangeAnimator)
+ } else {
+ dispatchChangeFinished(oldHolder, true)
+ }
+
+ val newChangeAnimator = (newHolder as OnAnimateChangeListener)
+ .onAnimateChange(oldHolder, newHolder, changeDuration)
+ if (newChangeAnimator != null) {
+ newChangeAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ dispatchChangeStarting(newHolder, false)
+ }
+
+ override fun onAnimationEnd(animator: Animator) {
+ animator.removeAllListeners()
+ mAnimators.remove(newHolder)
+ dispatchChangeFinished(newHolder, false)
+ }
+ })
+ mAnimators[newHolder] = newChangeAnimator
+ mChangeAnimatorsList.add(newChangeAnimator)
+ } else {
+ dispatchChangeFinished(newHolder, false)
+ }
+
+ return true
+ }
+
+ override fun animateChange(
+ oldHolder: ViewHolder,
+ newHolder: ViewHolder,
+ fromLeft: Int,
+ fromTop: Int,
+ toLeft: Int,
+ toTop: Int
+ ): Boolean {
+ /* Unused */
+ throw IllegalStateException("This method should not be used")
+ }
+
+ override fun runPendingAnimations() {
+ val removeAnimatorSet = AnimatorSet()
+ removeAnimatorSet.playTogether(mRemoveAnimatorsList)
+ mRemoveAnimatorsList.clear()
+
+ val addAnimatorSet = AnimatorSet()
+ addAnimatorSet.playTogether(mAddAnimatorsList)
+ mAddAnimatorsList.clear()
+
+ val changeAnimatorSet = AnimatorSet()
+ changeAnimatorSet.playTogether(mChangeAnimatorsList)
+ mChangeAnimatorsList.clear()
+
+ val moveAnimatorSet = AnimatorSet()
+ moveAnimatorSet.playTogether(mMoveAnimatorsList)
+ mMoveAnimatorsList.clear()
+
+ val pendingAnimatorSet = AnimatorSet()
+ pendingAnimatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator) {
+ animator.removeAllListeners()
+ dispatchFinishedWhenDone()
+ }
+ })
+ // Required order: removes, then changes & moves simultaneously, then additions. There are
+ // redundant edges because changes or moves may be empty, causing the removes to incorrectly
+ // play immediately.
+ pendingAnimatorSet.play(removeAnimatorSet).before(changeAnimatorSet)
+ pendingAnimatorSet.play(removeAnimatorSet).before(moveAnimatorSet)
+ pendingAnimatorSet.play(changeAnimatorSet).with(moveAnimatorSet)
+ pendingAnimatorSet.play(addAnimatorSet).after(changeAnimatorSet)
+ pendingAnimatorSet.play(addAnimatorSet).after(moveAnimatorSet)
+ pendingAnimatorSet.start()
+ }
+
+ override fun endAnimation(holder: ViewHolder) {
+ val animator = mAnimators[holder]
+
+ mAnimators.remove(holder)
+ mAddAnimatorsList.remove(animator)
+ mRemoveAnimatorsList.remove(animator)
+ mChangeAnimatorsList.remove(animator)
+ mMoveAnimatorsList.remove(animator)
+
+ animator?.end()
+ dispatchFinishedWhenDone()
+ }
+
+ override fun endAnimations() {
+ val animatorList: MutableList<Animator?> = ArrayList(mAnimators.values)
+ for (animator in animatorList) {
+ animator?.end()
+ }
+ dispatchFinishedWhenDone()
+ }
+
+ override fun isRunning(): Boolean = mAnimators.isNotEmpty()
+
+ private fun dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished()
+ }
+ }
+
+ override fun recordPreLayoutInformation(
+ state: State,
+ viewHolder: ViewHolder,
+ @AdapterChanges changeFlags: Int,
+ payloads: MutableList<Any>
+ ): ItemAnimator.ItemHolderInfo {
+ val itemHolderInfo: ItemHolderInfo =
+ super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads)
+ if (itemHolderInfo is PayloadItemHolderInfo) {
+ itemHolderInfo.payloads = payloads
+ }
+ return itemHolderInfo
+ }
+
+ override fun obtainHolderInfo(): ItemAnimator.ItemHolderInfo {
+ return PayloadItemHolderInfo()
+ }
+
+ override fun canReuseUpdatedViewHolder(
+ viewHolder: ViewHolder,
+ payloads: MutableList<Any?>
+ ): Boolean {
+ val defaultReusePolicy: Boolean = super.canReuseUpdatedViewHolder(viewHolder, payloads)
+ // Whenever we have a payload, this is an in-place animation.
+ return payloads.isNotEmpty() || defaultReusePolicy
+ }
+
+ private class PayloadItemHolderInfo : ItemHolderInfo() {
+ private val mPayloads: MutableList<Any> = ArrayList()
+
+ var payloads: MutableList<Any>
+ get() = mPayloads
+ set(payloads) {
+ mPayloads.clear()
+ mPayloads.addAll(payloads)
+ }
+ }
+
+ interface OnAnimateChangeListener {
+ fun onAnimateChange(oldHolder: ViewHolder, newHolder: ViewHolder, duration: Long): Animator?
+
+ fun onAnimateChange(
+ payloads: List<Any>?,
+ fromLeft: Int,
+ fromTop: Int,
+ fromRight: Int,
+ fromBottom: Int,
+ duration: Long
+ ): Animator?
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/LabelDialogFragment.java b/src/com/android/deskclock/LabelDialogFragment.java
deleted file mode 100644
index 0fa0eab..0000000
--- a/src/com/android/deskclock/LabelDialogFragment.java
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.deskclock;
-
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.AppCompatEditText;
-import android.text.Editable;
-import android.text.InputType;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.view.KeyEvent;
-import android.view.Window;
-import android.view.inputmethod.EditorInfo;
-import android.widget.TextView;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.provider.Alarm;
-
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
-
-/**
- * DialogFragment to edit label.
- */
-public class LabelDialogFragment extends DialogFragment {
-
- /**
- * The tag that identifies instances of LabelDialogFragment in the fragment manager.
- */
- private static final String TAG = "label_dialog";
-
- private static final String ARG_LABEL = "arg_label";
- private static final String ARG_ALARM = "arg_alarm";
- private static final String ARG_TIMER_ID = "arg_timer_id";
- private static final String ARG_TAG = "arg_tag";
-
- private AppCompatEditText mLabelBox;
- private Alarm mAlarm;
- private int mTimerId;
- private String mTag;
-
- public static LabelDialogFragment newInstance(Alarm alarm, String label, String tag) {
- final Bundle args = new Bundle();
- args.putString(ARG_LABEL, label);
- args.putParcelable(ARG_ALARM, alarm);
- args.putString(ARG_TAG, tag);
-
- final LabelDialogFragment frag = new LabelDialogFragment();
- frag.setArguments(args);
- return frag;
- }
-
- public static LabelDialogFragment newInstance(Timer timer) {
- final Bundle args = new Bundle();
- args.putString(ARG_LABEL, timer.getLabel());
- args.putInt(ARG_TIMER_ID, timer.getId());
-
- final LabelDialogFragment frag = new LabelDialogFragment();
- frag.setArguments(args);
- return frag;
- }
-
- /**
- * Replaces any existing LabelDialogFragment with the given {@code fragment}.
- */
- public static void show(FragmentManager manager, LabelDialogFragment fragment) {
- if (manager == null || manager.isDestroyed()) {
- return;
- }
-
- // Finish any outstanding fragment work.
- manager.executePendingTransactions();
-
- final FragmentTransaction tx = manager.beginTransaction();
-
- // Remove existing instance of LabelDialogFragment if necessary.
- final Fragment existing = manager.findFragmentByTag(TAG);
- if (existing != null) {
- tx.remove(existing);
- }
- tx.addToBackStack(null);
-
- fragment.show(tx, TAG);
- }
-
- @Override
- public void onSaveInstanceState(@NonNull Bundle outState) {
- super.onSaveInstanceState(outState);
- // As long as the label box exists, save its state.
- if (mLabelBox != null) {
- outState.putString(ARG_LABEL, mLabelBox.getText().toString());
- }
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final Bundle args = getArguments() == null ? Bundle.EMPTY : getArguments();
- mAlarm = args.getParcelable(ARG_ALARM);
- mTimerId = args.getInt(ARG_TIMER_ID, -1);
- mTag = args.getString(ARG_TAG);
-
- String label = args.getString(ARG_LABEL);
- if (savedInstanceState != null) {
- label = savedInstanceState.getString(ARG_LABEL, label);
- }
-
- final AlertDialog dialog = new AlertDialog.Builder(getActivity())
- .setPositiveButton(android.R.string.ok, new OkListener())
- .setNegativeButton(android.R.string.cancel, null /* listener */)
- .setMessage(R.string.label)
- .create();
- final Context context = dialog.getContext();
-
- final int colorControlActivated =
- ThemeUtils.resolveColor(context, R.attr.colorControlActivated);
- final int colorControlNormal =
- ThemeUtils.resolveColor(context, R.attr.colorControlNormal);
-
- mLabelBox = new AppCompatEditText(context);
- mLabelBox.setSupportBackgroundTintList(new ColorStateList(
- new int[][] { { android.R.attr.state_activated }, {} },
- new int[] { colorControlActivated, colorControlNormal }));
- mLabelBox.setOnEditorActionListener(new ImeDoneListener());
- mLabelBox.addTextChangedListener(new TextChangeListener());
- mLabelBox.setSingleLine();
- mLabelBox.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
- mLabelBox.setText(label);
- mLabelBox.selectAll();
-
- // The line at the bottom of EditText is part of its background therefore the padding
- // must be added to its container.
- final int padding = context.getResources()
- .getDimensionPixelSize(R.dimen.label_edittext_padding);
- dialog.setView(mLabelBox, padding, 0, padding, 0);
-
- final Window alertDialogWindow = dialog.getWindow();
- if (alertDialogWindow != null) {
- alertDialogWindow.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE);
- }
- return dialog;
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- // Stop callbacks from the IME since there is no view to process them.
- mLabelBox.setOnEditorActionListener(null);
- }
-
- /**
- * Sets the new label into the timer or alarm.
- */
- private void setLabel() {
- String label = mLabelBox.getText().toString();
- if (label.trim().isEmpty()) {
- // Don't allow user to input label with only whitespace.
- label = "";
- }
-
- if (mAlarm != null) {
- ((AlarmLabelDialogHandler) getActivity()).onDialogLabelSet(mAlarm, label, mTag);
- } else if (mTimerId >= 0) {
- final Timer timer = DataModel.getDataModel().getTimer(mTimerId);
- if (timer != null) {
- DataModel.getDataModel().setTimerLabel(timer, label);
- }
- }
- }
-
- public interface AlarmLabelDialogHandler {
- void onDialogLabelSet(Alarm alarm, String label, String tag);
- }
-
- /**
- * Alters the UI to indicate when input is valid or invalid.
- */
- private class TextChangeListener implements TextWatcher {
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- mLabelBox.setActivated(!TextUtils.isEmpty(s));
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void afterTextChanged(Editable editable) {
- }
- }
-
- /**
- * Handles completing the label edit from the IME keyboard.
- */
- private class ImeDoneListener implements TextView.OnEditorActionListener {
- @Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if (actionId == EditorInfo.IME_ACTION_DONE) {
- setLabel();
- dismissAllowingStateLoss();
- return true;
- }
- return false;
- }
- }
-
- /**
- * Handles completing the label edit from the Ok button of the dialog.
- */
- private class OkListener implements DialogInterface.OnClickListener {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- setLabel();
- dismiss();
- }
- }
-}
diff --git a/src/com/android/deskclock/LabelDialogFragment.kt b/src/com/android/deskclock/LabelDialogFragment.kt
new file mode 100644
index 0000000..77f0b7b
--- /dev/null
+++ b/src/com/android/deskclock/LabelDialogFragment.kt
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.deskclock
+
+import android.app.Dialog
+import android.content.Context
+import android.content.DialogInterface
+import android.content.res.ColorStateList
+import android.os.Bundle
+import android.text.Editable
+import android.text.InputType
+import android.text.TextUtils
+import android.text.TextWatcher
+import android.view.KeyEvent
+import android.view.Window
+import android.view.WindowManager
+import android.view.inputmethod.EditorInfo
+import android.widget.TextView
+import android.widget.TextView.OnEditorActionListener
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.widget.AppCompatEditText
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.provider.Alarm
+
+/**
+ * DialogFragment to edit label.
+ */
+class LabelDialogFragment : DialogFragment() {
+ private var mLabelBox: AppCompatEditText? = null
+ private var mAlarm: Alarm? = null
+ private var mTimerId = 0
+ private var mTag: String? = null
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ // As long as the label box exists, save its state.
+ mLabelBox?. let {
+ outState.putString(ARG_LABEL, it.getText().toString())
+ }
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val args = arguments ?: Bundle.EMPTY
+ mAlarm = args.getParcelable(ARG_ALARM)
+ mTimerId = args.getInt(ARG_TIMER_ID, -1)
+ mTag = args.getString(ARG_TAG)
+
+ var label = args.getString(ARG_LABEL)
+ savedInstanceState?.let {
+ label = it.getString(ARG_LABEL, label)
+ }
+
+ val dialog: AlertDialog = AlertDialog.Builder(requireActivity())
+ .setPositiveButton(android.R.string.ok, OkListener())
+ .setNegativeButton(android.R.string.cancel, null)
+ .setMessage(R.string.label)
+ .create()
+ val context: Context = dialog.context
+
+ val colorControlActivated = ThemeUtils.resolveColor(context, R.attr.colorControlActivated)
+ val colorControlNormal = ThemeUtils.resolveColor(context, R.attr.colorControlNormal)
+
+ mLabelBox = AppCompatEditText(context)
+ mLabelBox?.setSupportBackgroundTintList(ColorStateList(
+ arrayOf(intArrayOf(android.R.attr.state_activated), intArrayOf()),
+ intArrayOf(colorControlActivated, colorControlNormal)))
+ mLabelBox?.setOnEditorActionListener(ImeDoneListener())
+ mLabelBox?.addTextChangedListener(TextChangeListener())
+ mLabelBox?.setSingleLine()
+ mLabelBox?.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
+ mLabelBox?.setText(label)
+ mLabelBox?.selectAll()
+
+ // The line at the bottom of EditText is part of its background therefore the padding
+ // must be added to its container.
+ val padding = context.resources
+ .getDimensionPixelSize(R.dimen.label_edittext_padding)
+ dialog.setView(mLabelBox, padding, 0, padding, 0)
+
+ val alertDialogWindow: Window? = dialog.window
+ alertDialogWindow?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
+ return dialog
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+
+ // Stop callbacks from the IME since there is no view to process them.
+ mLabelBox?.setOnEditorActionListener(null)
+ }
+
+ /**
+ * Sets the new label into the timer or alarm.
+ */
+ private fun setLabel() {
+ var label: String = mLabelBox!!.getText().toString()
+ if (label.trim { it <= ' ' }.isEmpty()) {
+ // Don't allow user to input label with only whitespace.
+ label = ""
+ }
+
+ if (mAlarm != null) {
+ (activity as AlarmLabelDialogHandler).onDialogLabelSet(mAlarm!!, label, mTag!!)
+ } else if (mTimerId >= 0) {
+ val timer: Timer? = DataModel.dataModel.getTimer(mTimerId)
+ if (timer != null) {
+ DataModel.dataModel.setTimerLabel(timer, label)
+ }
+ }
+ }
+
+ interface AlarmLabelDialogHandler {
+ fun onDialogLabelSet(alarm: Alarm, label: String, tag: String)
+ }
+
+ /**
+ * Alters the UI to indicate when input is valid or invalid.
+ */
+ private inner class TextChangeListener : TextWatcher {
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ mLabelBox?.setActivated(!TextUtils.isEmpty(s))
+ }
+
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ }
+
+ override fun afterTextChanged(editable: Editable) {
+ }
+ }
+
+ /**
+ * Handles completing the label edit from the IME keyboard.
+ */
+ private inner class ImeDoneListener : OnEditorActionListener {
+ override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ setLabel()
+ dismissAllowingStateLoss()
+ return true
+ }
+ return false
+ }
+ }
+
+ /**
+ * Handles completing the label edit from the Ok button of the dialog.
+ */
+ private inner class OkListener : DialogInterface.OnClickListener {
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ setLabel()
+ dismiss()
+ }
+ }
+
+ companion object {
+ /**
+ * The tag that identifies instances of LabelDialogFragment in the fragment manager.
+ */
+ private const val TAG = "label_dialog"
+
+ private const val ARG_LABEL = "arg_label"
+ private const val ARG_ALARM = "arg_alarm"
+ private const val ARG_TIMER_ID = "arg_timer_id"
+ private const val ARG_TAG = "arg_tag"
+
+ fun newInstance(alarm: Alarm, label: String?, tag: String?): LabelDialogFragment {
+ val args = Bundle()
+ args.putString(ARG_LABEL, label)
+ args.putParcelable(ARG_ALARM, alarm)
+ args.putString(ARG_TAG, tag)
+
+ val frag = LabelDialogFragment()
+ frag.arguments = args
+ return frag
+ }
+
+ @JvmStatic
+ fun newInstance(timer: Timer): LabelDialogFragment {
+ val args = Bundle()
+ args.putString(ARG_LABEL, timer.label)
+ args.putInt(ARG_TIMER_ID, timer.id)
+
+ val frag = LabelDialogFragment()
+ frag.arguments = args
+ return frag
+ }
+
+ /**
+ * Replaces any existing LabelDialogFragment with the given `fragment`.
+ */
+ @JvmStatic
+ fun show(manager: FragmentManager?, fragment: LabelDialogFragment) {
+ if (manager == null || manager.isDestroyed) {
+ return
+ }
+
+ // Finish any outstanding fragment work.
+ manager.executePendingTransactions()
+
+ val tx = manager.beginTransaction()
+
+ // Remove existing instance of LabelDialogFragment if necessary.
+ val existing = manager.findFragmentByTag(TAG)
+ existing?.let {
+ tx.remove(it)
+ }
+ tx.addToBackStack(null)
+
+ fragment.show(tx, TAG)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/LogUtils.java b/src/com/android/deskclock/LogUtils.java
deleted file mode 100644
index c679ac2..0000000
--- a/src/com/android/deskclock/LogUtils.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.os.Build;
-import android.util.Log;
-
-public class LogUtils {
-
- /**
- * Default logger used for generic logging, i.eTAG. when a specific log tag isn't specified.
- */
- private final static Logger DEFAULT_LOGGER = new Logger("AlarmClock");
-
- public static void v(String message, Object... args) {
- DEFAULT_LOGGER.v(message, args);
- }
-
- public static void d(String message, Object... args) {
- DEFAULT_LOGGER.d(message, args);
- }
-
- public static void i(String message, Object... args) {
- DEFAULT_LOGGER.i(message, args);
- }
-
- public static void w(String message, Object... args) {
- DEFAULT_LOGGER.w(message, args);
- }
-
- public static void e(String message, Object... args) {
- DEFAULT_LOGGER.e(message, args);
- }
-
- public static void e(String message, Throwable e) {
- DEFAULT_LOGGER.e(message, e);
- }
-
- public static void wtf(String message, Object... args) {
- DEFAULT_LOGGER.wtf(message, args);
- }
-
- public static void wtf(Throwable e) {
- DEFAULT_LOGGER.wtf(e);
- }
-
- public final static class Logger {
-
- /**
- * Log everything for debug builds or if running on a dev device.
- */
- public final static boolean DEBUG = BuildConfig.DEBUG
- || "eng".equals(Build.TYPE)
- || "userdebug".equals(Build.TYPE);
-
- public final String logTag;
-
- public Logger(String logTag) {
- this.logTag = logTag;
- }
-
- public boolean isVerboseLoggable() { return DEBUG || Log.isLoggable(logTag, Log.VERBOSE); }
- public boolean isDebugLoggable() { return DEBUG || Log.isLoggable(logTag, Log.DEBUG); }
- public boolean isInfoLoggable() { return DEBUG || Log.isLoggable(logTag, Log.INFO); }
- public boolean isWarnLoggable() { return DEBUG || Log.isLoggable(logTag, Log.WARN); }
- public boolean isErrorLoggable() { return DEBUG || Log.isLoggable(logTag, Log.ERROR); }
- public boolean isWtfLoggable() { return DEBUG || Log.isLoggable(logTag, Log.ASSERT); }
-
- public void v(String message, Object... args) {
- if (isVerboseLoggable()) {
- Log.v(logTag, args == null || args.length == 0
- ? message : String.format(message, args));
- }
- }
-
- public void d(String message, Object... args) {
- if (isDebugLoggable()) {
- Log.d(logTag, args == null || args.length == 0 ? message
- : String.format(message, args));
- }
- }
-
- public void i(String message, Object... args) {
- if (isInfoLoggable()) {
- Log.i(logTag, args == null || args.length == 0 ? message
- : String.format(message, args));
- }
- }
-
- public void w(String message, Object... args) {
- if (isWarnLoggable()) {
- Log.w(logTag, args == null || args.length == 0 ? message
- : String.format(message, args));
- }
- }
-
- public void e(String message, Object... args) {
- if (isErrorLoggable()) {
- Log.e(logTag, args == null || args.length == 0 ? message
- : String.format(message, args));
- }
- }
-
- public void e(String message, Throwable e) {
- if (isErrorLoggable()) {
- Log.e(logTag, message, e);
- }
- }
-
- public void wtf(String message, Object... args) {
- if (isWtfLoggable()) {
- Log.wtf(logTag, args == null || args.length == 0 ? message
- : String.format(message, args));
- }
- }
-
- public void wtf(Throwable e) {
- if (isWtfLoggable()) {
- Log.wtf(logTag, e);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/LogUtils.kt b/src/com/android/deskclock/LogUtils.kt
new file mode 100644
index 0000000..0713559
--- /dev/null
+++ b/src/com/android/deskclock/LogUtils.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.os.Build
+import android.util.Log
+
+object LogUtils {
+ /** Default logger used for generic logging, i.e TAG. when a specific log tag isn't specified.*/
+ private val DEFAULT_LOGGER = Logger("AlarmClock")
+
+ @JvmStatic
+ fun v(message: String, vararg args: Any?) {
+ DEFAULT_LOGGER.v(message, *args)
+ }
+
+ @JvmStatic
+ fun d(message: String, vararg args: Any?) {
+ DEFAULT_LOGGER.d(message, *args)
+ }
+
+ @JvmStatic
+ fun i(message: String, vararg args: Any?) {
+ DEFAULT_LOGGER.i(message, *args)
+ }
+
+ @JvmStatic
+ fun w(message: String, vararg args: Any?) {
+ DEFAULT_LOGGER.w(message, *args)
+ }
+
+ fun e(message: String, vararg args: Any?) {
+ DEFAULT_LOGGER.e(message, *args)
+ }
+
+ @JvmStatic
+ fun e(message: String, e: Throwable) {
+ DEFAULT_LOGGER.e(message, e)
+ }
+
+ fun wtf(message: String, vararg args: Any?) {
+ DEFAULT_LOGGER.wtf(message, *args)
+ }
+
+ @JvmStatic
+ fun wtf(e: Throwable) {
+ DEFAULT_LOGGER.wtf(e)
+ }
+
+ class Logger(val logTag: String) {
+ val isVerboseLoggable: Boolean
+ get() = DEBUG || Log.isLoggable(logTag, Log.VERBOSE)
+
+ val isDebugLoggable: Boolean
+ get() = DEBUG || Log.isLoggable(logTag, Log.DEBUG)
+
+ val isInfoLoggable: Boolean
+ get() = DEBUG || Log.isLoggable(logTag, Log.INFO)
+
+ val isWarnLoggable: Boolean
+ get() = DEBUG || Log.isLoggable(logTag, Log.WARN)
+
+ val isErrorLoggable: Boolean
+ get() = DEBUG || Log.isLoggable(logTag, Log.ERROR)
+
+ val isWtfLoggable: Boolean
+ get() = DEBUG || Log.isLoggable(logTag, Log.ASSERT)
+
+ fun v(message: String, vararg args: Any?) {
+ if (isVerboseLoggable) {
+ Log.v(logTag, if (args.isEmpty() || args[0] == null) {
+ message
+ } else {
+ String.format(message, *args)
+ })
+ }
+ }
+
+ fun d(message: String, vararg args: Any?) {
+ if (isDebugLoggable) {
+ Log.d(logTag, if (args.isEmpty() || args[0] == null) {
+ message
+ } else {
+ String.format(message, *args)
+ })
+ }
+ }
+
+ fun i(message: String, vararg args: Any?) {
+ if (isInfoLoggable) {
+ Log.i(logTag, if (args.isEmpty() || args[0] == null) {
+ message
+ } else {
+ String.format(message, *args)
+ })
+ }
+ }
+
+ fun w(message: String, vararg args: Any?) {
+ if (isWarnLoggable) {
+ Log.w(logTag, if (args.isEmpty() || args[0] == null) {
+ message
+ } else {
+ String.format(message, *args)
+ })
+ }
+ }
+
+ fun e(message: String, vararg args: Any?) {
+ if (isErrorLoggable) {
+ Log.e(logTag, if (args.isEmpty() || args[0] == null) {
+ message
+ } else {
+ String.format(message, *args)
+ })
+ }
+ }
+
+ fun e(message: String, e: Throwable) {
+ if (isErrorLoggable) {
+ Log.e(logTag, message, e)
+ }
+ }
+
+ fun wtf(message: String, vararg args: Any?) {
+ if (isWtfLoggable) {
+ Log.wtf(logTag, if (args.isEmpty() || args[0] == null) {
+ message
+ } else {
+ String.format(message, *args)
+ })
+ }
+ }
+
+ fun wtf(e: Throwable) {
+ if (isWtfLoggable) {
+ Log.wtf(logTag, e)
+ }
+ }
+
+ companion object {
+ /** Log everything for debug builds or if running on a dev device. */
+ val DEBUG = (BuildConfig.DEBUG || "eng" == Build.TYPE || "userdebug" == Build.TYPE)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/MoveScreensaverRunnable.java b/src/com/android/deskclock/MoveScreensaverRunnable.java
deleted file mode 100644
index 73d2527..0000000
--- a/src/com/android/deskclock/MoveScreensaverRunnable.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.view.View;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.AnimatorUtils.getAlphaAnimator;
-import static com.android.deskclock.AnimatorUtils.getScaleAnimator;
-
-/**
- * This runnable chooses a random initial position for {@link #mSaverView} within
- * {@link #mContentView} if {@link #mSaverView} is transparent. It also schedules itself to run
- * each minute, at which time {@link #mSaverView} is faded out, set to a new random location, and
- * faded in.
- */
-public final class MoveScreensaverRunnable implements Runnable {
-
- /** The duration over which the fade in/out animations occur. */
- private static final long FADE_TIME = 3000L;
-
- /** Accelerate the hide animation. */
- private final Interpolator mAcceleration = new AccelerateInterpolator();
-
- /** Decelerate the show animation. */
- private final Interpolator mDeceleration = new DecelerateInterpolator();
-
- /** The container that houses {@link #mSaverView}. */
- private final View mContentView;
-
- /** The display within the {@link #mContentView} that is randomly positioned. */
- private final View mSaverView;
-
- /** Tracks the currently executing animation if any; used to gracefully stop the animation. */
- private Animator mActiveAnimator;
-
- /**
- * @param contentView contains the {@code saverView}
- * @param saverView a child view of {@code contentView} that periodically moves around
- */
- public MoveScreensaverRunnable(View contentView, View saverView) {
- mContentView = contentView;
- mSaverView = saverView;
- }
-
- /**
- * Start or restart the random movement of the saver view within the content view.
- */
- public void start() {
- // Stop any existing animations or callbacks.
- stop();
-
- // Reset the alpha to 0 so saver view will be randomly positioned within the new bounds.
- mSaverView.setAlpha(0);
-
- // Execute the position updater runnable to choose the first random position of saver view.
- run();
-
- // Schedule callbacks every minute to adjust the position of mSaverView.
- UiDataModel.getUiDataModel().addMinuteCallback(this, -FADE_TIME);
- }
-
- /**
- * Stop the random movement of the saver view within the content view.
- */
- public void stop() {
- UiDataModel.getUiDataModel().removePeriodicCallback(this);
-
- // End any animation currently running.
- if (mActiveAnimator != null) {
- mActiveAnimator.end();
- mActiveAnimator = null;
- }
- }
-
- @Override
- public void run() {
- Utils.enforceMainLooper();
-
- final boolean selectInitialPosition = mSaverView.getAlpha() == 0f;
- if (selectInitialPosition) {
- // When selecting an initial position for the saver view the width and height of
- // mContentView are untrustworthy if this was caused by a configuration change. To
- // combat this, we position the mSaverView randomly within the smallest box that is
- // guaranteed to work.
- final int smallestDim = Math.min(mContentView.getWidth(), mContentView.getHeight());
- final float newX = getRandomPoint(smallestDim - mSaverView.getWidth());
- final float newY = getRandomPoint(smallestDim - mSaverView.getHeight());
-
- mSaverView.setX(newX);
- mSaverView.setY(newY);
- mActiveAnimator = getAlphaAnimator(mSaverView, 0f, 1f);
- mActiveAnimator.setDuration(FADE_TIME);
- mActiveAnimator.setInterpolator(mDeceleration);
- mActiveAnimator.start();
- } else {
- // Select a new random position anywhere in mContentView that will fit mSaverView.
- final float newX = getRandomPoint(mContentView.getWidth() - mSaverView.getWidth());
- final float newY = getRandomPoint(mContentView.getHeight() - mSaverView.getHeight());
-
- // Fade out and shrink the saver view.
- final AnimatorSet hide = new AnimatorSet();
- hide.setDuration(FADE_TIME);
- hide.setInterpolator(mAcceleration);
- hide.play(getAlphaAnimator(mSaverView, 1f, 0f))
- .with(getScaleAnimator(mSaverView, 1f, 0.85f));
-
- // Fade in and grow the saver view after altering its position.
- final AnimatorSet show = new AnimatorSet();
- show.setDuration(FADE_TIME);
- show.setInterpolator(mDeceleration);
- show.play(getAlphaAnimator(mSaverView, 0f, 1f))
- .with(getScaleAnimator(mSaverView, 0.85f, 1f));
- show.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mSaverView.setX(newX);
- mSaverView.setY(newY);
- }
- });
-
- // Execute hide followed by show.
- final AnimatorSet all = new AnimatorSet();
- all.play(show).after(hide);
- mActiveAnimator = all;
- mActiveAnimator.start();
- }
- }
-
- /**
- * @return a random integer between 0 and the {@code maximum} exclusive.
- */
- private static float getRandomPoint(float maximum) {
- return (int) (Math.random() * maximum);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/MoveScreensaverRunnable.kt b/src/com/android/deskclock/MoveScreensaverRunnable.kt
new file mode 100644
index 0000000..ece7c7e
--- /dev/null
+++ b/src/com/android/deskclock/MoveScreensaverRunnable.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.view.View
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+
+import com.android.deskclock.uidata.UiDataModel
+
+import kotlin.math.min
+
+/**
+ * This runnable chooses a random initial position for [.mSaverView] within
+ * [.mContentView] if [.mSaverView] is transparent. It also schedules itself to run
+ * each minute, at which time [.mSaverView] is faded out, set to a new random location, and
+ * faded in.
+ */
+class MoveScreensaverRunnable(
+ /** The container that houses [.mSaverView]. */
+ private val mContentView: View,
+ /** The display within the [.mContentView] that is randomly positioned. */
+ private val mSaverView: View
+) : Runnable {
+ /** Accelerate the hide animation. */
+ private val mAcceleration: Interpolator = AccelerateInterpolator()
+
+ /** Decelerate the show animation. */
+ private val mDeceleration: Interpolator = DecelerateInterpolator()
+
+ /** Tracks the currently executing animation if any; used to gracefully stop the animation. */
+ private var mActiveAnimator: Animator? = null
+
+ /** Start or restart the random movement of the saver view within the content view. */
+ fun start() {
+ // Stop any existing animations or callbacks.
+ stop()
+
+ // Reset the alpha to 0 so saver view will be randomly positioned within the new bounds.
+ mSaverView.alpha = 0f
+
+ // Execute the position updater runnable to choose the first random position of saver view.
+ run()
+
+ // Schedule callbacks every minute to adjust the position of mSaverView.
+ UiDataModel.uiDataModel.addMinuteCallback(this, -FADE_TIME)
+ }
+
+ /** Stop the random movement of the saver view within the content view. */
+ fun stop() {
+ UiDataModel.uiDataModel.removePeriodicCallback(this)
+
+ // End any animation currently running.
+ if (mActiveAnimator != null) {
+ mActiveAnimator?.end()
+ mActiveAnimator = null
+ }
+ }
+
+ override fun run() {
+ Utils.enforceMainLooper()
+
+ val selectInitialPosition = mSaverView.alpha == 0f
+ if (selectInitialPosition) {
+ // When selecting an initial position for the saver view the width and height of
+ // mContentView are untrustworthy if this was caused by a configuration change. To
+ // combat this, we position the mSaverView randomly within the smallest box that is
+ // guaranteed to work.
+ val smallestDim = min(mContentView.width, mContentView.height)
+ val newX = getRandomPoint(smallestDim - mSaverView.width.toFloat())
+ val newY = getRandomPoint(smallestDim - mSaverView.height.toFloat())
+
+ mSaverView.x = newX
+ mSaverView.y = newY
+ mActiveAnimator = AnimatorUtils.getAlphaAnimator(mSaverView, 0f, 1f)
+ mActiveAnimator?.duration = FADE_TIME
+ mActiveAnimator?.interpolator = mDeceleration
+ mActiveAnimator?.start()
+ } else {
+ // Select a new random position anywhere in mContentView that will fit mSaverView.
+ val newX = getRandomPoint(mContentView.width - mSaverView.width.toFloat())
+ val newY = getRandomPoint(mContentView.height - mSaverView.height.toFloat())
+
+ // Fade out and shrink the saver view.
+ val hide = AnimatorSet()
+ hide.duration = FADE_TIME
+ hide.interpolator = mAcceleration
+ hide.play(AnimatorUtils.getAlphaAnimator(mSaverView, 1f, 0f))
+ .with(AnimatorUtils.getScaleAnimator(mSaverView, 1f, 0.85f))
+
+ // Fade in and grow the saver view after altering its position.
+ val show = AnimatorSet()
+ show.duration = FADE_TIME
+ show.interpolator = mDeceleration
+ show.play(AnimatorUtils.getAlphaAnimator(mSaverView, 0f, 1f))
+ .with(AnimatorUtils.getScaleAnimator(mSaverView, 0.85f, 1f))
+ show.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ mSaverView.x = newX
+ mSaverView.y = newY
+ }
+ })
+
+ // Execute hide followed by show.
+ val all = AnimatorSet()
+ all.play(show).after(hide)
+ mActiveAnimator = all
+ mActiveAnimator?.start()
+ }
+ }
+
+ companion object {
+ /** The duration over which the fade in/out animations occur. */
+ private const val FADE_TIME = 3000L
+
+ /**
+ * @return a random integer between 0 and the `maximum` exclusive.
+ */
+ private fun getRandomPoint(maximum: Float): Float {
+ return (Math.random() * maximum).toFloat()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/NotificationUtils.kt b/src/com/android/deskclock/NotificationUtils.kt
new file mode 100644
index 0000000..0cb2d55
--- /dev/null
+++ b/src/com/android/deskclock/NotificationUtils.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2020 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.NotificationChannel
+import android.content.Context
+import android.util.ArraySet
+import android.util.Log
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
+import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
+
+object NotificationUtils {
+ private val TAG = NotificationUtils::class.java.simpleName
+
+ /**
+ * Notification channel containing all missed alarm notifications.
+ */
+ const val ALARM_MISSED_NOTIFICATION_CHANNEL_ID = "alarmMissedNotification"
+
+ /**
+ * Notification channel containing all upcoming alarm notifications.
+ */
+ const val ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID = "alarmUpcomingNotification"
+
+ /**
+ * Notification channel containing all snooze notifications.
+ */
+ const val ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID = "alarmSnoozingNotification"
+
+ /**
+ * Notification channel containing all firing alarm and timer notifications.
+ */
+ const val FIRING_NOTIFICATION_CHANNEL_ID = "firingAlarmsAndTimersNotification"
+
+ /**
+ * Notification channel containing all TimerModel notifications.
+ */
+ const val TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "timerNotification"
+
+ /**
+ * Notification channel containing all stopwatch notifications.
+ */
+ const val STOPWATCH_NOTIFICATION_CHANNEL_ID = "stopwatchNotification"
+
+ /**
+ * Values used to bitmask certain channel defaults
+ */
+ private const val PLAY_SOUND = 0x01
+ private const val ENABLE_LIGHTS = 0x02
+ private const val ENABLE_VIBRATION = 0x04
+
+ private val CHANNEL_PROPS: MutableMap<String, IntArray> = HashMap()
+
+ init {
+ CHANNEL_PROPS[ALARM_MISSED_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+ R.string.alarm_missed_channel,
+ IMPORTANCE_HIGH
+ )
+ CHANNEL_PROPS[ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+ R.string.alarm_snooze_channel,
+ IMPORTANCE_LOW
+ )
+ CHANNEL_PROPS[ALARM_UPCOMING_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+ R.string.alarm_upcoming_channel,
+ IMPORTANCE_LOW
+ )
+ CHANNEL_PROPS[FIRING_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+ R.string.firing_alarms_timers_channel,
+ IMPORTANCE_HIGH,
+ ENABLE_LIGHTS
+ )
+ CHANNEL_PROPS[STOPWATCH_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+ R.string.stopwatch_channel,
+ IMPORTANCE_LOW
+ )
+ CHANNEL_PROPS[TIMER_MODEL_NOTIFICATION_CHANNEL_ID] = intArrayOf(
+ R.string.timer_channel,
+ IMPORTANCE_LOW
+ )
+ }
+
+ @JvmStatic
+ fun createChannel(context: Context, id: String) {
+ if (!Utils.isOOrLater) {
+ return
+ }
+
+ if (!CHANNEL_PROPS.containsKey(id)) {
+ Log.e(TAG, "Invalid channel requested: $id")
+ return
+ }
+
+ val properties = CHANNEL_PROPS[id]!!
+ val nameId = properties[0]
+ val importance = properties[1]
+ val channel = NotificationChannel(id, context.getString(nameId), importance)
+ if (properties.size >= 3) {
+ val bits = properties[2]
+ channel.enableLights(bits and ENABLE_LIGHTS != 0)
+ channel.enableVibration(bits and ENABLE_VIBRATION != 0)
+ if (bits and PLAY_SOUND == 0) {
+ channel.setSound(null, null)
+ }
+ }
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ nm.createNotificationChannel(channel)
+ }
+
+ private fun deleteChannel(nm: NotificationManagerCompat, channelId: String) {
+ val channel: NotificationChannel? = nm.getNotificationChannel(channelId)
+ if (channel != null) {
+ nm.deleteNotificationChannel(channelId)
+ }
+ }
+
+ private fun getAllExistingChannelIds(nm: NotificationManagerCompat): Set<String> {
+ val result: MutableSet<String> = ArraySet()
+ for (channel in nm.getNotificationChannels()) {
+ result.add(channel.id)
+ }
+ return result
+ }
+
+ @JvmStatic
+ fun updateNotificationChannels(context: Context) {
+ if (!Utils.isOOrLater) {
+ return
+ }
+
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+
+ // These channels got a new behavior so we need to recreate them with new ids
+ deleteChannel(nm, "alarmLowPriorityNotification")
+ deleteChannel(nm, "alarmHighPriorityNotification")
+ deleteChannel(nm, "StopwatchNotification")
+ deleteChannel(nm, "alarmNotification")
+ deleteChannel(nm, "TimerModelNotification")
+ deleteChannel(nm, "alarmSnoozeNotification")
+
+ // We recreate all existing channels so any language change or our name changes propagate
+ // to the actual channels
+ val existingChannelIds = getAllExistingChannelIds(nm)
+ for (id in existingChannelIds) {
+ createChannel(context, id)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Predicate.java b/src/com/android/deskclock/Predicate.java
deleted file mode 100644
index 9d9cebc..0000000
--- a/src/com/android/deskclock/Predicate.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-/**
- * A Predicate can determine a true or false value for any input of its
- * parameterized type. For example, a {@code RegexPredicate} might implement
- * {@code Predicate<String>}, and return true for any String that matches its
- * given regular expression.
- * <p/>
- * <p/>
- * Implementors of Predicate which may cause side effects upon evaluation are
- * strongly encouraged to state this fact clearly in their API documentation.
- */
-public interface Predicate<T> {
-
- boolean apply(T t);
-
- /**
- * An implementation of the predicate interface that always returns true.
- */
- Predicate TRUE = new Predicate() {
- @Override
- public boolean apply(Object o) {
- return true;
- }
- };
-
- /**
- * An implementation of the predicate interface that always returns false.
- */
- Predicate FALSE = new Predicate() {
- @Override
- public boolean apply(Object o) {
- return false;
- }
- };
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Predicate.kt b/src/com/android/deskclock/Predicate.kt
new file mode 100644
index 0000000..758b57b
--- /dev/null
+++ b/src/com/android/deskclock/Predicate.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+/**
+ * A Predicate can determine a true or false value for any input of its
+ * parameterized type. For example, a `RegexPredicate` might implement
+ * `Predicate<String>`, and return true for any String that matches its
+ * given regular expression.
+ *
+ * Implementors of Predicate which may cause side effects upon evaluation are
+ * strongly encouraged to state this fact clearly in their API documentation.
+ */
+interface Predicate<T> {
+ fun apply(t: T): Boolean
+
+ companion object {
+ /**
+ * An implementation of the predicate interface that always returns true.
+ */
+ @JvmField
+ val TRUE: Predicate<*> = object : Predicate<Any> {
+ override fun apply(t: Any): Boolean = true
+ }
+
+ /**
+ * An implementation of the predicate interface that always returns false.
+ */
+ @JvmField
+ val FALSE: Predicate<*> = object : Predicate<Any> {
+ override fun apply(t: Any): Boolean = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/RingtonePreviewKlaxon.java b/src/com/android/deskclock/RingtonePreviewKlaxon.java
deleted file mode 100644
index eea5fe3..0000000
--- a/src/com/android/deskclock/RingtonePreviewKlaxon.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import android.net.Uri;
-
-public final class RingtonePreviewKlaxon {
-
- private static AsyncRingtonePlayer sAsyncRingtonePlayer;
-
- private RingtonePreviewKlaxon() {
- }
-
- public static void stop(Context context) {
- LogUtils.i("RingtonePreviewKlaxon.stop()");
- getAsyncRingtonePlayer(context).stop();
- }
-
- public static void start(Context context, Uri uri) {
- stop(context);
- LogUtils.i("RingtonePreviewKlaxon.start()");
- getAsyncRingtonePlayer(context).play(uri, 0);
- }
-
- private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
- if (sAsyncRingtonePlayer == null) {
- sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
- }
-
- return sAsyncRingtonePlayer;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/RingtonePreviewKlaxon.kt b/src/com/android/deskclock/RingtonePreviewKlaxon.kt
new file mode 100644
index 0000000..a7d406c
--- /dev/null
+++ b/src/com/android/deskclock/RingtonePreviewKlaxon.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.net.Uri
+
+import com.android.deskclock.LogUtils.i
+
+object RingtonePreviewKlaxon {
+ private var sAsyncRingtonePlayer: AsyncRingtonePlayer? = null
+
+ @JvmStatic
+ fun stop(context: Context) {
+ i("RingtonePreviewKlaxon.stop()")
+ getAsyncRingtonePlayer(context).stop()
+ }
+
+ @JvmStatic
+ fun start(context: Context, uri: Uri) {
+ stop(context)
+ i("RingtonePreviewKlaxon.start()")
+ getAsyncRingtonePlayer(context).play(uri, 0)
+ }
+
+ @Synchronized
+ private fun getAsyncRingtonePlayer(context: Context): AsyncRingtonePlayer {
+ if (sAsyncRingtonePlayer == null) {
+ sAsyncRingtonePlayer = AsyncRingtonePlayer(context.applicationContext)
+ }
+ return sAsyncRingtonePlayer!!
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Screensaver.java b/src/com/android/deskclock/Screensaver.java
deleted file mode 100644
index 427885e..0000000
--- a/src/com/android/deskclock/Screensaver.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.AlarmManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-import android.service.dreams.DreamService;
-import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
-import android.widget.TextClock;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.uidata.UiDataModel;
-
-public final class Screensaver extends DreamService {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Screensaver");
-
- private final OnPreDrawListener mStartPositionUpdater = new StartPositionUpdater();
- private MoveScreensaverRunnable mPositionUpdater;
-
- private String mDateFormat;
- private String mDateFormatForAccessibility;
-
- private View mContentView;
- private View mMainClockView;
- private TextClock mDigitalClock;
- private AnalogClock mAnalogClock;
-
- /* Register ContentObserver to see alarm changes for pre-L */
- private final ContentObserver mSettingsContentObserver =
- Utils.isLOrLater() ? null : new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- Utils.refreshAlarm(Screensaver.this, mContentView);
- }
- };
-
- // Runs every midnight or when the time changes and refreshes the date.
- private final Runnable mMidnightUpdater = new Runnable() {
- @Override
- public void run() {
- Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
- }
- };
-
- /**
- * Receiver to alarm clock changes.
- */
- private final BroadcastReceiver mAlarmChangedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- Utils.refreshAlarm(Screensaver.this, mContentView);
- }
- };
-
- @Override
- public void onCreate() {
- LOGGER.v("Screensaver created");
-
- setTheme(R.style.Theme_DeskClock);
- super.onCreate();
-
- mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
- mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
- }
-
- @Override
- public void onAttachedToWindow() {
- LOGGER.v("Screensaver attached to window");
- super.onAttachedToWindow();
-
- setContentView(R.layout.desk_clock_saver);
-
- mContentView = findViewById(R.id.saver_container);
- mMainClockView = mContentView.findViewById(R.id.main_clock);
- mDigitalClock = (TextClock) mMainClockView.findViewById(R.id.digital_clock);
- mAnalogClock = (AnalogClock) mMainClockView.findViewById(R.id.analog_clock);
-
- setClockStyle();
- Utils.setClockIconTypeface(mContentView);
- Utils.setTimeFormat(mDigitalClock, false);
- mAnalogClock.enableSeconds(false);
-
- mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
- | View.SYSTEM_UI_FLAG_IMMERSIVE
- | View.SYSTEM_UI_FLAG_FULLSCREEN
- | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
-
- mPositionUpdater = new MoveScreensaverRunnable(mContentView, mMainClockView);
-
- // We want the screen saver to exit upon user interaction.
- setInteractive(false);
- setFullscreen(true);
-
- // Setup handlers for time reference changes and date updates.
- if (Utils.isLOrLater()) {
- registerReceiver(mAlarmChangedReceiver,
- new IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED));
- }
-
- if (mSettingsContentObserver != null) {
- @SuppressWarnings("deprecation")
- final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
- getContentResolver().registerContentObserver(uri, false, mSettingsContentObserver);
- }
-
- Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
- Utils.refreshAlarm(this, mContentView);
-
- startPositionUpdater();
- UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
- }
-
- @Override
- public void onDetachedFromWindow() {
- LOGGER.v("Screensaver detached from window");
- super.onDetachedFromWindow();
-
- if (mSettingsContentObserver != null) {
- getContentResolver().unregisterContentObserver(mSettingsContentObserver);
- }
-
- UiDataModel.getUiDataModel().removePeriodicCallback(mMidnightUpdater);
- stopPositionUpdater();
-
- // Tear down handlers for time reference changes and date updates.
- if (Utils.isLOrLater()) {
- unregisterReceiver(mAlarmChangedReceiver);
- }
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- LOGGER.v("Screensaver configuration changed");
- super.onConfigurationChanged(newConfig);
-
- startPositionUpdater();
- }
-
- private void setClockStyle() {
- Utils.setScreensaverClockStyle(mDigitalClock, mAnalogClock);
- final boolean dimNightMode = DataModel.getDataModel().getScreensaverNightModeOn();
- Utils.dimClockView(dimNightMode, mMainClockView);
- setScreenBright(!dimNightMode);
- }
-
- /**
- * The {@link #mContentView} will be drawn shortly. When that draw occurs, the position updater
- * callback will also be executed to choose a random position for the time display as well as
- * schedule future callbacks to move the time display each minute.
- */
- private void startPositionUpdater() {
- if (mContentView != null) {
- mContentView.getViewTreeObserver().addOnPreDrawListener(mStartPositionUpdater);
- }
- }
-
- /**
- * This activity is no longer in the foreground; position callbacks should be removed.
- */
- private void stopPositionUpdater() {
- if (mContentView != null) {
- mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
- }
- mPositionUpdater.stop();
- }
-
- private final class StartPositionUpdater implements OnPreDrawListener {
- /**
- * This callback occurs after initial layout has completed. It is an appropriate place to
- * select a random position for {@link #mMainClockView} and schedule future callbacks to update
- * its position.
- *
- * @return {@code true} to continue with the drawing pass
- */
- @Override
- public boolean onPreDraw() {
- if (mContentView.getViewTreeObserver().isAlive()) {
- // (Re)start the periodic position updater.
- mPositionUpdater.start();
-
- // This listener must now be removed to avoid starting the position updater again.
- mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
- }
- return true;
- }
- }
-}
diff --git a/src/com/android/deskclock/Screensaver.kt b/src/com/android/deskclock/Screensaver.kt
new file mode 100644
index 0000000..3c64131
--- /dev/null
+++ b/src/com/android/deskclock/Screensaver.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.AlarmManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Configuration
+import android.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.service.dreams.DreamService
+import android.view.View
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.widget.TextClock
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.uidata.UiDataModel
+
+class Screensaver : DreamService() {
+ private val mStartPositionUpdater: OnPreDrawListener = StartPositionUpdater()
+ private var mPositionUpdater: MoveScreensaverRunnable? = null
+
+ private var mDateFormat: String? = null
+ private var mDateFormatForAccessibility: String? = null
+
+ private var mContentView: View? = null
+ private var mMainClockView: View? = null
+ private var mDigitalClock: TextClock? = null
+ private var mAnalogClock: AnalogClock? = null
+
+ /* Register ContentObserver to see alarm changes for pre-L */
+ private val mSettingsContentObserver: ContentObserver? = if (Utils.isLOrLater) {
+ null
+ } else {
+ object : ContentObserver(Handler(Looper.myLooper()!!)) {
+ override fun onChange(selfChange: Boolean) {
+ Utils.refreshAlarm(this@Screensaver, mContentView)
+ }
+ }
+ }
+
+ // Runs every midnight or when the time changes and refreshes the date.
+ private val mMidnightUpdater = Runnable {
+ Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+ }
+
+ /**
+ * Receiver to alarm clock changes.
+ */
+ private val mAlarmChangedReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Utils.refreshAlarm(this@Screensaver, mContentView)
+ }
+ }
+
+ override fun onCreate() {
+ LOGGER.v("Screensaver created")
+
+ setTheme(R.style.Theme_DeskClock)
+ super.onCreate()
+
+ mDateFormat = getString(R.string.abbrev_wday_month_day_no_year)
+ mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year)
+ }
+
+ override fun onAttachedToWindow() {
+ LOGGER.v("Screensaver attached to window")
+ super.onAttachedToWindow()
+
+ setContentView(R.layout.desk_clock_saver)
+
+ mContentView = findViewById(R.id.saver_container)
+ mMainClockView = mContentView?.findViewById(R.id.main_clock)
+ mDigitalClock = mMainClockView?.findViewById<View>(R.id.digital_clock) as TextClock
+ mAnalogClock = mMainClockView?.findViewById<View>(R.id.analog_clock) as AnalogClock
+
+ setClockStyle()
+ Utils.setClockIconTypeface(mContentView)
+ Utils.setTimeFormat(mDigitalClock, false)
+ mAnalogClock?.enableSeconds(false)
+
+ mContentView?.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
+ or View.SYSTEM_UI_FLAG_IMMERSIVE
+ or View.SYSTEM_UI_FLAG_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+
+ mPositionUpdater = MoveScreensaverRunnable(mContentView!!, mMainClockView!!)
+
+ // We want the screen saver to exit upon user interaction.
+ isInteractive = false
+ isFullscreen = true
+
+ // Setup handlers for time reference changes and date updates.
+ if (Utils.isLOrLater) {
+ registerReceiver(mAlarmChangedReceiver,
+ IntentFilter(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED))
+ }
+
+ mSettingsContentObserver?.let {
+ val uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED)
+ contentResolver.registerContentObserver(uri, false, it)
+ }
+
+ Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+ Utils.refreshAlarm(this, mContentView)
+
+ startPositionUpdater()
+ UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
+ }
+
+ override fun onDetachedFromWindow() {
+ LOGGER.v("Screensaver detached from window")
+ super.onDetachedFromWindow()
+
+ mSettingsContentObserver?.let {
+ contentResolver.unregisterContentObserver(it)
+ }
+
+ UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
+ stopPositionUpdater()
+
+ // Tear down handlers for time reference changes and date updates.
+ if (Utils.isLOrLater) {
+ unregisterReceiver(mAlarmChangedReceiver)
+ }
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ LOGGER.v("Screensaver configuration changed")
+ super.onConfigurationChanged(newConfig)
+
+ startPositionUpdater()
+ }
+
+ private fun setClockStyle() {
+ Utils.setScreensaverClockStyle(mDigitalClock!!, mAnalogClock!!)
+ val dimNightMode: Boolean = DataModel.dataModel.screensaverNightModeOn
+ Utils.dimClockView(dimNightMode, mMainClockView!!)
+ isScreenBright = !dimNightMode
+ }
+
+ /**
+ * The [.mContentView] will be drawn shortly. When that draw occurs, the position updater
+ * callback will also be executed to choose a random position for the time display as well as
+ * schedule future callbacks to move the time display each minute.
+ */
+ private fun startPositionUpdater() {
+ mContentView?.viewTreeObserver?.addOnPreDrawListener(mStartPositionUpdater)
+ }
+
+ /**
+ * This activity is no longer in the foreground; position callbacks should be removed.
+ */
+ private fun stopPositionUpdater() {
+ mContentView?.viewTreeObserver?.removeOnPreDrawListener(mStartPositionUpdater)
+ mPositionUpdater?.stop()
+ }
+
+ private inner class StartPositionUpdater : OnPreDrawListener {
+ /**
+ * This callback occurs after initial layout has completed. It is an appropriate place to
+ * select a random position for [.mMainClockView] and schedule future callbacks to update
+ * its position.
+ *
+ * @return `true` to continue with the drawing pass
+ */
+ override fun onPreDraw(): Boolean {
+ if (mContentView!!.viewTreeObserver.isAlive) {
+ // (Re)start the periodic position updater.
+ mPositionUpdater?.start()
+
+ // This listener must now be removed to avoid starting the position updater again.
+ mContentView?.viewTreeObserver?.removeOnPreDrawListener(mStartPositionUpdater)
+ }
+ return true
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("Screensaver")
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ScreensaverActivity.java b/src/com/android/deskclock/ScreensaverActivity.java
deleted file mode 100644
index 656cfc7..0000000
--- a/src/com/android/deskclock/ScreensaverActivity.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.app.AlarmManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.provider.Settings;
-import android.view.View;
-import android.view.ViewTreeObserver.OnPreDrawListener;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.TextClock;
-
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static android.content.Intent.ACTION_BATTERY_CHANGED;
-import static android.os.BatteryManager.EXTRA_PLUGGED;
-
-public class ScreensaverActivity extends BaseActivity {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("ScreensaverActivity");
-
- /** These flags keep the screen on if the device is plugged in. */
- private static final int WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
- | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
- | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
-
- private final OnPreDrawListener mStartPositionUpdater = new StartPositionUpdater();
-
- private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- LOGGER.v("ScreensaverActivity onReceive, action: " + intent.getAction());
-
- switch (intent.getAction()) {
- case Intent.ACTION_POWER_CONNECTED:
- updateWakeLock(true);
- break;
- case Intent.ACTION_POWER_DISCONNECTED:
- updateWakeLock(false);
- break;
- case Intent.ACTION_USER_PRESENT:
- finish();
- break;
- case AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED:
- Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
- break;
- }
- }
- };
-
- /* Register ContentObserver to see alarm changes for pre-L */
- private final ContentObserver mSettingsContentObserver = Utils.isPreL()
- ? new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
- }
- }
- : null;
-
- // Runs every midnight or when the time changes and refreshes the date.
- private final Runnable mMidnightUpdater = new Runnable() {
- @Override
- public void run() {
- Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
- }
- };
-
- private String mDateFormat;
- private String mDateFormatForAccessibility;
-
- private View mContentView;
- private View mMainClockView;
-
- private MoveScreensaverRunnable mPositionUpdater;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mDateFormat = getString(R.string.abbrev_wday_month_day_no_year);
- mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year);
-
- setContentView(R.layout.desk_clock_saver);
- mContentView = findViewById(R.id.saver_container);
- mMainClockView = mContentView.findViewById(R.id.main_clock);
-
- final View digitalClock = mMainClockView.findViewById(R.id.digital_clock);
- final AnalogClock analogClock =
- (AnalogClock) mMainClockView.findViewById(R.id.analog_clock);
-
- Utils.setClockIconTypeface(mMainClockView);
- Utils.setTimeFormat((TextClock) digitalClock, false);
- Utils.setClockStyle(digitalClock, analogClock);
- Utils.dimClockView(true, mMainClockView);
- analogClock.enableSeconds(false);
-
- mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE
- | View.SYSTEM_UI_FLAG_IMMERSIVE
- | View.SYSTEM_UI_FLAG_FULLSCREEN
- | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
- mContentView.setOnSystemUiVisibilityChangeListener(new InteractionListener());
-
- mPositionUpdater = new MoveScreensaverRunnable(mContentView, mMainClockView);
-
- final Intent intent = getIntent();
- if (intent != null) {
- final int eventLabel = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, 0);
- Events.sendScreensaverEvent(R.string.action_show, eventLabel);
- }
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- final IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_POWER_CONNECTED);
- filter.addAction(Intent.ACTION_POWER_DISCONNECTED);
- filter.addAction(Intent.ACTION_USER_PRESENT);
- if (Utils.isLOrLater()) {
- filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
- }
- registerReceiver(mIntentReceiver, filter);
-
- if (mSettingsContentObserver != null) {
- @SuppressWarnings("deprecation")
- final Uri uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED);
- getContentResolver().registerContentObserver(uri, false, mSettingsContentObserver);
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView);
- Utils.refreshAlarm(ScreensaverActivity.this, mContentView);
-
- startPositionUpdater();
- UiDataModel.getUiDataModel().addMidnightCallback(mMidnightUpdater, 100);
-
- final Intent intent = registerReceiver(null, new IntentFilter(ACTION_BATTERY_CHANGED));
- final boolean pluggedIn = intent != null && intent.getIntExtra(EXTRA_PLUGGED, 0) != 0;
- updateWakeLock(pluggedIn);
- }
-
- @Override
- public void onPause() {
- super.onPause();
- UiDataModel.getUiDataModel().removePeriodicCallback(mMidnightUpdater);
- stopPositionUpdater();
- }
-
- @Override
- public void onStop() {
- if (mSettingsContentObserver != null) {
- getContentResolver().unregisterContentObserver(mSettingsContentObserver);
- }
- unregisterReceiver(mIntentReceiver);
- super.onStop();
- }
-
- @Override
- public void onUserInteraction() {
- // We want the screen saver to exit upon user interaction.
- finish();
- }
-
- /**
- * @param pluggedIn {@code true} iff the device is currently plugged in to a charger
- */
- private void updateWakeLock(boolean pluggedIn) {
- final Window win = getWindow();
- final WindowManager.LayoutParams winParams = win.getAttributes();
- winParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
- if (pluggedIn) {
- winParams.flags |= WINDOW_FLAGS;
- } else {
- winParams.flags &= (~WINDOW_FLAGS);
- }
- win.setAttributes(winParams);
- }
-
- /**
- * The {@link #mContentView} will be drawn shortly. When that draw occurs, the position updater
- * callback will also be executed to choose a random position for the time display as well as
- * schedule future callbacks to move the time display each minute.
- */
- private void startPositionUpdater() {
- mContentView.getViewTreeObserver().addOnPreDrawListener(mStartPositionUpdater);
- }
-
- /**
- * This activity is no longer in the foreground; position callbacks should be removed.
- */
- private void stopPositionUpdater() {
- mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
- mPositionUpdater.stop();
- }
-
- private final class StartPositionUpdater implements OnPreDrawListener {
- /**
- * This callback occurs after initial layout has completed. It is an appropriate place to
- * select a random position for {@link #mMainClockView} and schedule future callbacks to update
- * its position.
- *
- * @return {@code true} to continue with the drawing pass
- */
- @Override
- public boolean onPreDraw() {
- if (mContentView.getViewTreeObserver().isAlive()) {
- // Start the periodic position updater.
- mPositionUpdater.start();
-
- // This listener must now be removed to avoid starting the position updater again.
- mContentView.getViewTreeObserver().removeOnPreDrawListener(mStartPositionUpdater);
- }
- return true;
- }
- }
-
- private final class InteractionListener implements View.OnSystemUiVisibilityChangeListener {
- @Override
- public void onSystemUiVisibilityChange(int visibility) {
- // When the user interacts with the screen, the navigation bar reappears
- if ((visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
- // We want the screen saver to exit upon user interaction.
- finish();
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/ScreensaverActivity.kt b/src/com/android/deskclock/ScreensaverActivity.kt
new file mode 100644
index 0000000..2f0f47f
--- /dev/null
+++ b/src/com/android/deskclock/ScreensaverActivity.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.app.AlarmManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.os.BatteryManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.view.View
+import android.view.View.OnSystemUiVisibilityChangeListener
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.Window
+import android.view.WindowManager
+import android.widget.TextClock
+
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+
+class ScreensaverActivity : BaseActivity() {
+ private val mStartPositionUpdater: OnPreDrawListener = StartPositionUpdater()
+
+ private val mIntentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ LOGGER.v("ScreensaverActivity onReceive, action: " + intent.action)
+
+ when (intent.action) {
+ Intent.ACTION_POWER_CONNECTED -> updateWakeLock(true)
+ Intent.ACTION_POWER_DISCONNECTED -> updateWakeLock(false)
+ Intent.ACTION_USER_PRESENT -> finish()
+ AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED -> {
+ Utils.refreshAlarm(this@ScreensaverActivity, mContentView)
+ }
+ }
+ }
+ }
+
+ /* Register ContentObserver to see alarm changes for pre-L */
+ private val mSettingsContentObserver: ContentObserver? = if (Utils.isPreL) {
+ object : ContentObserver(Handler(Looper.myLooper()!!)) {
+ override fun onChange(selfChange: Boolean) {
+ Utils.refreshAlarm(this@ScreensaverActivity, mContentView)
+ }
+ }
+ } else {
+ null
+ }
+
+ // Runs every midnight or when the time changes and refreshes the date.
+ private val mMidnightUpdater = Runnable {
+ Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+ }
+
+ private lateinit var mDateFormat: String
+ private lateinit var mDateFormatForAccessibility: String
+
+ private lateinit var mContentView: View
+ private lateinit var mMainClockView: View
+
+ private lateinit var mPositionUpdater: MoveScreensaverRunnable
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ mDateFormat = getString(R.string.abbrev_wday_month_day_no_year)
+ mDateFormatForAccessibility = getString(R.string.full_wday_month_day_no_year)
+
+ setContentView(R.layout.desk_clock_saver)
+ mContentView = findViewById(R.id.saver_container)
+ mMainClockView = mContentView.findViewById(R.id.main_clock)
+
+ val digitalClock = mMainClockView.findViewById<View>(R.id.digital_clock)
+ val analogClock = mMainClockView.findViewById<View>(R.id.analog_clock) as AnalogClock
+
+ Utils.setClockIconTypeface(mMainClockView)
+ Utils.setTimeFormat(digitalClock as TextClock, false)
+ Utils.setClockStyle(digitalClock, analogClock)
+ Utils.dimClockView(true, mMainClockView)
+ analogClock.enableSeconds(false)
+
+ mContentView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
+ or View.SYSTEM_UI_FLAG_IMMERSIVE
+ or View.SYSTEM_UI_FLAG_FULLSCREEN
+ or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
+ mContentView.setOnSystemUiVisibilityChangeListener(InteractionListener())
+
+ mPositionUpdater = MoveScreensaverRunnable(mContentView, mMainClockView)
+
+ getIntent()?.let {
+ val eventLabel = it.getIntExtra(Events.EXTRA_EVENT_LABEL, 0)
+ Events.sendScreensaverEvent(R.string.action_show, eventLabel)
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ val filter = IntentFilter()
+ filter.addAction(Intent.ACTION_POWER_CONNECTED)
+ filter.addAction(Intent.ACTION_POWER_DISCONNECTED)
+ filter.addAction(Intent.ACTION_USER_PRESENT)
+ if (Utils.isLOrLater) {
+ filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED)
+ }
+ registerReceiver(mIntentReceiver, filter)
+
+ mSettingsContentObserver?.let {
+ val uri = Settings.System.getUriFor(Settings.System.NEXT_ALARM_FORMATTED)
+ getContentResolver().registerContentObserver(uri, false, it)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ Utils.updateDate(mDateFormat, mDateFormatForAccessibility, mContentView)
+ Utils.refreshAlarm(this, mContentView)
+
+ startPositionUpdater()
+ UiDataModel.uiDataModel.addMidnightCallback(mMidnightUpdater)
+
+ val intent: Intent? = registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
+ val pluggedIn = intent != null && intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
+ updateWakeLock(pluggedIn)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ UiDataModel.uiDataModel.removePeriodicCallback(mMidnightUpdater)
+ stopPositionUpdater()
+ }
+
+ override fun onStop() {
+ mSettingsContentObserver?.let {
+ getContentResolver().unregisterContentObserver(it)
+ }
+ unregisterReceiver(mIntentReceiver)
+ super.onStop()
+ }
+
+ override fun onUserInteraction() {
+ // We want the screen saver to exit upon user interaction.
+ finish()
+ }
+
+ /**
+ * @param pluggedIn `true` iff the device is currently plugged in to a charger
+ */
+ private fun updateWakeLock(pluggedIn: Boolean) {
+ val win: Window = getWindow()
+ val winParams = win.attributes
+ winParams.flags = winParams.flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
+ if (pluggedIn) {
+ winParams.flags = winParams.flags or WINDOW_FLAGS
+ } else {
+ winParams.flags = winParams.flags and WINDOW_FLAGS.inv()
+ }
+ win.attributes = winParams
+ }
+
+ /**
+ * The [.mContentView] will be drawn shortly. When that draw occurs, the position updater
+ * callback will also be executed to choose a random position for the time display as well as
+ * schedule future callbacks to move the time display each minute.
+ */
+ private fun startPositionUpdater() {
+ mContentView.viewTreeObserver.addOnPreDrawListener(mStartPositionUpdater)
+ }
+
+ /**
+ * This activity is no longer in the foreground; position callbacks should be removed.
+ */
+ private fun stopPositionUpdater() {
+ mContentView.viewTreeObserver.removeOnPreDrawListener(mStartPositionUpdater)
+ mPositionUpdater.stop()
+ }
+
+ private inner class StartPositionUpdater : OnPreDrawListener {
+ /**
+ * This callback occurs after initial layout has completed. It is an appropriate place to
+ * select a random position for [.mMainClockView] and schedule future callbacks to update
+ * its position.
+ *
+ * @return `true` to continue with the drawing pass
+ */
+ override fun onPreDraw(): Boolean {
+ if (mContentView.viewTreeObserver.isAlive) {
+ // Start the periodic position updater.
+ mPositionUpdater.start()
+
+ // This listener must now be removed to avoid starting the position updater again.
+ mContentView.viewTreeObserver.removeOnPreDrawListener(mStartPositionUpdater)
+ }
+ return true
+ }
+ }
+
+ private inner class InteractionListener : OnSystemUiVisibilityChangeListener {
+ override fun onSystemUiVisibilityChange(visibility: Int) {
+ // When the user interacts with the screen, the navigation bar reappears
+ if (visibility and View.SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0) {
+ // We want the screen saver to exit upon user interaction.
+ finish()
+ }
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("ScreensaverActivity")
+
+ /** These flags keep the screen on if the device is plugged in. */
+ private const val WINDOW_FLAGS = (WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
+ or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/StopwatchTextController.java b/src/com/android/deskclock/StopwatchTextController.java
deleted file mode 100644
index 4b94dbd..0000000
--- a/src/com/android/deskclock/StopwatchTextController.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import android.widget.TextView;
-
-import com.android.deskclock.uidata.UiDataModel;
-
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-
-/**
- * A controller which will format a provided time in millis to display as a stopwatch.
- */
-public final class StopwatchTextController {
-
- private final TextView mMainTextView;
- private final TextView mHundredthsTextView;
-
- private long mLastTime = Long.MIN_VALUE;
-
- public StopwatchTextController(TextView mainTextView, TextView hundredthsTextView) {
- mMainTextView = mainTextView;
- mHundredthsTextView = hundredthsTextView;
- }
-
- public void setTimeString(long accumulatedTime) {
- // Since time is only displayed to centiseconds, if there is a change at the milliseconds
- // level but not the centiseconds level, we can avoid unnecessary work.
- if ((mLastTime / 10) == (accumulatedTime / 10)) {
- return;
- }
-
- final int hours = (int) (accumulatedTime / HOUR_IN_MILLIS);
- int remainder = (int) (accumulatedTime % HOUR_IN_MILLIS);
-
- final int minutes = (int) (remainder / MINUTE_IN_MILLIS);
- remainder = (int) (remainder % MINUTE_IN_MILLIS);
-
- final int seconds = (int) (remainder / SECOND_IN_MILLIS);
- remainder = (int) (remainder % SECOND_IN_MILLIS);
-
- mHundredthsTextView.setText(UiDataModel.getUiDataModel().getFormattedNumber(
- remainder / 10, 2));
-
- // Avoid unnecessary computations and garbage creation if seconds have not changed since
- // last layout pass.
- if ((mLastTime / SECOND_IN_MILLIS) != (accumulatedTime / SECOND_IN_MILLIS)) {
- final Context context = mMainTextView.getContext();
- final String time = Utils.getTimeString(context, hours, minutes, seconds);
- mMainTextView.setText(time);
- }
- mLastTime = accumulatedTime;
- }
-}
diff --git a/src/com/android/deskclock/StopwatchTextController.kt b/src/com/android/deskclock/StopwatchTextController.kt
new file mode 100644
index 0000000..a2a2c8e
--- /dev/null
+++ b/src/com/android/deskclock/StopwatchTextController.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.text.format.DateUtils
+import android.widget.TextView
+
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * A controller which will format a provided time in millis to display as a stopwatch.
+ */
+class StopwatchTextController(
+ private val mMainTextView: TextView,
+ private val mHundredthsTextView: TextView
+) {
+ private var mLastTime = Long.MIN_VALUE
+
+ fun setTimeString(accumulatedTime: Long) {
+ // Since time is only displayed to centiseconds, if there is a change at the milliseconds
+ // level but not the centiseconds level, we can avoid unnecessary work.
+ if (mLastTime / 10 == accumulatedTime / 10) {
+ return
+ }
+
+ val hours = (accumulatedTime / DateUtils.HOUR_IN_MILLIS).toInt()
+ var remainder = (accumulatedTime % DateUtils.HOUR_IN_MILLIS).toInt()
+
+ val minutes = (remainder / DateUtils.MINUTE_IN_MILLIS).toInt()
+ remainder = (remainder % DateUtils.MINUTE_IN_MILLIS).toInt()
+
+ val seconds = (remainder / DateUtils.SECOND_IN_MILLIS).toInt()
+ remainder = (remainder % DateUtils.SECOND_IN_MILLIS).toInt()
+
+ mHundredthsTextView.text = UiDataModel.uiDataModel.getFormattedNumber(remainder / 10, 2)
+
+ // Avoid unnecessary computations and garbage creation if seconds have not changed since
+ // last layout pass.
+ if (mLastTime / DateUtils.SECOND_IN_MILLIS !=
+ accumulatedTime / DateUtils.SECOND_IN_MILLIS) {
+ val context = mMainTextView.context
+ val time = Utils.getTimeString(context, hours, minutes, seconds)
+ mMainTextView.text = time
+ }
+ mLastTime = accumulatedTime
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ThemeUtils.java b/src/com/android/deskclock/ThemeUtils.java
deleted file mode 100644
index a6d3156..0000000
--- a/src/com/android/deskclock/ThemeUtils.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import androidx.annotation.AttrRes;
-import androidx.annotation.ColorInt;
-
-public final class ThemeUtils {
-
- /** Temporary array used internally to resolve attributes. */
- private static final int[] TEMP_ATTR = new int[1];
-
- private ThemeUtils() {
- // Prevent instantiation.
- }
-
- /**
- * Convenience method for retrieving a themed color value.
- *
- * @param context the {@link Context} to resolve the theme attribute against
- * @param attr the attribute corresponding to the color to resolve
- * @return the color value of the resolved attribute
- */
- @ColorInt
- public static int resolveColor(Context context, @AttrRes int attr) {
- return resolveColor(context, attr, null /* stateSet */);
- }
-
- /**
- * Convenience method for retrieving a themed color value.
- *
- * @param context the {@link Context} to resolve the theme attribute against
- * @param attr the attribute corresponding to the color to resolve
- * @param stateSet an array of {@link android.view.View} states
- * @return the color value of the resolved attribute
- */
- @ColorInt
- public static int resolveColor(Context context, @AttrRes int attr, @AttrRes int[] stateSet) {
- final TypedArray a;
- synchronized (TEMP_ATTR) {
- TEMP_ATTR[0] = attr;
- a = context.obtainStyledAttributes(TEMP_ATTR);
- }
-
- try {
- if (stateSet == null) {
- return a.getColor(0, Color.RED);
- }
-
- final ColorStateList colorStateList = a.getColorStateList(0);
- if (colorStateList != null) {
- return colorStateList.getColorForState(stateSet, Color.RED);
- }
- return Color.RED;
- } finally {
- a.recycle();
- }
- }
-
- /**
- * Convenience method for retrieving a themed drawable.
- *
- * @param context the {@link Context} to resolve the theme attribute against
- * @param attr the attribute corresponding to the drawable to resolve
- * @return the drawable of the resolved attribute
- */
- public static Drawable resolveDrawable(Context context, @AttrRes int attr) {
- final TypedArray a;
- synchronized (TEMP_ATTR) {
- TEMP_ATTR[0] = attr;
- a = context.obtainStyledAttributes(TEMP_ATTR);
- }
-
- try {
- return a.getDrawable(0);
- } finally {
- a.recycle();
- }
- }
-}
-
diff --git a/src/com/android/deskclock/ThemeUtils.kt b/src/com/android/deskclock/ThemeUtils.kt
new file mode 100644
index 0000000..1fdae54
--- /dev/null
+++ b/src/com/android/deskclock/ThemeUtils.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
+
+object ThemeUtils {
+ /** Temporary array used internally to resolve attributes. */
+ private val TEMP_ATTR = IntArray(1)
+
+ /**
+ * Convenience method for retrieving a themed color value.
+ *
+ * @param context the [Context] to resolve the theme attribute against
+ * @param attr the attribute corresponding to the color to resolve
+ * @return the color value of the resolved attribute
+ */
+ @ColorInt
+ fun resolveColor(context: Context, @AttrRes attr: Int): Int =
+ resolveColor(context, attr, stateSet = null)
+
+ /**
+ * Convenience method for retrieving a themed color value.
+ *
+ * @param context the [Context] to resolve the theme attribute against
+ * @param attr the attribute corresponding to the color to resolve
+ * @param stateSet an array of [android.view.View] states
+ * @return the color value of the resolved attribute
+ */
+ @JvmStatic
+ @ColorInt
+ fun resolveColor(context: Context, @AttrRes attr: Int, @AttrRes stateSet: IntArray?): Int {
+ var a: TypedArray
+ synchronized(TEMP_ATTR) {
+ TEMP_ATTR[0] = attr
+ a = context.obtainStyledAttributes(TEMP_ATTR)
+ }
+
+ try {
+ if (stateSet == null) {
+ return a.getColor(0, Color.RED)
+ }
+ val colorStateList = a.getColorStateList(0)
+ return colorStateList?.getColorForState(stateSet, Color.RED) ?: Color.RED
+ } finally {
+ a.recycle()
+ }
+ }
+
+ /**
+ * Convenience method for retrieving a themed drawable.
+ *
+ * @param context the [Context] to resolve the theme attribute against
+ * @param attr the attribute corresponding to the drawable to resolve
+ * @return the drawable of the resolved attribute
+ */
+ @JvmStatic
+ fun resolveDrawable(context: Context, @AttrRes attr: Int): Drawable? {
+ var a: TypedArray
+ synchronized(TEMP_ATTR) {
+ TEMP_ATTR[0] = attr
+ a = context.obtainStyledAttributes(TEMP_ATTR)
+ }
+
+ return try {
+ a.getDrawable(0)
+ } finally {
+ a.recycle()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/TimerCircleFrameLayout.java b/src/com/android/deskclock/TimerCircleFrameLayout.java
deleted file mode 100644
index e0cf320..0000000
--- a/src/com/android/deskclock/TimerCircleFrameLayout.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.FrameLayout;
-
-/**
- * A container that frames a timer circle of some sort. The circle is allowed to grow naturally
- * according to its layout constraints up to the {@link R.dimen#max_timer_circle_size largest}
- * allowable size.
- */
-public class TimerCircleFrameLayout extends FrameLayout {
-
- public TimerCircleFrameLayout(Context context) {
- super(context);
- }
-
- public TimerCircleFrameLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public TimerCircleFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- /**
- * Note: this method assumes the parent container will specify {@link MeasureSpec#EXACTLY exact}
- * width and height values.
- *
- * @param widthMeasureSpec horizontal space requirements as imposed by the parent
- * @param heightMeasureSpec vertical space requirements as imposed by the parent
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int paddingLeft = getPaddingLeft();
- final int paddingRight = getPaddingRight();
-
- final int paddingTop = getPaddingTop();
- final int paddingBottom = getPaddingBottom();
-
- // Fetch the exact sizes imposed by the parent container.
- final int width = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight;
- final int height = MeasureSpec.getSize(heightMeasureSpec) - paddingTop - paddingBottom;
- final int smallestDimension = Math.min(width, height);
-
- // Fetch the absolute maximum circle size allowed.
- final int maxSize = getResources().getDimensionPixelSize(R.dimen.max_timer_circle_size);
- final int size = Math.min(smallestDimension, maxSize);
-
- // Set the size of this container.
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingLeft + paddingRight,
- MeasureSpec.EXACTLY);
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingTop + paddingBottom,
- MeasureSpec.EXACTLY);
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-}
diff --git a/src/com/android/deskclock/TimerCircleFrameLayout.kt b/src/com/android/deskclock/TimerCircleFrameLayout.kt
new file mode 100644
index 0000000..b77c738
--- /dev/null
+++ b/src/com/android/deskclock/TimerCircleFrameLayout.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+import kotlin.math.min
+
+/**
+ * A container that frames a timer circle of some sort. The circle is allowed to grow naturally
+ * according to its layout constraints up to the [largest][R.dimen.max_timer_circle_size]
+ * allowable size.
+ */
+class TimerCircleFrameLayout : FrameLayout {
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
+ /**
+ * Note: this method assumes the parent container will specify [exact][MeasureSpec.EXACTLY]
+ * width and height values.
+ *
+ * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+ * @param heightMeasureSpec vertical space requirements as imposed by the parent
+ */
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ var variableWidthMeasureSpec = widthMeasureSpec
+ var variableHeightMeasureSpec = heightMeasureSpec
+ val paddingLeft = paddingLeft
+ val paddingRight = paddingRight
+
+ val paddingTop = paddingTop
+ val paddingBottom = paddingBottom
+
+ // Fetch the exact sizes imposed by the parent container.
+ val width = MeasureSpec.getSize(variableWidthMeasureSpec) - paddingLeft - paddingRight
+ val height = MeasureSpec.getSize(variableHeightMeasureSpec) - paddingTop - paddingBottom
+ val smallestDimension = min(width, height)
+
+ // Fetch the absolute maximum circle size allowed.
+ val maxSize = resources.getDimensionPixelSize(R.dimen.max_timer_circle_size)
+ val size = min(smallestDimension, maxSize)
+
+ // Set the size of this container.
+ variableWidthMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingLeft + paddingRight,
+ MeasureSpec.EXACTLY)
+ variableHeightMeasureSpec = MeasureSpec.makeMeasureSpec(size + paddingTop + paddingBottom,
+ MeasureSpec.EXACTLY)
+
+ super.onMeasure(variableWidthMeasureSpec, variableHeightMeasureSpec)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/TimerTextController.java b/src/com/android/deskclock/TimerTextController.java
deleted file mode 100644
index 1744a7b..0000000
--- a/src/com/android/deskclock/TimerTextController.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.widget.TextView;
-
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-
-/**
- * A controller which will format a provided time in millis to display as a timer.
- */
-public final class TimerTextController {
-
- private final TextView mTextView;
-
- public TimerTextController(TextView textView) {
- mTextView = textView;
- }
-
- public void setTimeString(long remainingTime) {
- boolean isNegative = false;
- if (remainingTime < 0) {
- remainingTime = -remainingTime;
- isNegative = true;
- }
-
- int hours = (int) (remainingTime / HOUR_IN_MILLIS);
- int remainder = (int) (remainingTime % HOUR_IN_MILLIS);
-
- int minutes = (int) (remainder / MINUTE_IN_MILLIS);
- remainder = (int) (remainder % MINUTE_IN_MILLIS);
-
- int seconds = (int) (remainder / SECOND_IN_MILLIS);
- remainder = (int) (remainder % SECOND_IN_MILLIS);
-
- // Round up to the next second
- if (!isNegative && remainder != 0) {
- seconds++;
- if (seconds == 60) {
- seconds = 0;
- minutes++;
- if (minutes == 60) {
- minutes = 0;
- hours++;
- }
- }
- }
-
- String time = Utils.getTimeString(mTextView.getContext(), hours, minutes, seconds);
- if (isNegative && !(hours == 0 && minutes == 0 && seconds == 0)) {
- time = "\u2212" + time;
- }
-
- mTextView.setText(time);
- }
-}
diff --git a/src/com/android/deskclock/TimerTextController.kt b/src/com/android/deskclock/TimerTextController.kt
new file mode 100644
index 0000000..77610e6
--- /dev/null
+++ b/src/com/android/deskclock/TimerTextController.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.text.format.DateUtils
+import android.widget.TextView
+
+/**
+ * A controller which will format a provided time in millis to display as a timer.
+ */
+class TimerTextController(private val mTextView: TextView) {
+ fun setTimeString(remainingTime: Long) {
+ var variableRemainingTime = remainingTime
+ var isNegative = false
+ if (variableRemainingTime < 0) {
+ variableRemainingTime = -variableRemainingTime
+ isNegative = true
+ }
+
+ var hours = (variableRemainingTime / DateUtils.HOUR_IN_MILLIS).toInt()
+ var remainder = (variableRemainingTime % DateUtils.HOUR_IN_MILLIS).toInt()
+
+ var minutes = (remainder / DateUtils.MINUTE_IN_MILLIS).toInt()
+ remainder = (remainder % DateUtils.MINUTE_IN_MILLIS).toInt()
+
+ var seconds = (remainder / DateUtils.SECOND_IN_MILLIS).toInt()
+ remainder = (remainder % DateUtils.SECOND_IN_MILLIS).toInt()
+
+ // Round up to the next second
+ if (!isNegative && remainder != 0) {
+ seconds++
+ if (seconds == 60) {
+ seconds = 0
+ minutes++
+ if (minutes == 60) {
+ minutes = 0
+ hours++
+ }
+ }
+ }
+
+ var time = Utils.getTimeString(mTextView.context, hours, minutes, seconds)
+ if (isNegative && !(hours == 0 && minutes == 0 && seconds == 0)) {
+ time = "\u2212" + time
+ }
+
+ mTextView.text = time
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Utils.java b/src/com/android/deskclock/Utils.java
deleted file mode 100644
index 0c9daf6..0000000
--- a/src/com/android/deskclock/Utils.java
+++ /dev/null
@@ -1,628 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
-import android.app.AlarmManager;
-import android.app.AlarmManager.AlarmClockInfo;
-import android.app.PendingIntent;
-import android.appwidget.AppWidgetManager;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.Typeface;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Looper;
-import android.provider.Settings;
-import androidx.annotation.AnyRes;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.StringRes;
-import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
-import androidx.core.os.BuildCompat;
-import androidx.core.view.AccessibilityDelegateCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
-import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.text.format.DateUtils;
-import android.text.style.RelativeSizeSpan;
-import android.text.style.StyleSpan;
-import android.text.style.TypefaceSpan;
-import android.util.ArraySet;
-import android.view.View;
-import android.widget.TextClock;
-import android.widget.TextView;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.text.NumberFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
-import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY;
-import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
-import static android.graphics.Bitmap.Config.ARGB_8888;
-
-public class Utils {
-
- /**
- * {@link Uri} signifying the "silent" ringtone.
- */
- public static final Uri RINGTONE_SILENT = Uri.EMPTY;
-
- public static void enforceMainLooper() {
- if (Looper.getMainLooper() != Looper.myLooper()) {
- throw new IllegalAccessError("May only call from main thread.");
- }
- }
-
- public static void enforceNotMainLooper() {
- if (Looper.getMainLooper() == Looper.myLooper()) {
- throw new IllegalAccessError("May not call from main thread.");
- }
- }
-
- public static int indexOf(Object[] array, Object item) {
- for (int i = 0; i < array.length; i++) {
- if (array[i].equals(item)) {
- return i;
- }
- }
- return -1;
- }
-
- /**
- * @return {@code true} if the device is prior to {@link Build.VERSION_CODES#LOLLIPOP}
- */
- public static boolean isPreL() {
- return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
- }
-
- /**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or
- * {@link Build.VERSION_CODES#LOLLIPOP_MR1}
- */
- public static boolean isLOrLMR1() {
- final int sdkInt = Build.VERSION.SDK_INT;
- return sdkInt == Build.VERSION_CODES.LOLLIPOP || sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1;
- }
-
- /**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP} or later
- */
- public static boolean isLOrLater() {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
- }
-
- /**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#LOLLIPOP_MR1} or later
- */
- public static boolean isLMR1OrLater() {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
- }
-
- /**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#M} or later
- */
- public static boolean isMOrLater() {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
- }
-
- /**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#N} or later
- */
- public static boolean isNOrLater() {
- return BuildCompat.isAtLeastN();
- }
-
- /**
- * @return {@code true} if the device is {@link Build.VERSION_CODES#N_MR1} or later
- */
- public static boolean isNMR1OrLater() {
- return BuildCompat.isAtLeastNMR1();
- }
-
- /**
- * @param resourceId identifies an application resource
- * @return the Uri by which the application resource is accessed
- */
- public static Uri getResourceUri(Context context, @AnyRes int resourceId) {
- return new Uri.Builder()
- .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
- .authority(context.getPackageName())
- .path(String.valueOf(resourceId))
- .build();
- }
-
- /**
- * @param view the scrollable view to test
- * @return {@code true} iff the {@code view} content is currently scrolled to the top
- */
- public static boolean isScrolledToTop(View view) {
- return !view.canScrollVertically(-1);
- }
-
- /**
- * Calculate the amount by which the radius of a CircleTimerView should be offset by any
- * of the extra painted objects.
- */
- public static float calculateRadiusOffset(
- float strokeSize, float dotStrokeSize, float markerStrokeSize) {
- return Math.max(strokeSize, Math.max(dotStrokeSize, markerStrokeSize));
- }
-
- /**
- * Configure the clock that is visible to display seconds. The clock that is not visible never
- * displays seconds to avoid it scheduling unnecessary ticking runnables.
- */
- public static void setClockSecondsEnabled(TextClock digitalClock, AnalogClock analogClock) {
- final boolean displaySeconds = DataModel.getDataModel().getDisplayClockSeconds();
- final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle();
- switch (clockStyle) {
- case ANALOG:
- setTimeFormat(digitalClock, false);
- analogClock.enableSeconds(displaySeconds);
- return;
- case DIGITAL:
- analogClock.enableSeconds(false);
- setTimeFormat(digitalClock, displaySeconds);
- return;
- }
-
- throw new IllegalStateException("unexpected clock style: " + clockStyle);
- }
-
- /**
- * Set whether the digital or analog clock should be displayed in the application.
- * Returns the view to be displayed.
- */
- public static View setClockStyle(View digitalClock, View analogClock) {
- final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getClockStyle();
- switch (clockStyle) {
- case ANALOG:
- digitalClock.setVisibility(View.GONE);
- analogClock.setVisibility(View.VISIBLE);
- return analogClock;
- case DIGITAL:
- digitalClock.setVisibility(View.VISIBLE);
- analogClock.setVisibility(View.GONE);
- return digitalClock;
- }
-
- throw new IllegalStateException("unexpected clock style: " + clockStyle);
- }
-
- /**
- * For screensavers to set whether the digital or analog clock should be displayed.
- * Returns the view to be displayed.
- */
- public static View setScreensaverClockStyle(View digitalClock, View analogClock) {
- final DataModel.ClockStyle clockStyle = DataModel.getDataModel().getScreensaverClockStyle();
- switch (clockStyle) {
- case ANALOG:
- digitalClock.setVisibility(View.GONE);
- analogClock.setVisibility(View.VISIBLE);
- return analogClock;
- case DIGITAL:
- digitalClock.setVisibility(View.VISIBLE);
- analogClock.setVisibility(View.GONE);
- return digitalClock;
- }
-
- throw new IllegalStateException("unexpected clock style: " + clockStyle);
- }
-
- /**
- * For screensavers to dim the lights if necessary.
- */
- public static void dimClockView(boolean dim, View clockView) {
- Paint paint = new Paint();
- paint.setColor(Color.WHITE);
- paint.setColorFilter(new PorterDuffColorFilter(
- (dim ? 0x40FFFFFF : 0xC0FFFFFF),
- PorterDuff.Mode.MULTIPLY));
- clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
- }
-
- /**
- * Update and return the PendingIntent corresponding to the given {@code intent}.
- *
- * @param context the Context in which the PendingIntent should start the service
- * @param intent an Intent describing the service to be started
- * @return a PendingIntent that will start a service
- */
- public static PendingIntent pendingServiceIntent(Context context, Intent intent) {
- return PendingIntent.getService(context, 0, intent, FLAG_UPDATE_CURRENT);
- }
-
- /**
- * Update and return the PendingIntent corresponding to the given {@code intent}.
- *
- * @param context the Context in which the PendingIntent should start the activity
- * @param intent an Intent describing the activity to be started
- * @return a PendingIntent that will start an activity
- */
- public static PendingIntent pendingActivityIntent(Context context, Intent intent) {
- return PendingIntent.getActivity(context, 0, intent, FLAG_UPDATE_CURRENT);
- }
-
- /**
- * @return The next alarm from {@link AlarmManager}
- */
- public static String getNextAlarm(Context context) {
- return isPreL() ? getNextAlarmPreL(context) : getNextAlarmLOrLater(context);
- }
-
- @SuppressWarnings("deprecation")
- @TargetApi(Build.VERSION_CODES.KITKAT)
- private static String getNextAlarmPreL(Context context) {
- final ContentResolver cr = context.getContentResolver();
- return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED);
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private static String getNextAlarmLOrLater(Context context) {
- final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- final AlarmClockInfo info = getNextAlarmClock(am);
- if (info != null) {
- final long triggerTime = info.getTriggerTime();
- final Calendar alarmTime = Calendar.getInstance();
- alarmTime.setTimeInMillis(triggerTime);
- return AlarmUtils.getFormattedTime(context, alarmTime);
- }
-
- return null;
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private static AlarmClockInfo getNextAlarmClock(AlarmManager am) {
- return am.getNextAlarmClock();
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- public static void updateNextAlarm(AlarmManager am, AlarmClockInfo info, PendingIntent op) {
- am.setAlarmClock(info, op);
- }
-
- public static boolean isAlarmWithin24Hours(AlarmInstance alarmInstance) {
- final Calendar nextAlarmTime = alarmInstance.getAlarmTime();
- final long nextAlarmTimeMillis = nextAlarmTime.getTimeInMillis();
- return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS;
- }
-
- /**
- * Clock views can call this to refresh their alarm to the next upcoming value.
- */
- public static void refreshAlarm(Context context, View clock) {
- final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon);
- final TextView nextAlarmView = (TextView) clock.findViewById(R.id.nextAlarm);
- if (nextAlarmView == null) {
- return;
- }
-
- final String alarm = getNextAlarm(context);
- if (!TextUtils.isEmpty(alarm)) {
- final String description = context.getString(R.string.next_alarm_description, alarm);
- nextAlarmView.setText(alarm);
- nextAlarmView.setContentDescription(description);
- nextAlarmView.setVisibility(View.VISIBLE);
- nextAlarmIconView.setVisibility(View.VISIBLE);
- nextAlarmIconView.setContentDescription(description);
- } else {
- nextAlarmView.setVisibility(View.GONE);
- nextAlarmIconView.setVisibility(View.GONE);
- }
- }
-
- public static void setClockIconTypeface(View clock) {
- final TextView nextAlarmIconView = (TextView) clock.findViewById(R.id.nextAlarmIcon);
- nextAlarmIconView.setTypeface(UiDataModel.getUiDataModel().getAlarmIconTypeface());
- }
-
- /**
- * Clock views can call this to refresh their date.
- **/
- public static void updateDate(String dateSkeleton, String descriptionSkeleton, View clock) {
- final TextView dateDisplay = (TextView) clock.findViewById(R.id.date);
- if (dateDisplay == null) {
- return;
- }
-
- final Locale l = Locale.getDefault();
- final String datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton);
- final String descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton);
-
- final Date now = new Date();
- dateDisplay.setText(new SimpleDateFormat(datePattern, l).format(now));
- dateDisplay.setVisibility(View.VISIBLE);
- dateDisplay.setContentDescription(new SimpleDateFormat(descriptionPattern, l).format(now));
- }
-
- /***
- * Formats the time in the TextClock according to the Locale with a special
- * formatting treatment for the am/pm label.
- *
- * @param clock TextClock to format
- * @param includeSeconds whether or not to include seconds in the clock's time
- */
- public static void setTimeFormat(TextClock clock, boolean includeSeconds) {
- if (clock != null) {
- // Get the best format for 12 hours mode according to the locale
- clock.setFormat12Hour(get12ModeFormat(0.4f /* amPmRatio */, includeSeconds));
- // Get the best format for 24 hours mode according to the locale
- clock.setFormat24Hour(get24ModeFormat(includeSeconds));
- }
- }
-
- /**
- * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the
- * am/pm string to the time string
- * @param includeSeconds whether or not to include seconds in the time string
- * @return format string for 12 hours mode time, not including seconds
- */
- public static CharSequence get12ModeFormat(float amPmRatio, boolean includeSeconds) {
- String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
- includeSeconds ? "hmsa" : "hma");
- if (amPmRatio <= 0) {
- pattern = pattern.replaceAll("a", "").trim();
- }
-
- // Replace spaces with "Hair Space"
- pattern = pattern.replaceAll(" ", "\u200A");
- // Build a spannable so that the am/pm will be formatted
- int amPmPos = pattern.indexOf('a');
- if (amPmPos == -1) {
- return pattern;
- }
-
- final Spannable sp = new SpannableString(pattern);
- sp.setSpan(new RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- sp.setSpan(new StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- sp.setSpan(new TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1,
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- return sp;
- }
-
- public static CharSequence get24ModeFormat(boolean includeSeconds) {
- return DateFormat.getBestDateTimePattern(Locale.getDefault(),
- includeSeconds ? "Hms" : "Hm");
- }
-
- /**
- * Returns string denoting the timezone hour offset (e.g. GMT -8:00)
- *
- * @param useShortForm Whether to return a short form of the header that rounds to the
- * nearest hour and excludes the "GMT" prefix
- */
- public static String getGMTHourOffset(TimeZone timezone, boolean useShortForm) {
- final int gmtOffset = timezone.getRawOffset();
- final long hour = gmtOffset / DateUtils.HOUR_IN_MILLIS;
- final long min = (Math.abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS) /
- DateUtils.MINUTE_IN_MILLIS;
-
- if (useShortForm) {
- return String.format(Locale.ENGLISH, "%+d", hour);
- } else {
- return String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min);
- }
- }
-
- /**
- * Given a point in time, return the subsequent moment any of the time zones changes days.
- * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for
- * midnight on 1/2/2016 in the NY timezone since it changes days first.
- *
- * @param time a point in time from which to compute midnight on the subsequent day
- * @param zones a collection of time zones
- * @return the nearest point in the future at which any of the time zones changes days
- */
- public static Date getNextDay(Date time, Collection<TimeZone> zones) {
- Calendar next = null;
- for (TimeZone tz : zones) {
- final Calendar c = Calendar.getInstance(tz);
- c.setTime(time);
-
- // Advance to the next day.
- c.add(Calendar.DAY_OF_YEAR, 1);
-
- // Reset the time to midnight.
- c.set(Calendar.HOUR_OF_DAY, 0);
- c.set(Calendar.MINUTE, 0);
- c.set(Calendar.SECOND, 0);
- c.set(Calendar.MILLISECOND, 0);
-
- if (next == null || c.compareTo(next) < 0) {
- next = c;
- }
- }
-
- return next == null ? null : next.getTime();
- }
-
- public static String getNumberFormattedQuantityString(Context context, int id, int quantity) {
- final String localizedQuantity = NumberFormat.getInstance().format(quantity);
- return context.getResources().getQuantityString(id, quantity, localizedQuantity);
- }
-
- /**
- * @return {@code true} iff the widget is being hosted in a container where tapping is allowed
- */
- public static boolean isWidgetClickable(AppWidgetManager widgetManager, int widgetId) {
- final Bundle wo = widgetManager.getAppWidgetOptions(widgetId);
- return wo != null
- && wo.getInt(OPTION_APPWIDGET_HOST_CATEGORY, -1) != WIDGET_CATEGORY_KEYGUARD;
- }
-
- /**
- * @return a vector-drawable inflated from the given {@code resId}
- */
- public static VectorDrawableCompat getVectorDrawable(Context context, @DrawableRes int resId) {
- return VectorDrawableCompat.create(context.getResources(), resId, context.getTheme());
- }
-
- /**
- * This method assumes the given {@code view} has already been layed out.
- *
- * @return a Bitmap containing an image of the {@code view} at its current size
- */
- public static Bitmap createBitmap(View view) {
- final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), ARGB_8888);
- final Canvas canvas = new Canvas(bitmap);
- view.draw(canvas);
- return bitmap;
- }
-
- /**
- * {@link ArraySet} is @hide prior to {@link Build.VERSION_CODES#M}.
- */
- @SuppressLint("NewApi")
- public static <E> ArraySet<E> newArraySet(Collection<E> collection) {
- final ArraySet<E> arraySet = new ArraySet<>(collection.size());
- arraySet.addAll(collection);
- return arraySet;
- }
-
- /**
- * @param context from which to query the current device configuration
- * @return {@code true} if the device is currently in portrait or reverse portrait orientation
- */
- public static boolean isPortrait(Context context) {
- return context.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
- }
-
- /**
- * @param context from which to query the current device configuration
- * @return {@code true} if the device is currently in landscape or reverse landscape orientation
- */
- public static boolean isLandscape(Context context) {
- return context.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
- }
-
- public static long now() {
- return DataModel.getDataModel().elapsedRealtime();
- }
-
- public static long wallClock() {
- return DataModel.getDataModel().currentTimeMillis();
- }
-
- /**
- * @param context to obtain strings.
- * @param displayMinutes whether or not minutes should be included
- * @param isAhead {@code true} if the time should be marked 'ahead', else 'behind'
- * @param hoursDifferent the number of hours the time is ahead/behind
- * @param minutesDifferent the number of minutes the time is ahead/behind
- * @return String describing the hours/minutes ahead or behind
- */
- public static String createHoursDifferentString(Context context, boolean displayMinutes,
- boolean isAhead, int hoursDifferent, int minutesDifferent) {
- String timeString;
- if (displayMinutes && hoursDifferent != 0) {
- // Both minutes and hours
- final String hoursShortQuantityString =
- Utils.getNumberFormattedQuantityString(context,
- R.plurals.hours_short, Math.abs(hoursDifferent));
- final String minsShortQuantityString =
- Utils.getNumberFormattedQuantityString(context,
- R.plurals.minutes_short, Math.abs(minutesDifferent));
- final @StringRes int stringType = isAhead
- ? R.string.world_hours_minutes_ahead
- : R.string.world_hours_minutes_behind;
- timeString = context.getString(stringType, hoursShortQuantityString,
- minsShortQuantityString);
- } else {
- // Minutes alone or hours alone
- final String hoursQuantityString = Utils.getNumberFormattedQuantityString(
- context, R.plurals.hours, Math.abs(hoursDifferent));
- final String minutesQuantityString = Utils.getNumberFormattedQuantityString(
- context, R.plurals.minutes, Math.abs(minutesDifferent));
- final @StringRes int stringType = isAhead ? R.string.world_time_ahead
- : R.string.world_time_behind;
- timeString = context.getString(stringType, displayMinutes
- ? minutesQuantityString : hoursQuantityString);
- }
- return timeString;
- }
-
- /**
- * @param context The context from which to obtain strings
- * @param hours Hours to display (if any)
- * @param minutes Minutes to display (if any)
- * @param seconds Seconds to display
- * @return Provided time formatted as a String
- */
- static String getTimeString(Context context, int hours, int minutes, int seconds) {
- if (hours != 0) {
- return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds);
- }
- if (minutes != 0) {
- return context.getString(R.string.minutes_seconds, minutes, seconds);
- }
- return context.getString(R.string.seconds, seconds);
- }
-
- public static final class ClickAccessibilityDelegate extends AccessibilityDelegateCompat {
-
- /** The label for talkback to apply to the view */
- private final String mLabel;
-
- /** Whether or not to always make the view visible to talkback */
- private final boolean mIsAlwaysAccessibilityVisible;
-
- public ClickAccessibilityDelegate(String label) {
- this(label, false);
- }
-
- public ClickAccessibilityDelegate(String label, boolean isAlwaysAccessibilityVisible) {
- mLabel = label;
- mIsAlwaysAccessibilityVisible = isAlwaysAccessibilityVisible;
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- if (mIsAlwaysAccessibilityVisible) {
- info.setVisibleToUser(true);
- }
- info.addAction(new AccessibilityActionCompat(
- AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel));
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/Utils.kt b/src/com/android/deskclock/Utils.kt
new file mode 100644
index 0000000..4692320
--- /dev/null
+++ b/src/com/android/deskclock/Utils.kt
@@ -0,0 +1,615 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.app.AlarmManager
+import android.app.AlarmManager.AlarmClockInfo
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Typeface
+import android.net.Uri
+import android.os.Build
+import android.os.Looper
+import android.provider.Settings
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.text.format.DateUtils
+import android.text.style.RelativeSizeSpan
+import android.text.style.StyleSpan
+import android.text.style.TypefaceSpan
+import android.util.ArraySet
+import android.view.View
+import android.widget.TextClock
+import android.widget.TextView
+import androidx.annotation.AnyRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.core.os.BuildCompat
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
+import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.uidata.UiDataModel
+
+import java.text.NumberFormat
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+import kotlin.math.abs
+import kotlin.math.max
+
+object Utils {
+ /**
+ * [Uri] signifying the "silent" ringtone.
+ */
+ @JvmField
+ val RINGTONE_SILENT = Uri.EMPTY
+
+ fun enforceMainLooper() {
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw IllegalAccessError("May only call from main thread.")
+ }
+ }
+
+ fun enforceNotMainLooper() {
+ if (Looper.getMainLooper() == Looper.myLooper()) {
+ throw IllegalAccessError("May not call from main thread.")
+ }
+ }
+
+ fun indexOf(array: Array<out Any>, item: Any): Int {
+ for (i in array.indices) {
+ if (array[i] == item) {
+ return i
+ }
+ }
+ return -1
+ }
+
+ /**
+ * @return `true` if the device is prior to [Build.VERSION_CODES.LOLLIPOP]
+ */
+ val isPreL: Boolean
+ get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
+
+ /**
+ * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP] or
+ * [Build.VERSION_CODES.LOLLIPOP_MR1]
+ */
+ val isLOrLMR1: Boolean
+ get() {
+ val sdkInt = Build.VERSION.SDK_INT
+ return sdkInt == Build.VERSION_CODES.LOLLIPOP ||
+ sdkInt == Build.VERSION_CODES.LOLLIPOP_MR1
+ }
+
+ /**
+ * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP] or later
+ */
+ val isLOrLater: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+
+ /**
+ * @return `true` if the device is [Build.VERSION_CODES.LOLLIPOP_MR1] or later
+ */
+ val isLMR1OrLater: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1
+
+ /**
+ * @return `true` if the device is [Build.VERSION_CODES.M] or later
+ */
+ val isMOrLater: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+
+ /**
+ * @return `true` if the device is [Build.VERSION_CODES.N] or later
+ */
+ val isNOrLater: Boolean
+ get() = BuildCompat.isAtLeastN()
+
+ /**
+ * @return `true` if the device is [Build.VERSION_CODES.N_MR1] or later
+ */
+ val isNMR1OrLater: Boolean
+ get() = BuildCompat.isAtLeastNMR1()
+
+ /**
+ * @return `true` if the device is [Build.VERSION_CODES.O] or later
+ */
+ val isOOrLater: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+
+ /**
+ * @param resourceId identifies an application resource
+ * @return the Uri by which the application resource is accessed
+ */
+ fun getResourceUri(context: Context, @AnyRes resourceId: Int): Uri {
+ return Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .authority(context.packageName)
+ .path(resourceId.toString())
+ .build()
+ }
+
+ /**
+ * @param view the scrollable view to test
+ * @return `true` iff the `view` content is currently scrolled to the top
+ */
+ fun isScrolledToTop(view: View): Boolean {
+ return !view.canScrollVertically(-1)
+ }
+
+ /**
+ * Calculate the amount by which the radius of a CircleTimerView should be offset by any
+ * of the extra painted objects.
+ */
+ fun calculateRadiusOffset(
+ strokeSize: Float,
+ dotStrokeSize: Float,
+ markerStrokeSize: Float
+ ): Float {
+ return max(strokeSize, max(dotStrokeSize, markerStrokeSize))
+ }
+
+ /**
+ * Configure the clock that is visible to display seconds. The clock that is not visible never
+ * displays seconds to avoid it scheduling unnecessary ticking runnables.
+ */
+ fun setClockSecondsEnabled(digitalClock: TextClock, analogClock: AnalogClock) {
+ val displaySeconds: Boolean = DataModel.dataModel.displayClockSeconds
+ when (DataModel.dataModel.clockStyle) {
+ DataModel.ClockStyle.ANALOG -> {
+ setTimeFormat(digitalClock, false)
+ analogClock.enableSeconds(displaySeconds)
+ }
+ DataModel.ClockStyle.DIGITAL -> {
+ analogClock.enableSeconds(false)
+ setTimeFormat(digitalClock, displaySeconds)
+ }
+ }
+ }
+
+ /**
+ * Set whether the digital or analog clock should be displayed in the application.
+ * Returns the view to be displayed.
+ */
+ fun setClockStyle(digitalClock: View, analogClock: View): View {
+ return when (DataModel.dataModel.clockStyle) {
+ DataModel.ClockStyle.ANALOG -> {
+ digitalClock.visibility = View.GONE
+ analogClock.visibility = View.VISIBLE
+ analogClock
+ }
+ DataModel.ClockStyle.DIGITAL -> {
+ digitalClock.visibility = View.VISIBLE
+ analogClock.visibility = View.GONE
+ digitalClock
+ }
+ }
+ }
+
+ /**
+ * For screensavers to set whether the digital or analog clock should be displayed.
+ * Returns the view to be displayed.
+ */
+ fun setScreensaverClockStyle(digitalClock: View, analogClock: View): View {
+ return when (DataModel.dataModel.screensaverClockStyle) {
+ DataModel.ClockStyle.ANALOG -> {
+ digitalClock.visibility = View.GONE
+ analogClock.visibility = View.VISIBLE
+ analogClock
+ }
+ DataModel.ClockStyle.DIGITAL -> {
+ digitalClock.visibility = View.VISIBLE
+ analogClock.visibility = View.GONE
+ digitalClock
+ }
+ }
+ }
+
+ /**
+ * For screensavers to dim the lights if necessary.
+ */
+ fun dimClockView(dim: Boolean, clockView: View) {
+ val paint = Paint()
+ paint.color = Color.WHITE
+ paint.colorFilter = PorterDuffColorFilter(
+ if (dim) 0x40FFFFFF else -0x3f000001,
+ PorterDuff.Mode.MULTIPLY)
+ clockView.setLayerType(View.LAYER_TYPE_HARDWARE, paint)
+ }
+
+ /**
+ * Update and return the PendingIntent corresponding to the given `intent`.
+ *
+ * @param context the Context in which the PendingIntent should start the service
+ * @param intent an Intent describing the service to be started
+ * @return a PendingIntent that will start a service
+ */
+ fun pendingServiceIntent(context: Context, intent: Intent): PendingIntent {
+ return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ /**
+ * Update and return the PendingIntent corresponding to the given `intent`.
+ *
+ * @param context the Context in which the PendingIntent should start the activity
+ * @param intent an Intent describing the activity to be started
+ * @return a PendingIntent that will start an activity
+ */
+ fun pendingActivityIntent(context: Context, intent: Intent): PendingIntent {
+ return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
+ /**
+ * @return The next alarm from [AlarmManager]
+ */
+ fun getNextAlarm(context: Context): String? {
+ return if (isPreL) getNextAlarmPreL(context) else getNextAlarmLOrLater(context)
+ }
+
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private fun getNextAlarmPreL(context: Context): String {
+ val cr = context.contentResolver
+ return Settings.System.getString(cr, Settings.System.NEXT_ALARM_FORMATTED)
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private fun getNextAlarmLOrLater(context: Context): String? {
+ val am = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val info = getNextAlarmClock(am)
+ if (info != null) {
+ val triggerTime = info.triggerTime
+ val alarmTime = Calendar.getInstance()
+ alarmTime.timeInMillis = triggerTime
+ return AlarmUtils.getFormattedTime(context, alarmTime)
+ }
+
+ return null
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private fun getNextAlarmClock(am: AlarmManager): AlarmClockInfo? {
+ return am.nextAlarmClock
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ fun updateNextAlarm(am: AlarmManager, info: AlarmClockInfo?, op: PendingIntent?) {
+ am.setAlarmClock(info, op)
+ }
+
+ fun isAlarmWithin24Hours(alarmInstance: AlarmInstance): Boolean {
+ val nextAlarmTime: Calendar = alarmInstance.alarmTime
+ val nextAlarmTimeMillis = nextAlarmTime.timeInMillis
+ return nextAlarmTimeMillis - System.currentTimeMillis() <= DateUtils.DAY_IN_MILLIS
+ }
+
+ /**
+ * Clock views can call this to refresh their alarm to the next upcoming value.
+ */
+ fun refreshAlarm(context: Context, clock: View?) {
+ val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView
+ val nextAlarmView = clock.findViewById<View>(R.id.nextAlarm) as TextView? ?: return
+
+ val alarm = getNextAlarm(context)
+ if (!TextUtils.isEmpty(alarm)) {
+ val description = context.getString(R.string.next_alarm_description, alarm)
+ nextAlarmView.text = alarm
+ nextAlarmView.contentDescription = description
+ nextAlarmView.visibility = View.VISIBLE
+ nextAlarmIconView.visibility = View.VISIBLE
+ nextAlarmIconView.contentDescription = description
+ } else {
+ nextAlarmView.visibility = View.GONE
+ nextAlarmIconView.visibility = View.GONE
+ }
+ }
+
+ fun setClockIconTypeface(clock: View?) {
+ val nextAlarmIconView = clock?.findViewById<View>(R.id.nextAlarmIcon) as TextView?
+ nextAlarmIconView?.typeface = UiDataModel.uiDataModel.alarmIconTypeface
+ }
+
+ /**
+ * Clock views can call this to refresh their date.
+ */
+ fun updateDate(dateSkeleton: String?, descriptionSkeleton: String?, clock: View?) {
+ val dateDisplay = clock?.findViewById<View>(R.id.date) as TextView? ?: return
+
+ val l = Locale.getDefault()
+ val datePattern = DateFormat.getBestDateTimePattern(l, dateSkeleton)
+ val descriptionPattern = DateFormat.getBestDateTimePattern(l, descriptionSkeleton)
+
+ val now = Date()
+ dateDisplay.text = SimpleDateFormat(datePattern, l).format(now)
+ dateDisplay.visibility = View.VISIBLE
+ dateDisplay.contentDescription = SimpleDateFormat(descriptionPattern, l).format(now)
+ }
+
+ /***
+ * Formats the time in the TextClock according to the Locale with a special
+ * formatting treatment for the am/pm label.
+ *
+ * @param clock TextClock to format
+ * @param includeSeconds whether or not to include seconds in the clock's time
+ */
+ fun setTimeFormat(clock: TextClock?, includeSeconds: Boolean) {
+ // Get the best format for 12 hours mode according to the locale
+ clock?.format12Hour = get12ModeFormat(amPmRatio = 0.4f, includeSeconds = includeSeconds)
+ // Get the best format for 24 hours mode according to the locale
+ clock?.format24Hour = get24ModeFormat(includeSeconds)
+ }
+
+ /**
+ * @param amPmRatio a value between 0 and 1 that is the ratio of the relative size of the
+ * am/pm string to the time string
+ * @param includeSeconds whether or not to include seconds in the time string
+ * @return format string for 12 hours mode time, not including seconds
+ */
+ fun get12ModeFormat(amPmRatio: Float, includeSeconds: Boolean): CharSequence {
+ var pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
+ if (includeSeconds) "hmsa" else "hma")
+ if (amPmRatio <= 0) {
+ pattern = pattern.replace("a".toRegex(), "").trim { it <= ' ' }
+ }
+
+ // Replace spaces with "Hair Space"
+ pattern = pattern.replace(" ".toRegex(), "\u200A")
+ // Build a spannable so that the am/pm will be formatted
+ val amPmPos = pattern.indexOf('a')
+ if (amPmPos == -1) {
+ return pattern
+ }
+
+ val sp: Spannable = SpannableString(pattern)
+ sp.setSpan(RelativeSizeSpan(amPmRatio), amPmPos, amPmPos + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ sp.setSpan(StyleSpan(Typeface.NORMAL), amPmPos, amPmPos + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ sp.setSpan(TypefaceSpan("sans-serif"), amPmPos, amPmPos + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+
+ return sp
+ }
+
+ fun get24ModeFormat(includeSeconds: Boolean): CharSequence {
+ return DateFormat.getBestDateTimePattern(Locale.getDefault(),
+ if (includeSeconds) "Hms" else "Hm")
+ }
+
+ /**
+ * Returns string denoting the timezone hour offset (e.g. GMT -8:00)
+ *
+ * @param useShortForm Whether to return a short form of the header that rounds to the
+ * nearest hour and excludes the "GMT" prefix
+ */
+ fun getGMTHourOffset(timezone: TimeZone, useShortForm: Boolean): String {
+ val gmtOffset = timezone.rawOffset
+ val hour = gmtOffset / DateUtils.HOUR_IN_MILLIS
+ val min = abs(gmtOffset) % DateUtils.HOUR_IN_MILLIS / DateUtils.MINUTE_IN_MILLIS
+
+ return if (useShortForm) {
+ String.format(Locale.ENGLISH, "%+d", hour)
+ } else {
+ String.format(Locale.ENGLISH, "GMT %+d:%02d", hour, min)
+ }
+ }
+
+ /**
+ * Given a point in time, return the subsequent moment any of the time zones changes days.
+ * e.g. Given 8:00pm on 1/1/2016 and time zones in LA and NY this method would return a Date for
+ * midnight on 1/2/2016 in the NY timezone since it changes days first.
+ *
+ * @param time a point in time from which to compute midnight on the subsequent day
+ * @param zones a collection of time zones
+ * @return the nearest point in the future at which any of the time zones changes days
+ */
+ fun getNextDay(time: Date, zones: Collection<TimeZone>): Date {
+ var next: Calendar? = null
+ for (tz in zones) {
+ val c = Calendar.getInstance(tz)
+ c.time = time
+
+ // Advance to the next day.
+ c.add(Calendar.DAY_OF_YEAR, 1)
+
+ // Reset the time to midnight.
+ c[Calendar.HOUR_OF_DAY] = 0
+ c[Calendar.MINUTE] = 0
+ c[Calendar.SECOND] = 0
+ c[Calendar.MILLISECOND] = 0
+
+ if (next == null || c < next) {
+ next = c
+ }
+ }
+
+ return next!!.time
+ }
+
+ fun getNumberFormattedQuantityString(context: Context, id: Int, quantity: Int): String {
+ val localizedQuantity = NumberFormat.getInstance().format(quantity.toLong())
+ return context.resources.getQuantityString(id, quantity, localizedQuantity)
+ }
+
+ /**
+ * @return `true` iff the widget is being hosted in a container where tapping is allowed
+ */
+ fun isWidgetClickable(widgetManager: AppWidgetManager, widgetId: Int): Boolean {
+ val wo = widgetManager.getAppWidgetOptions(widgetId)
+ return (wo != null &&
+ wo.getInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, -1)
+ != AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD)
+ }
+
+ /**
+ * @return a vector-drawable inflated from the given `resId`
+ */
+ fun getVectorDrawable(context: Context, @DrawableRes resId: Int): VectorDrawableCompat? {
+ return VectorDrawableCompat.create(context.resources, resId, context.theme)
+ }
+
+ /**
+ * This method assumes the given `view` has already been layed out.
+ *
+ * @return a Bitmap containing an image of the `view` at its current size
+ */
+ fun createBitmap(view: View): Bitmap {
+ val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+ view.draw(canvas)
+ return bitmap
+ }
+
+ /**
+ * [ArraySet] is @hide prior to [Build.VERSION_CODES.M].
+ */
+ @SuppressLint("NewApi")
+ fun <E> newArraySet(collection: Collection<E>): ArraySet<E> {
+ val arraySet = ArraySet<E>(collection.size)
+ arraySet.addAll(collection)
+ return arraySet
+ }
+
+ /**
+ * @param context from which to query the current device configuration
+ * @return `true` if the device is currently in portrait or reverse portrait orientation
+ */
+ fun isPortrait(context: Context): Boolean {
+ return context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
+ }
+
+ /**
+ * @param context from which to query the current device configuration
+ * @return `true` if the device is currently in landscape or reverse landscape orientation
+ */
+ fun isLandscape(context: Context): Boolean {
+ return context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ }
+
+ fun now(): Long = DataModel.dataModel.elapsedRealtime()
+
+ fun wallClock(): Long = DataModel.dataModel.currentTimeMillis()
+
+ /**
+ * @param context to obtain strings.
+ * @param displayMinutes whether or not minutes should be included
+ * @param isAhead `true` if the time should be marked 'ahead', else 'behind'
+ * @param hoursDifferent the number of hours the time is ahead/behind
+ * @param minutesDifferent the number of minutes the time is ahead/behind
+ * @return String describing the hours/minutes ahead or behind
+ */
+ fun createHoursDifferentString(
+ context: Context,
+ displayMinutes: Boolean,
+ isAhead: Boolean,
+ hoursDifferent: Int,
+ minutesDifferent: Int
+ ): String {
+ val timeString: String
+ timeString = if (displayMinutes && hoursDifferent != 0) {
+ // Both minutes and hours
+ val hoursShortQuantityString = getNumberFormattedQuantityString(context,
+ R.plurals.hours_short, abs(hoursDifferent))
+ val minsShortQuantityString = getNumberFormattedQuantityString(context,
+ R.plurals.minutes_short, abs(minutesDifferent))
+ @StringRes val stringType = if (isAhead) {
+ R.string.world_hours_minutes_ahead
+ } else {
+ R.string.world_hours_minutes_behind
+ }
+ context.getString(stringType, hoursShortQuantityString,
+ minsShortQuantityString)
+ } else {
+ // Minutes alone or hours alone
+ val hoursQuantityString = getNumberFormattedQuantityString(
+ context, R.plurals.hours, abs(hoursDifferent))
+ val minutesQuantityString = getNumberFormattedQuantityString(
+ context, R.plurals.minutes, abs(minutesDifferent))
+ @StringRes val stringType = if (isAhead) {
+ R.string.world_time_ahead
+ } else {
+ R.string.world_time_behind
+ }
+ context.getString(stringType, if (displayMinutes) {
+ minutesQuantityString
+ } else {
+ hoursQuantityString
+ })
+ }
+ return timeString
+ }
+
+ /**
+ * @param context The context from which to obtain strings
+ * @param hours Hours to display (if any)
+ * @param minutes Minutes to display (if any)
+ * @param seconds Seconds to display
+ * @return Provided time formatted as a String
+ */
+ fun getTimeString(context: Context, hours: Int, minutes: Int, seconds: Int): String {
+ if (hours != 0) {
+ return context.getString(R.string.hours_minutes_seconds, hours, minutes, seconds)
+ }
+ return if (minutes != 0) {
+ context.getString(R.string.minutes_seconds, minutes, seconds)
+ } else {
+ context.getString(R.string.seconds, seconds)
+ }
+ }
+
+ class ClickAccessibilityDelegate @JvmOverloads constructor(
+ /** The label for talkback to apply to the view */
+ private val mLabel: String,
+ /** Whether or not to always make the view visible to talkback */
+ private val mIsAlwaysAccessibilityVisible: Boolean = false
+ ) : AccessibilityDelegateCompat() {
+
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfoCompat
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ if (mIsAlwaysAccessibilityVisible) {
+ info.setVisibleToUser(true)
+ }
+ info.addAction(AccessibilityActionCompat(
+ AccessibilityActionCompat.ACTION_CLICK.getId(), mLabel))
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/VerticalViewPager.java b/src/com/android/deskclock/VerticalViewPager.java
deleted file mode 100644
index da630ca..0000000
--- a/src/com/android/deskclock/VerticalViewPager.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock;
-
-import android.content.Context;
-import androidx.viewpager.widget.ViewPager;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-
-public class VerticalViewPager extends ViewPager {
-
- public VerticalViewPager(Context context) {
- this(context, null);
- }
-
- public VerticalViewPager(Context context, AttributeSet attrs) {
- super(context, attrs);
- init();
- }
-
- /**
- * @return {@code false} since a vertical view pager can never be scrolled horizontally
- */
- @Override
- public boolean canScrollHorizontally(int direction) {
- return false;
- }
-
- /**
- * @return {@code true} iff a normal view pager would support horizontal scrolling at this time
- */
- @Override
- public boolean canScrollVertically(int direction) {
- return super.canScrollHorizontally(direction);
- }
-
- private void init() {
- // Make page transit vertical
- setPageTransformer(true, new VerticalPageTransformer());
- // Get rid of the overscroll drawing that happens on the left and right (the ripple)
- setOverScrollMode(View.OVER_SCROLL_NEVER);
- }
-
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- final boolean toIntercept = super.onInterceptTouchEvent(flipXY(ev));
- // Return MotionEvent to normal
- flipXY(ev);
- return toIntercept;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- final boolean toHandle = super.onTouchEvent(flipXY(ev));
- // Return MotionEvent to normal
- flipXY(ev);
- return toHandle;
- }
-
- private MotionEvent flipXY(MotionEvent ev) {
- final float width = getWidth();
- final float height = getHeight();
-
- final float x = (ev.getY() / height) * width;
- final float y = (ev.getX() / width) * height;
-
- ev.setLocation(x, y);
-
- return ev;
- }
-
- private static final class VerticalPageTransformer implements ViewPager.PageTransformer {
- @Override
- public void transformPage(View view, float position) {
- final int pageWidth = view.getWidth();
- final int pageHeight = view.getHeight();
- if (position < -1) {
- // This page is way off-screen to the left.
- view.setAlpha(0);
- } else if (position <= 1) {
- view.setAlpha(1);
- // Counteract the default slide transition
- view.setTranslationX(pageWidth * -position);
- // set Y position to swipe in from top
- float yPosition = position * pageHeight;
- view.setTranslationY(yPosition);
- } else {
- // This page is way off-screen to the right.
- view.setAlpha(0);
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/VerticalViewPager.kt b/src/com/android/deskclock/VerticalViewPager.kt
new file mode 100644
index 0000000..d399726
--- /dev/null
+++ b/src/com/android/deskclock/VerticalViewPager.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import androidx.viewpager.widget.ViewPager
+
+class VerticalViewPager @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : ViewPager(context, attrs) {
+ init {
+ init()
+ }
+
+ /**
+ * @return `false` since a vertical view pager can never be scrolled horizontally
+ */
+ override fun canScrollHorizontally(direction: Int): Boolean = false
+
+ /**
+ * @return `true` iff a normal view pager would support horizontal scrolling at this time
+ */
+ override fun canScrollVertically(direction: Int): Boolean {
+ return super.canScrollHorizontally(direction)
+ }
+
+ private fun init() {
+ // Make page transit vertical
+ setPageTransformer(true, VerticalPageTransformer())
+ // Get rid of the overscroll drawing that happens on the left and right (the ripple)
+ setOverScrollMode(View.OVER_SCROLL_NEVER)
+ }
+
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+ val toIntercept: Boolean = super.onInterceptTouchEvent(flipXY(ev))
+ // Return MotionEvent to normal
+ flipXY(ev)
+ return toIntercept
+ }
+
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
+ val toHandle: Boolean = super.onTouchEvent(flipXY(ev))
+ // Return MotionEvent to normal
+ flipXY(ev)
+ return toHandle
+ }
+
+ private fun flipXY(ev: MotionEvent): MotionEvent {
+ val width: Float = width.toFloat()
+ val height: Float = height.toFloat()
+
+ val x = ev.y / height * width
+ val y = ev.x / width * height
+
+ ev.setLocation(x, y)
+
+ return ev
+ }
+
+ private class VerticalPageTransformer : ViewPager.PageTransformer {
+ override fun transformPage(view: View, position: Float) {
+ val pageWidth = view.width
+ val pageHeight = view.height
+ when {
+ position < -1 -> {
+ // This page is way off-screen to the left.
+ view.alpha = 0f
+ }
+ position <= 1 -> {
+ view.alpha = 1f
+ // Counteract the default slide transition
+ view.translationX = pageWidth * -position
+ // set Y position to swipe in from top
+ val yPosition = position * pageHeight
+ view.translationY = yPosition
+ }
+ else -> {
+ // This page is way off-screen to the right.
+ view.alpha = 0f
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemController.java b/src/com/android/deskclock/actionbarmenu/MenuItemController.java
deleted file mode 100644
index 542a035..0000000
--- a/src/com/android/deskclock/actionbarmenu/MenuItemController.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.actionbarmenu;
-
-import android.view.Menu;
-import android.view.MenuItem;
-
-/**
- * Interface for handling a single menu item in action bar.
- */
-public interface MenuItemController {
-
- /**
- * Returns the menu item resource id that the controller manages.
- */
- int getId();
-
- /**
- * Create the menu item.
- */
- void onCreateOptionsItem(Menu menu);
-
- /**
- * Called immediately before the {@link MenuItem} is shown.
- *
- * @param item the {@link MenuItem} created by the controller
- */
- void onPrepareOptionsItem(MenuItem item);
-
- /**
- * Attempts to handle the click action.
- *
- * @param item the {@link MenuItem} that was selected
- * @return {@code true} if the action is handled by this controller
- */
- boolean onOptionsItemSelected(MenuItem item);
-}
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemController.kt b/src/com/android/deskclock/actionbarmenu/MenuItemController.kt
new file mode 100644
index 0000000..12cdcce
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemController.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.actionbarmenu
+
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * Interface for handling a single menu item in action bar.
+ */
+interface MenuItemController {
+ /**
+ * Returns the menu item resource id that the controller manages.
+ */
+ val id: Int
+
+ /**
+ * Create the menu item.
+ */
+ fun onCreateOptionsItem(menu: Menu)
+
+ /**
+ * Called immediately before the [MenuItem] is shown.
+ *
+ * @param item the [MenuItem] created by the controller
+ */
+ fun onPrepareOptionsItem(item: MenuItem)
+
+ /**
+ * Attempts to handle the click action.
+ *
+ * @param item the [MenuItem] that was selected
+ * @return `true` if the action is handled by this controller
+ */
+ fun onOptionsItemSelected(item: MenuItem): Boolean
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java
deleted file mode 100644
index 3598f16..0000000
--- a/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.actionbarmenu;
-
-import android.app.Activity;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Factory that builds optional {@link MenuItemController} instances.
- */
-public final class MenuItemControllerFactory {
-
- private static final MenuItemControllerFactory INSTANCE = new MenuItemControllerFactory();
-
- public static MenuItemControllerFactory getInstance() {
- return INSTANCE;
- }
-
- private final List<MenuItemProvider> mMenuItemProviders;
-
- private MenuItemControllerFactory() {
- mMenuItemProviders = new ArrayList<>();
- }
-
- public MenuItemControllerFactory addMenuItemProvider(MenuItemProvider provider) {
- mMenuItemProviders.add(provider);
- return this;
- }
-
- public MenuItemController[] buildMenuItemControllers(Activity activity) {
- final int providerSize = mMenuItemProviders.size();
- final MenuItemController[] controllers = new MenuItemController[providerSize];
- for (int i = 0; i < providerSize; i++) {
- controllers[i] = mMenuItemProviders.get(i).provide(activity);
- }
- return controllers;
- }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.kt b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.kt
new file mode 100644
index 0000000..97ee000
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemControllerFactory.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.actionbarmenu
+
+import android.app.Activity
+
+import kotlin.collections.ArrayList
+
+/**
+ * Factory that builds optional [MenuItemController] instances.
+ */
+object MenuItemControllerFactory {
+ private val mMenuItemProviders: MutableList<MenuItemProvider> = ArrayList()
+
+ fun buildMenuItemControllers(activity: Activity?): Array<MenuItemController?> {
+ val providerSize = mMenuItemProviders.size
+ val controllers = arrayOfNulls<MenuItemController>(providerSize)
+ for (i in 0 until providerSize) {
+ controllers[i] = mMenuItemProviders[i].provide(activity)
+ }
+ return controllers
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt
similarity index 63%
rename from src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
rename to src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt
index c3e460d..33d7878 100644
--- a/src/com/android/deskclock/actionbarmenu/MenuItemProvider.java
+++ b/src/com/android/deskclock/actionbarmenu/MenuItemProvider.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,17 +14,16 @@
* limitations under the License.
*/
-package com.android.deskclock.actionbarmenu;
+package com.android.deskclock.actionbarmenu
-import android.app.Activity;
+import android.app.Activity
/**
- * Provider for a {@link MenuItemController} instances.
+ * Provider for a [MenuItemController] instances.
*/
-public interface MenuItemProvider {
-
+interface MenuItemProvider {
/**
- * provides a {@link MenuItemController} that handles menu item.
+ * provides a [MenuItemController] that handles menu item.
*/
- MenuItemController provide(Activity activity);
-}
+ fun provide(activity: Activity?): MenuItemController?
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java
deleted file mode 100644
index b1622da..0000000
--- a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.deskclock.actionbarmenu;
-
-import android.app.Activity;
-import android.view.Menu;
-import android.view.MenuItem;
-
-/**
- * {@link MenuItemController} for handling navigation up button in actionbar. It is a special
- * menu item because it's not inflated through menu.xml, and has its own predefined id.
- */
-public final class NavUpMenuItemController implements MenuItemController {
-
- private final Activity mActivity;
-
- public NavUpMenuItemController(Activity activity) {
- mActivity = activity;
- }
-
- @Override
- public int getId() {
- return android.R.id.home;
- }
-
- @Override
- public void onCreateOptionsItem(Menu menu) {
- // "Home" option is automatically created by the Toolbar.
- }
-
- @Override
- public void onPrepareOptionsItem(MenuItem item) {
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- mActivity.finish();
- return true;
- }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt
new file mode 100644
index 0000000..3640dd4
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/NavUpMenuItemController.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.deskclock.actionbarmenu
+
+import android.app.Activity
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * [MenuItemController] for handling navigation up button in actionbar. It is a special
+ * menu item because it's not inflated through menu.xml, and has its own predefined id.
+ */
+class NavUpMenuItemController(private val activity: Activity) : MenuItemController {
+
+ override val id: Int = android.R.id.home
+
+ override fun onCreateOptionsItem(menu: Menu) {
+ // "Home" option is automatically created by the Toolbar.
+ }
+
+ override fun onPrepareOptionsItem(item: MenuItem) {
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ activity.finish()
+ return true
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java
deleted file mode 100644
index 8975221..0000000
--- a/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.actionbarmenu;
-
-import android.content.Context;
-import android.content.Intent;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import com.android.deskclock.R;
-import com.android.deskclock.ScreensaverActivity;
-import com.android.deskclock.events.Events;
-
-import static android.view.Menu.NONE;
-
-/**
- * {@link MenuItemController} for controlling night mode display.
- */
-public final class NightModeMenuItemController implements MenuItemController {
-
- private static final int NIGHT_MODE_MENU_RES_ID = R.id.menu_item_night_mode;
-
- private final Context mContext;
-
- public NightModeMenuItemController(Context context) {
- mContext = context;
- }
-
- @Override
- public int getId() {
- return NIGHT_MODE_MENU_RES_ID;
- }
-
- @Override
- public void onCreateOptionsItem(Menu menu) {
- menu.add(NONE, NIGHT_MODE_MENU_RES_ID, NONE, R.string.menu_item_night_mode)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- }
-
- @Override
- public void onPrepareOptionsItem(MenuItem item) {
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- mContext.startActivity(new Intent(mContext, ScreensaverActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock));
- return true;
- }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.kt
new file mode 100644
index 0000000..d8f2dd4
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/NightModeMenuItemController.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.actionbarmenu
+
+import android.content.Context
+import android.content.Intent
+import android.view.Menu
+import android.view.Menu.NONE
+import android.view.MenuItem
+
+import com.android.deskclock.R
+import com.android.deskclock.ScreensaverActivity
+import com.android.deskclock.events.Events
+
+/**
+ * [MenuItemController] for controlling night mode display.
+ */
+class NightModeMenuItemController(private val context: Context) : MenuItemController {
+
+ override val id: Int = R.id.menu_item_night_mode
+
+ override fun onCreateOptionsItem(menu: Menu) {
+ menu.add(NONE, id, NONE, R.string.menu_item_night_mode)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+ }
+
+ override fun onPrepareOptionsItem(item: MenuItem) {
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ context.startActivity(Intent(context, ScreensaverActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_deskclock))
+ return true
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.java b/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.java
deleted file mode 100644
index 4d66e2d..0000000
--- a/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.actionbarmenu;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Activity scoped singleton that manages action bar menus. Each menu item is controlled by a
- * {@link MenuItemController} instance.
- */
-public final class OptionsMenuManager {
-
- private final List<MenuItemController> mControllers = new ArrayList<>();
-
- /**
- * Add one or more {@link MenuItemController} to the actionbar menu.
- * <p/>
- * This should be called in {@link Activity#onCreate(Bundle)}.
- */
- public OptionsMenuManager addMenuItemController(MenuItemController... controllers) {
- Collections.addAll(mControllers, controllers);
- return this;
- }
-
- /**
- * Inflates {@link Menu} for the activity.
- * <p/>
- * This method should be called during {@link Activity#onCreateOptionsMenu(Menu)}.
- */
- public void onCreateOptionsMenu(Menu menu) {
- for (MenuItemController controller : mControllers) {
- controller.onCreateOptionsItem(menu);
- }
- }
-
- /**
- * Prepares the popup to displays all required menu items.
- * <p/>
- * This method should be called during {@link Activity#onPrepareOptionsMenu(Menu)} (Menu)}.
- */
- public void onPrepareOptionsMenu(Menu menu) {
- for (MenuItemController controller : mControllers) {
- final MenuItem menuItem = menu.findItem(controller.getId());
- if (menuItem != null) {
- controller.onPrepareOptionsItem(menuItem);
- }
- }
- }
-
- /**
- * Handles click action for a menu item.
- * <p/>
- * This method should be called during {@link Activity#onOptionsItemSelected(MenuItem)}.
- */
- public boolean onOptionsItemSelected(MenuItem item) {
- final int itemId = item.getItemId();
- for (MenuItemController controller : mControllers) {
- if (controller.getId() == itemId
- && controller.onOptionsItemSelected(item)) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.kt b/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.kt
new file mode 100644
index 0000000..ec44544
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/OptionsMenuManager.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.actionbarmenu
+
+import android.app.Activity
+import android.view.Menu
+import android.view.MenuItem
+
+/**
+ * Activity scoped singleton that manages action bar menus. Each menu item is controlled by a
+ * [MenuItemController] instance.
+ */
+class OptionsMenuManager {
+
+ private val mControllers: MutableList<MenuItemController?> = ArrayList()
+
+ /**
+ * Add one or more [MenuItemController] to the actionbar menu.
+ *
+ * This should be called in [Activity.onCreate].
+ */
+ fun addMenuItemController(vararg controllers: MenuItemController?): OptionsMenuManager {
+ mControllers.addAll(controllers)
+ return this
+ }
+
+ /**
+ * Inflates [Menu] for the activity.
+ *
+ * This method should be called during [Activity.onCreateOptionsMenu].
+ */
+ fun onCreateOptionsMenu(menu: Menu) {
+ for (controller in mControllers) {
+ controller?.onCreateOptionsItem(menu)
+ }
+ }
+
+ /**
+ * Prepares the popup to displays all required menu items.
+ *
+ * This method should be called during [Activity.onPrepareOptionsMenu] (Menu)}.
+ */
+ fun onPrepareOptionsMenu(menu: Menu) {
+ for (controller in mControllers) {
+ controller?.let {
+ val menuItem: MenuItem? = menu.findItem(controller.id)
+ if (menuItem != null) {
+ controller.onPrepareOptionsItem(menuItem)
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles click action for a menu item.
+ *
+ * This method should be called during [Activity.onOptionsItemSelected].
+ */
+ fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val itemId: Int = item.getItemId()
+ for (controller in mControllers) {
+ if (controller?.id == itemId && controller.onOptionsItemSelected(item)) {
+ return true
+ }
+ }
+ return false
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java
deleted file mode 100644
index 3777a47..0000000
--- a/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.deskclock.actionbarmenu;
-
-import android.content.Context;
-import android.os.Bundle;
-import androidx.appcompat.widget.SearchView;
-import androidx.appcompat.widget.SearchView.OnQueryTextListener;
-import android.text.InputType;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-
-import com.android.deskclock.R;
-
-import static android.view.Menu.FIRST;
-import static android.view.Menu.NONE;
-
-/**
- * {@link MenuItemController} for search menu.
- */
-public final class SearchMenuItemController implements MenuItemController {
-
- private static final String KEY_SEARCH_QUERY = "search_query";
- private static final String KEY_SEARCH_MODE = "search_mode";
-
- private static final int SEARCH_MENU_RES_ID = R.id.menu_item_search;
-
- private final Context mContext;
- private final SearchView.OnQueryTextListener mQueryListener;
- private final SearchModeChangeListener mSearchModeChangeListener;
-
- private String mQuery = "";
- private boolean mSearchMode;
-
- public SearchMenuItemController(Context context, OnQueryTextListener queryListener,
- Bundle savedState) {
- mContext = context;
- mSearchModeChangeListener = new SearchModeChangeListener();
- mQueryListener = queryListener;
-
- if (savedState != null) {
- mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE, false);
- mQuery = savedState.getString(KEY_SEARCH_QUERY, "");
- }
- }
-
- public void saveInstance(Bundle outState) {
- outState.putString(KEY_SEARCH_QUERY, mQuery);
- outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
- }
-
- @Override
- public int getId() {
- return SEARCH_MENU_RES_ID;
- }
-
- @Override
- public void onCreateOptionsItem(Menu menu) {
- final SearchView searchView = new SearchView(mContext);
- searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
- searchView.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_WORDS);
- searchView.setQuery(mQuery, false);
- searchView.setOnCloseListener(mSearchModeChangeListener);
- searchView.setOnSearchClickListener(mSearchModeChangeListener);
- searchView.setOnQueryTextListener(mQueryListener);
-
- menu.add(NONE, SEARCH_MENU_RES_ID, FIRST, android.R.string.search_go)
- .setActionView(searchView)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
-
- if (mSearchMode) {
- searchView.requestFocus();
- searchView.setIconified(false);
- }
- }
-
- @Override
- public void onPrepareOptionsItem(MenuItem item) {
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // The search view is handled by {@link #mSearchListener}. Skip handling here.
- return false;
- }
-
- public String getQueryText() {
- return mQuery;
- }
-
- public void setQueryText(String query) {
- mQuery = query;
- }
-
- /**
- * Listener for user actions on search view.
- */
- private final class SearchModeChangeListener implements View.OnClickListener,
- SearchView.OnCloseListener {
- @Override
- public void onClick(View v) {
- mSearchMode = true;
- }
-
- @Override
- public boolean onClose() {
- mSearchMode = false;
- return false;
- }
- }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.kt
new file mode 100644
index 0000000..7c819f8
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/SearchMenuItemController.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.deskclock.actionbarmenu
+
+import android.content.Context
+import android.os.Bundle
+import android.text.InputType
+import android.view.Menu
+import android.view.Menu.FIRST
+import android.view.Menu.NONE
+import android.view.MenuItem
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import androidx.appcompat.widget.SearchView
+import androidx.appcompat.widget.SearchView.OnQueryTextListener
+
+import com.android.deskclock.R
+
+/**
+ * [MenuItemController] for search menu.
+ */
+class SearchMenuItemController(
+ private val context: Context,
+ private val queryListener: OnQueryTextListener,
+ savedState: Bundle?
+) : MenuItemController {
+
+ override val id: Int = R.id.menu_item_search
+
+ private val mSearchModeChangeListener: SearchModeChangeListener = SearchModeChangeListener()
+
+ var queryText = ""
+ private var mSearchMode = false
+
+ init {
+ if (savedState != null) {
+ mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE, false)
+ queryText = savedState.getString(KEY_SEARCH_QUERY, "")
+ }
+ }
+
+ fun saveInstance(outState: Bundle) {
+ outState.putString(KEY_SEARCH_QUERY, queryText)
+ outState.putBoolean(KEY_SEARCH_MODE, mSearchMode)
+ }
+
+ override fun onCreateOptionsItem(menu: Menu) {
+ val searchView = SearchView(context)
+ searchView.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI)
+ searchView.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_WORDS)
+ searchView.setQuery(queryText, false)
+ searchView.setOnCloseListener(mSearchModeChangeListener)
+ searchView.setOnSearchClickListener(mSearchModeChangeListener)
+ searchView.setOnQueryTextListener(queryListener)
+
+ menu.add(NONE, id, FIRST, android.R.string.search_go)
+ .setActionView(searchView)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
+
+ if (mSearchMode) {
+ searchView.requestFocus()
+ searchView.setIconified(false)
+ }
+ }
+
+ override fun onPrepareOptionsItem(item: MenuItem) {
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ // The search view is handled by {@link #mSearchListener}. Skip handling here.
+ return false
+ }
+
+ /**
+ * Listener for user actions on search view.
+ */
+ private inner class SearchModeChangeListener
+ : View.OnClickListener, SearchView.OnCloseListener {
+
+ override fun onClick(v: View?) {
+ mSearchMode = true
+ }
+
+ override fun onClose(): Boolean {
+ mSearchMode = false
+ return false
+ }
+ }
+
+ companion object {
+ private const val KEY_SEARCH_QUERY = "search_query"
+ private const val KEY_SEARCH_MODE = "search_mode"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.java b/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.java
deleted file mode 100644
index ecf442e..0000000
--- a/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.actionbarmenu;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import com.android.deskclock.R;
-import com.android.deskclock.settings.SettingsActivity;
-
-import static android.view.Menu.NONE;
-
-/**
- * {@link MenuItemController} for settings menu.
- */
-public final class SettingsMenuItemController implements MenuItemController {
-
- public static final int REQUEST_CHANGE_SETTINGS = 1;
-
- private static final int SETTING_MENU_RES_ID = R.id.menu_item_settings;
-
- private final Activity mActivity;
-
- public SettingsMenuItemController(Activity activity) {
- mActivity = activity;
- }
-
- @Override
- public int getId() {
- return SETTING_MENU_RES_ID;
- }
-
- @Override
- public void onCreateOptionsItem(Menu menu) {
- menu.add(NONE, SETTING_MENU_RES_ID, NONE, R.string.menu_item_settings)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- }
-
- @Override
- public void onPrepareOptionsItem(MenuItem item) {
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- final Intent settingIntent = new Intent(mActivity, SettingsActivity.class);
- mActivity.startActivityForResult(settingIntent, REQUEST_CHANGE_SETTINGS);
- return true;
- }
-}
diff --git a/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.kt b/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.kt
new file mode 100644
index 0000000..7204a24
--- /dev/null
+++ b/src/com/android/deskclock/actionbarmenu/SettingsMenuItemController.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.actionbarmenu
+
+import android.app.Activity
+import android.content.Intent
+import android.view.Menu
+import android.view.Menu.NONE
+import android.view.MenuItem
+
+import com.android.deskclock.R
+import com.android.deskclock.settings.SettingsActivity
+
+/**
+ * [MenuItemController] for settings menu.
+ */
+class SettingsMenuItemController(private val activity: Activity) : MenuItemController {
+
+ override val id: Int = R.id.menu_item_settings
+
+ override fun onCreateOptionsItem(menu: Menu) {
+ menu.add(NONE, id, NONE, R.string.menu_item_settings)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+ }
+
+ override fun onPrepareOptionsItem(item: MenuItem) {
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ val settingIntent = Intent(activity, SettingsActivity::class.java)
+ activity.startActivityForResult(settingIntent, REQUEST_CHANGE_SETTINGS)
+ return true
+ }
+
+ companion object {
+ const val REQUEST_CHANGE_SETTINGS = 1
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmActivity.java b/src/com/android/deskclock/alarms/AlarmActivity.java
deleted file mode 100644
index 88632c7..0000000
--- a/src/com/android/deskclock/alarms/AlarmActivity.java
+++ /dev/null
@@ -1,662 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.alarms;
-
-import android.accessibilityservice.AccessibilityServiceInfo;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.content.BroadcastReceiver;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.ActivityInfo;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
-import android.media.AudioManager;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import androidx.annotation.NonNull;
-import androidx.core.graphics.ColorUtils;
-import androidx.core.view.animation.PathInterpolatorCompat;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.ImageView;
-import android.widget.TextClock;
-import android.widget.TextView;
-
-import com.android.deskclock.AnimatorUtils;
-import com.android.deskclock.BaseActivity;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.widget.CircleView;
-
-import java.util.List;
-
-import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
-
-public class AlarmActivity extends BaseActivity
- implements View.OnClickListener, View.OnTouchListener {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmActivity");
-
- private static final TimeInterpolator PULSE_INTERPOLATOR =
- PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f);
- private static final TimeInterpolator REVEAL_INTERPOLATOR =
- PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f);
-
- private static final int PULSE_DURATION_MILLIS = 1000;
- private static final int ALARM_BOUNCE_DURATION_MILLIS = 500;
- private static final int ALERT_REVEAL_DURATION_MILLIS = 500;
- private static final int ALERT_FADE_DURATION_MILLIS = 500;
- private static final int ALERT_DISMISS_DELAY_MILLIS = 2000;
-
- private static final float BUTTON_SCALE_DEFAULT = 0.7f;
- private static final int BUTTON_DRAWABLE_ALPHA_DEFAULT = 165;
-
- private final Handler mHandler = new Handler();
- private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- LOGGER.v("Received broadcast: %s", action);
-
- if (!mAlarmHandled) {
- switch (action) {
- case AlarmService.ALARM_SNOOZE_ACTION:
- snooze();
- break;
- case AlarmService.ALARM_DISMISS_ACTION:
- dismiss();
- break;
- case AlarmService.ALARM_DONE_ACTION:
- finish();
- break;
- default:
- LOGGER.i("Unknown broadcast: %s", action);
- break;
- }
- } else {
- LOGGER.v("Ignored broadcast: %s", action);
- }
- }
- };
-
- private final ServiceConnection mConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- LOGGER.i("Finished binding to AlarmService");
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- LOGGER.i("Disconnected from AlarmService");
- }
- };
-
- private AlarmInstance mAlarmInstance;
- private boolean mAlarmHandled;
- private AlarmVolumeButtonBehavior mVolumeBehavior;
- private int mCurrentHourColor;
- private boolean mReceiverRegistered;
- /** Whether the AlarmService is currently bound */
- private boolean mServiceBound;
-
- private AccessibilityManager mAccessibilityManager;
-
- private ViewGroup mAlertView;
- private TextView mAlertTitleView;
- private TextView mAlertInfoView;
-
- private ViewGroup mContentView;
- private ImageView mAlarmButton;
- private ImageView mSnoozeButton;
- private ImageView mDismissButton;
- private TextView mHintView;
-
- private ValueAnimator mAlarmAnimator;
- private ValueAnimator mSnoozeAnimator;
- private ValueAnimator mDismissAnimator;
- private ValueAnimator mPulseAnimator;
-
- private int mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setVolumeControlStream(AudioManager.STREAM_ALARM);
- final long instanceId = AlarmInstance.getId(getIntent().getData());
- mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId);
- if (mAlarmInstance == null) {
- // The alarm was deleted before the activity got created, so just finish()
- LOGGER.e("Error displaying alarm for intent: %s", getIntent());
- finish();
- return;
- } else if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) {
- LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance);
- finish();
- return;
- }
-
- LOGGER.i("Displaying alarm for instance: %s", mAlarmInstance);
-
- // Get the volume/camera button behavior setting
- mVolumeBehavior = DataModel.getDataModel().getAlarmVolumeButtonBehavior();
-
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
- | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
- | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);
-
- // Hide navigation bar to minimize accidental tap on Home key
- hideNavigationBar();
-
- // Close dialogs and window shade, so this is fully visible
- sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-
- // Honor rotation on tablets; fix the orientation on phones.
- if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
- }
-
- mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
-
- setContentView(R.layout.alarm_activity);
-
- mAlertView = (ViewGroup) findViewById(R.id.alert);
- mAlertTitleView = (TextView) mAlertView.findViewById(R.id.alert_title);
- mAlertInfoView = (TextView) mAlertView.findViewById(R.id.alert_info);
-
- mContentView = (ViewGroup) findViewById(R.id.content);
- mAlarmButton = (ImageView) mContentView.findViewById(R.id.alarm);
- mSnoozeButton = (ImageView) mContentView.findViewById(R.id.snooze);
- mDismissButton = (ImageView) mContentView.findViewById(R.id.dismiss);
- mHintView = (TextView) mContentView.findViewById(R.id.hint);
-
- final TextView titleView = (TextView) mContentView.findViewById(R.id.title);
- final TextClock digitalClock = (TextClock) mContentView.findViewById(R.id.digital_clock);
- final CircleView pulseView = (CircleView) mContentView.findViewById(R.id.pulse);
-
- titleView.setText(mAlarmInstance.getLabelOrDefault(this));
- Utils.setTimeFormat(digitalClock, false);
-
- mCurrentHourColor = ThemeUtils.resolveColor(this, android.R.attr.windowBackground);
- getWindow().setBackgroundDrawable(new ColorDrawable(mCurrentHourColor));
-
- mAlarmButton.setOnTouchListener(this);
- mSnoozeButton.setOnClickListener(this);
- mDismissButton.setOnClickListener(this);
-
- mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f);
- mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE);
- mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor);
- mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView,
- PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.getRadius()),
- PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR,
- ColorUtils.setAlphaComponent(pulseView.getFillColor(), 0)));
- mPulseAnimator.setDuration(PULSE_DURATION_MILLIS);
- mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR);
- mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
- mPulseAnimator.start();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- // Re-query for AlarmInstance in case the state has changed externally
- final long instanceId = AlarmInstance.getId(getIntent().getData());
- mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId);
-
- if (mAlarmInstance == null) {
- LOGGER.i("No alarm instance for instanceId: %d", instanceId);
- finish();
- return;
- }
-
- // Verify that the alarm is still firing before showing the activity
- if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) {
- LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance);
- finish();
- return;
- }
-
- if (!mReceiverRegistered) {
- // Register to get the alarm done/snooze/dismiss intent.
- final IntentFilter filter = new IntentFilter(AlarmService.ALARM_DONE_ACTION);
- filter.addAction(AlarmService.ALARM_SNOOZE_ACTION);
- filter.addAction(AlarmService.ALARM_DISMISS_ACTION);
- registerReceiver(mReceiver, filter);
- mReceiverRegistered = true;
- }
-
- bindAlarmService();
-
- resetAnimations();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
-
- unbindAlarmService();
-
- // Skip if register didn't happen to avoid IllegalArgumentException
- if (mReceiverRegistered) {
- unregisterReceiver(mReceiver);
- mReceiverRegistered = false;
- }
- }
-
- @Override
- public boolean dispatchKeyEvent(@NonNull KeyEvent keyEvent) {
- // Do this in dispatch to intercept a few of the system keys.
- LOGGER.v("dispatchKeyEvent: %s", keyEvent);
-
- final int keyCode = keyEvent.getKeyCode();
- switch (keyCode) {
- // Volume keys and camera keys dismiss the alarm.
- case KeyEvent.KEYCODE_VOLUME_UP:
- case KeyEvent.KEYCODE_VOLUME_DOWN:
- case KeyEvent.KEYCODE_VOLUME_MUTE:
- case KeyEvent.KEYCODE_HEADSETHOOK:
- case KeyEvent.KEYCODE_CAMERA:
- case KeyEvent.KEYCODE_FOCUS:
- if (!mAlarmHandled) {
- switch (mVolumeBehavior) {
- case SNOOZE:
- if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
- snooze();
- }
- return true;
- case DISMISS:
- if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
- dismiss();
- }
- return true;
- }
- }
- }
- return super.dispatchKeyEvent(keyEvent);
- }
-
- @Override
- public void onBackPressed() {
- // Don't allow back to dismiss.
- }
-
- @Override
- public void onClick(View view) {
- if (mAlarmHandled) {
- LOGGER.v("onClick ignored: %s", view);
- return;
- }
- LOGGER.v("onClick: %s", view);
-
- // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons.
- if (isAccessibilityEnabled()) {
- if (view == mSnoozeButton) {
- snooze();
- } else if (view == mDismissButton) {
- dismiss();
- }
- return;
- }
-
- if (view == mSnoozeButton) {
- hintSnooze();
- } else if (view == mDismissButton) {
- hintDismiss();
- }
- }
-
- @Override
- public boolean onTouch(View view, MotionEvent event) {
- if (mAlarmHandled) {
- LOGGER.v("onTouch ignored: %s", event);
- return false;
- }
-
- final int action = event.getActionMasked();
- if (action == MotionEvent.ACTION_DOWN) {
- LOGGER.v("onTouch started: %s", event);
-
- // Track the pointer that initiated the touch sequence.
- mInitialPointerIndex = event.getPointerId(event.getActionIndex());
-
- // Stop the pulse, allowing the last pulse to finish.
- mPulseAnimator.setRepeatCount(0);
- } else if (action == MotionEvent.ACTION_CANCEL) {
- LOGGER.v("onTouch canceled: %s", event);
-
- // Clear the pointer index.
- mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
-
- // Reset everything.
- resetAnimations();
- }
-
- final int actionIndex = event.getActionIndex();
- if (mInitialPointerIndex == MotionEvent.INVALID_POINTER_ID
- || mInitialPointerIndex != event.getPointerId(actionIndex)) {
- // Ignore any pointers other than the initial one, bail early.
- return true;
- }
-
- final int[] contentLocation = {0, 0};
- mContentView.getLocationOnScreen(contentLocation);
-
- final float x = event.getRawX() - contentLocation[0];
- final float y = event.getRawY() - contentLocation[1];
-
- final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
- final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
-
- final float snoozeFraction, dismissFraction;
- if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
- snoozeFraction = getFraction(alarmRight, mSnoozeButton.getLeft(), x);
- dismissFraction = getFraction(alarmLeft, mDismissButton.getRight(), x);
- } else {
- snoozeFraction = getFraction(alarmLeft, mSnoozeButton.getRight(), x);
- dismissFraction = getFraction(alarmRight, mDismissButton.getLeft(), x);
- }
- setAnimatedFractions(snoozeFraction, dismissFraction);
-
- if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
- LOGGER.v("onTouch ended: %s", event);
-
- mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID;
- if (snoozeFraction == 1.0f) {
- snooze();
- } else if (dismissFraction == 1.0f) {
- dismiss();
- } else {
- if (snoozeFraction > 0.0f || dismissFraction > 0.0f) {
- // Animate back to the initial state.
- AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator);
- } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) {
- // User touched the alarm button, hint the dismiss action.
- hintDismiss();
- }
-
- // Restart the pulse.
- mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
- if (!mPulseAnimator.isStarted()) {
- mPulseAnimator.start();
- }
- }
- }
-
- return true;
- }
-
- private void hideNavigationBar() {
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
- }
-
- /**
- * Returns {@code true} if accessibility is enabled, to enable alternate behavior for click
- * handling, etc.
- */
- private boolean isAccessibilityEnabled() {
- if (mAccessibilityManager == null || !mAccessibilityManager.isEnabled()) {
- // Accessibility is unavailable or disabled.
- return false;
- } else if (mAccessibilityManager.isTouchExplorationEnabled()) {
- // TalkBack's touch exploration mode is enabled.
- return true;
- }
-
- // Check if "Switch Access" is enabled.
- final List<AccessibilityServiceInfo> enabledAccessibilityServices =
- mAccessibilityManager.getEnabledAccessibilityServiceList(FEEDBACK_GENERIC);
- return !enabledAccessibilityServices.isEmpty();
- }
-
- private void hintSnooze() {
- final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
- final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
- final float translationX = Math.max(mSnoozeButton.getLeft() - alarmRight, 0)
- + Math.min(mSnoozeButton.getRight() - alarmLeft, 0);
- getAlarmBounceAnimator(translationX, translationX < 0.0f ?
- R.string.description_direction_left : R.string.description_direction_right).start();
- }
-
- private void hintDismiss() {
- final int alarmLeft = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft();
- final int alarmRight = mAlarmButton.getRight() - mAlarmButton.getPaddingRight();
- final float translationX = Math.max(mDismissButton.getLeft() - alarmRight, 0)
- + Math.min(mDismissButton.getRight() - alarmLeft, 0);
- getAlarmBounceAnimator(translationX, translationX < 0.0f ?
- R.string.description_direction_left : R.string.description_direction_right).start();
- }
-
- /**
- * Set animators to initial values and restart pulse on alarm button.
- */
- private void resetAnimations() {
- // Set the animators to their initial values.
- setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
- // Restart the pulse.
- mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
- if (!mPulseAnimator.isStarted()) {
- mPulseAnimator.start();
- }
- }
-
- /**
- * Perform snooze animation and send snooze intent.
- */
- private void snooze() {
- mAlarmHandled = true;
- LOGGER.v("Snoozed: %s", mAlarmInstance);
-
- final int colorAccent = ThemeUtils.resolveColor(this, R.attr.colorAccent);
- setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */);
-
- final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength();
- final String infoText = getResources().getQuantityString(
- R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes);
- final String accessibilityText = getResources().getQuantityString(
- R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes);
-
- getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText,
- accessibilityText, colorAccent, colorAccent).start();
-
- AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */);
-
- Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock);
-
- // Unbind here, otherwise alarm will keep ringing until activity finishes.
- unbindAlarmService();
- }
-
- /**
- * Perform dismiss animation and send dismiss intent.
- */
- private void dismiss() {
- mAlarmHandled = true;
- LOGGER.v("Dismissed: %s", mAlarmInstance);
-
- setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */);
-
- getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */,
- getString(R.string.alarm_alert_off_text) /* accessibilityText */,
- Color.WHITE, mCurrentHourColor).start();
-
- AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance);
-
- Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock);
-
- // Unbind here, otherwise alarm will keep ringing until activity finishes.
- unbindAlarmService();
- }
-
- /**
- * Bind AlarmService if not yet bound.
- */
- private void bindAlarmService() {
- if (!mServiceBound) {
- final Intent intent = new Intent(this, AlarmService.class);
- bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
- mServiceBound = true;
- }
- }
-
- /**
- * Unbind AlarmService if bound.
- */
- private void unbindAlarmService() {
- if (mServiceBound) {
- unbindService(mConnection);
- mServiceBound = false;
- }
- }
-
- private void setAnimatedFractions(float snoozeFraction, float dismissFraction) {
- final float alarmFraction = Math.max(snoozeFraction, dismissFraction);
- AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction);
- AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction);
- AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction);
- }
-
- private float getFraction(float x0, float x1, float x) {
- return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f);
- }
-
- private ValueAnimator getButtonAnimator(ImageView button, int tintColor) {
- return ObjectAnimator.ofPropertyValuesHolder(button,
- PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f),
- PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f),
- PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255),
- PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA,
- BUTTON_DRAWABLE_ALPHA_DEFAULT, 255),
- PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT,
- AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor));
- }
-
- private ValueAnimator getAlarmBounceAnimator(float translationX, final int hintResId) {
- final ValueAnimator bounceAnimator = ObjectAnimator.ofFloat(mAlarmButton,
- View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f);
- bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR);
- bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS);
- bounceAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- mHintView.setText(hintResId);
- if (mHintView.getVisibility() != View.VISIBLE) {
- mHintView.setVisibility(View.VISIBLE);
- ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start();
- }
- }
- });
- return bounceAnimator;
- }
-
- private Animator getAlertAnimator(final View source, final int titleResId,
- final String infoText, final String accessibilityText, final int revealColor,
- final int backgroundColor) {
- final ViewGroup containerView = (ViewGroup) findViewById(android.R.id.content);
-
- final Rect sourceBounds = new Rect(0, 0, source.getHeight(), source.getWidth());
- containerView.offsetDescendantRectToMyCoords(source, sourceBounds);
-
- final int centerX = sourceBounds.centerX();
- final int centerY = sourceBounds.centerY();
-
- final int xMax = Math.max(centerX, containerView.getWidth() - centerX);
- final int yMax = Math.max(centerY, containerView.getHeight() - centerY);
-
- final float startRadius = Math.max(sourceBounds.width(), sourceBounds.height()) / 2.0f;
- final float endRadius = (float) Math.sqrt(xMax * xMax + yMax * yMax);
-
- final CircleView revealView = new CircleView(this)
- .setCenterX(centerX)
- .setCenterY(centerY)
- .setFillColor(revealColor);
- containerView.addView(revealView);
-
- // TODO: Fade out source icon over the reveal (like LOLLIPOP version).
-
- final Animator revealAnimator = ObjectAnimator.ofFloat(
- revealView, CircleView.RADIUS, startRadius, endRadius);
- revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS);
- revealAnimator.setInterpolator(REVEAL_INTERPOLATOR);
- revealAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mAlertView.setVisibility(View.VISIBLE);
- mAlertTitleView.setText(titleResId);
-
- if (infoText != null) {
- mAlertInfoView.setText(infoText);
- mAlertInfoView.setVisibility(View.VISIBLE);
- }
- mContentView.setVisibility(View.GONE);
-
- getWindow().setBackgroundDrawable(new ColorDrawable(backgroundColor));
- }
- });
-
- final ValueAnimator fadeAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f);
- fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS);
- fadeAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- containerView.removeView(revealView);
- }
- });
-
- final AnimatorSet alertAnimator = new AnimatorSet();
- alertAnimator.play(revealAnimator).before(fadeAnimator);
- alertAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mAlertView.announceForAccessibility(accessibilityText);
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- finish();
- }
- }, ALERT_DISMISS_DELAY_MILLIS);
- }
- });
-
- return alertAnimator;
- }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmActivity.kt b/src/com/android/deskclock/alarms/AlarmActivity.kt
new file mode 100644
index 0000000..4061fc6
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmActivity.kt
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.accessibilityservice.AccessibilityServiceInfo
+import android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.animation.TimeInterpolator
+import android.animation.ValueAnimator
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.ServiceConnection
+import android.content.pm.ActivityInfo
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.ColorDrawable
+import android.media.AudioManager
+import android.os.Bundle
+import android.os.Handler
+import android.os.IBinder
+import android.os.Looper
+import android.view.KeyEvent
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.view.accessibility.AccessibilityManager
+import android.widget.ImageView
+import android.widget.TextClock
+import android.widget.TextView
+import androidx.core.graphics.ColorUtils
+import androidx.core.view.animation.PathInterpolatorCompat
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.LogUtils
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.widget.CircleView
+
+import kotlin.math.max
+import kotlin.math.sqrt
+
+class AlarmActivity : BaseActivity(), View.OnClickListener, View.OnTouchListener {
+ private val mHandler: Handler = Handler(Looper.myLooper()!!)
+
+ private val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ val action: String? = intent.getAction()
+ LOGGER.v("Received broadcast: %s", action)
+
+ if (!mAlarmHandled) {
+ when (action) {
+ AlarmService.ALARM_SNOOZE_ACTION -> snooze()
+ AlarmService.ALARM_DISMISS_ACTION -> dismiss()
+ AlarmService.ALARM_DONE_ACTION -> finish()
+ else -> LOGGER.i("Unknown broadcast: %s", action)
+ }
+ } else {
+ LOGGER.v("Ignored broadcast: %s", action)
+ }
+ }
+ }
+
+ private val mConnection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ LOGGER.i("Finished binding to AlarmService")
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ LOGGER.i("Disconnected from AlarmService")
+ }
+ }
+
+ private var mAlarmInstance: AlarmInstance? = null
+ private var mAlarmHandled = false
+ private var mVolumeBehavior: AlarmVolumeButtonBehavior? = null
+ private var mCurrentHourColor = 0
+ private var mReceiverRegistered = false
+ /** Whether the AlarmService is currently bound */
+ private var mServiceBound = false
+
+ private var mAccessibilityManager: AccessibilityManager? = null
+
+ private lateinit var mAlertView: ViewGroup
+ private lateinit var mAlertTitleView: TextView
+ private lateinit var mAlertInfoView: TextView
+
+ private lateinit var mContentView: ViewGroup
+ private lateinit var mAlarmButton: ImageView
+ private lateinit var mSnoozeButton: ImageView
+ private lateinit var mDismissButton: ImageView
+ private lateinit var mHintView: TextView
+
+ private lateinit var mAlarmAnimator: ValueAnimator
+ private lateinit var mSnoozeAnimator: ValueAnimator
+ private lateinit var mDismissAnimator: ValueAnimator
+ private lateinit var mPulseAnimator: ValueAnimator
+
+ private var mInitialPointerIndex: Int = MotionEvent.INVALID_POINTER_ID
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setVolumeControlStream(AudioManager.STREAM_ALARM)
+ val instanceId = AlarmInstance.getId(getIntent().getData()!!)
+ mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId)
+ if (mAlarmInstance == null) {
+ // The alarm was deleted before the activity got created, so just finish()
+ LOGGER.e("Error displaying alarm for intent: %s", getIntent())
+ finish()
+ return
+ } else if (mAlarmInstance!!.mAlarmState != InstancesColumns.FIRED_STATE) {
+ LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance)
+ finish()
+ return
+ }
+
+ LOGGER.i("Displaying alarm for instance: %s", mAlarmInstance)
+
+ // Get the volume/camera button behavior setting
+ mVolumeBehavior = DataModel.dataModel.alarmVolumeButtonBehavior
+
+ if (Utils.isOOrLater) {
+ setShowWhenLocked(true)
+ setTurnScreenOn(true)
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
+ } else {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
+ or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
+ }
+
+ // Hide navigation bar to minimize accidental tap on Home key
+ hideNavigationBar()
+
+ // Close dialogs and window shade, so this is fully visible
+ sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+
+ // Honor rotation on tablets; fix the orientation on phones.
+ if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR)
+ }
+
+ mAccessibilityManager = getSystemService(ACCESSIBILITY_SERVICE) as AccessibilityManager?
+
+ setContentView(R.layout.alarm_activity)
+
+ mAlertView = findViewById(R.id.alert) as ViewGroup
+ mAlertTitleView = mAlertView.findViewById(R.id.alert_title) as TextView
+ mAlertInfoView = mAlertView.findViewById(R.id.alert_info) as TextView
+
+ mContentView = findViewById(R.id.content) as ViewGroup
+ mAlarmButton = mContentView.findViewById(R.id.alarm) as ImageView
+ mSnoozeButton = mContentView.findViewById(R.id.snooze) as ImageView
+ mDismissButton = mContentView.findViewById(R.id.dismiss) as ImageView
+ mHintView = mContentView.findViewById(R.id.hint) as TextView
+
+ val titleView: TextView = mContentView.findViewById(R.id.title) as TextView
+ val digitalClock: TextClock = mContentView.findViewById(R.id.digital_clock) as TextClock
+ val pulseView = mContentView.findViewById(R.id.pulse) as CircleView
+
+ titleView.setText(mAlarmInstance!!.getLabelOrDefault(this))
+ Utils.setTimeFormat(digitalClock, false)
+
+ mCurrentHourColor = ThemeUtils.resolveColor(this, android.R.attr.windowBackground)
+ getWindow().setBackgroundDrawable(ColorDrawable(mCurrentHourColor))
+
+ mAlarmButton.setOnTouchListener(this)
+ mSnoozeButton.setOnClickListener(this)
+ mDismissButton.setOnClickListener(this)
+
+ mAlarmAnimator = AnimatorUtils.getScaleAnimator(mAlarmButton, 1.0f, 0.0f)
+ mSnoozeAnimator = getButtonAnimator(mSnoozeButton, Color.WHITE)
+ mDismissAnimator = getButtonAnimator(mDismissButton, mCurrentHourColor)
+ mPulseAnimator = ObjectAnimator.ofPropertyValuesHolder(pulseView,
+ PropertyValuesHolder.ofFloat(CircleView.RADIUS, 0.0f, pulseView.radius),
+ PropertyValuesHolder.ofObject(CircleView.FILL_COLOR, AnimatorUtils.ARGB_EVALUATOR,
+ ColorUtils.setAlphaComponent(pulseView.fillColor, 0)))
+ mPulseAnimator.setDuration(PULSE_DURATION_MILLIS.toLong())
+ mPulseAnimator.setInterpolator(PULSE_INTERPOLATOR)
+ mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE)
+ mPulseAnimator.start()
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // Re-query for AlarmInstance in case the state has changed externally
+ val instanceId = AlarmInstance.getId(getIntent().getData()!!)
+ mAlarmInstance = AlarmInstance.getInstance(getContentResolver(), instanceId)
+
+ if (mAlarmInstance == null) {
+ LOGGER.i("No alarm instance for instanceId: %d", instanceId)
+ finish()
+ return
+ }
+
+ // Verify that the alarm is still firing before showing the activity
+ if (mAlarmInstance!!.mAlarmState != InstancesColumns.FIRED_STATE) {
+ LOGGER.i("Skip displaying alarm for instance: %s", mAlarmInstance)
+ finish()
+ return
+ }
+
+ if (!mReceiverRegistered) {
+ // Register to get the alarm done/snooze/dismiss intent.
+ val filter = IntentFilter(AlarmService.ALARM_DONE_ACTION)
+ filter.addAction(AlarmService.ALARM_SNOOZE_ACTION)
+ filter.addAction(AlarmService.ALARM_DISMISS_ACTION)
+ registerReceiver(mReceiver, filter)
+ mReceiverRegistered = true
+ }
+ bindAlarmService()
+ resetAnimations()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ unbindAlarmService()
+
+ // Skip if register didn't happen to avoid IllegalArgumentException
+ if (mReceiverRegistered) {
+ unregisterReceiver(mReceiver)
+ mReceiverRegistered = false
+ }
+ }
+
+ override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean {
+ // Do this in dispatch to intercept a few of the system keys.
+ LOGGER.v("dispatchKeyEvent: %s", keyEvent)
+
+ val keyCode: Int = keyEvent.getKeyCode()
+ when (keyCode) {
+ KeyEvent.KEYCODE_VOLUME_UP,
+ KeyEvent.KEYCODE_VOLUME_DOWN,
+ KeyEvent.KEYCODE_VOLUME_MUTE,
+ KeyEvent.KEYCODE_HEADSETHOOK,
+ KeyEvent.KEYCODE_CAMERA,
+ KeyEvent.KEYCODE_FOCUS -> if (!mAlarmHandled) {
+ when (mVolumeBehavior) {
+ AlarmVolumeButtonBehavior.SNOOZE -> {
+ if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ snooze()
+ }
+ return true
+ }
+ AlarmVolumeButtonBehavior.DISMISS -> {
+ if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
+ dismiss()
+ }
+ return true
+ }
+ AlarmVolumeButtonBehavior.NOTHING -> {
+ }
+ }
+ }
+ }
+ return super.dispatchKeyEvent(keyEvent)
+ }
+
+ override fun onBackPressed() {
+ // Don't allow back to dismiss.
+ }
+
+ override fun onClick(view: View) {
+ if (mAlarmHandled) {
+ LOGGER.v("onClick ignored: %s", view)
+ return
+ }
+ LOGGER.v("onClick: %s", view)
+
+ // If in accessibility mode, allow snooze/dismiss by double tapping on respective icons.
+ if (isAccessibilityEnabled) {
+ if (view == mSnoozeButton) {
+ snooze()
+ } else if (view == mDismissButton) {
+ dismiss()
+ }
+ return
+ }
+
+ if (view == mSnoozeButton) {
+ hintSnooze()
+ } else if (view == mDismissButton) {
+ hintDismiss()
+ }
+ }
+
+ override fun onTouch(view: View?, event: MotionEvent): Boolean {
+ if (mAlarmHandled) {
+ LOGGER.v("onTouch ignored: %s", event)
+ return false
+ }
+
+ val action: Int = event.getActionMasked()
+ if (action == MotionEvent.ACTION_DOWN) {
+ LOGGER.v("onTouch started: %s", event)
+
+ // Track the pointer that initiated the touch sequence.
+ mInitialPointerIndex = event.getPointerId(event.getActionIndex())
+
+ // Stop the pulse, allowing the last pulse to finish.
+ mPulseAnimator.setRepeatCount(0)
+ } else if (action == MotionEvent.ACTION_CANCEL) {
+ LOGGER.v("onTouch canceled: %s", event)
+
+ // Clear the pointer index.
+ mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID
+
+ // Reset everything.
+ resetAnimations()
+ }
+
+ val actionIndex: Int = event.getActionIndex()
+ if (mInitialPointerIndex == MotionEvent.INVALID_POINTER_ID ||
+ mInitialPointerIndex != event.getPointerId(actionIndex)) {
+ // Ignore any pointers other than the initial one, bail early.
+ return true
+ }
+
+ val contentLocation = intArrayOf(0, 0)
+ mContentView.getLocationOnScreen(contentLocation)
+
+ val x: Float = event.getRawX() - contentLocation[0]
+ val y: Float = event.getRawY() - contentLocation[1]
+
+ val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft()
+ val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight()
+
+ val snoozeFraction: Float
+ val dismissFraction: Float
+ if (mContentView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ snoozeFraction =
+ getFraction(alarmRight.toFloat(), mSnoozeButton.getLeft().toFloat(), x)
+ dismissFraction =
+ getFraction(alarmLeft.toFloat(), mDismissButton.getRight().toFloat(), x)
+ } else {
+ snoozeFraction = getFraction(alarmLeft.toFloat(), mSnoozeButton.getRight().toFloat(), x)
+ dismissFraction =
+ getFraction(alarmRight.toFloat(), mDismissButton.getLeft().toFloat(), x)
+ }
+ setAnimatedFractions(snoozeFraction, dismissFraction)
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
+ LOGGER.v("onTouch ended: %s", event)
+
+ mInitialPointerIndex = MotionEvent.INVALID_POINTER_ID
+ if (snoozeFraction == 1.0f) {
+ snooze()
+ } else if (dismissFraction == 1.0f) {
+ dismiss()
+ } else {
+ if (snoozeFraction > 0.0f || dismissFraction > 0.0f) {
+ // Animate back to the initial state.
+ AnimatorUtils.reverse(mAlarmAnimator, mSnoozeAnimator, mDismissAnimator)
+ } else if (mAlarmButton.getTop() <= y && y <= mAlarmButton.getBottom()) {
+ // User touched the alarm button, hint the dismiss action.
+ hintDismiss()
+ }
+
+ // Restart the pulse.
+ mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE)
+ if (!mPulseAnimator.isStarted()) {
+ mPulseAnimator.start()
+ }
+ }
+ }
+
+ return true
+ }
+
+ private fun hideNavigationBar() {
+ getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
+ }
+
+ /**
+ * Returns `true` if accessibility is enabled, to enable alternate behavior for click
+ * handling, etc.
+ */
+ private val isAccessibilityEnabled: Boolean
+ get() {
+ if (mAccessibilityManager == null || !mAccessibilityManager!!.isEnabled()) {
+ // Accessibility is unavailable or disabled.
+ return false
+ } else if (mAccessibilityManager!!.isTouchExplorationEnabled()) {
+ // TalkBack's touch exploration mode is enabled.
+ return true
+ }
+
+ // Check if "Switch Access" is enabled.
+ val enabledAccessibilityServices: List<AccessibilityServiceInfo> =
+ mAccessibilityManager!!.getEnabledAccessibilityServiceList(FEEDBACK_GENERIC)
+ return !enabledAccessibilityServices.isEmpty()
+ }
+
+ private fun hintSnooze() {
+ val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft()
+ val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight()
+ val translationX = (Math.max(mSnoozeButton.getLeft() - alarmRight, 0) +
+ Math.min(mSnoozeButton.getRight() - alarmLeft, 0)).toFloat()
+ getAlarmBounceAnimator(translationX, if (translationX < 0.0f) {
+ R.string.description_direction_left
+ } else {
+ R.string.description_direction_right
+ }).start()
+ }
+
+ private fun hintDismiss() {
+ val alarmLeft: Int = mAlarmButton.getLeft() + mAlarmButton.getPaddingLeft()
+ val alarmRight: Int = mAlarmButton.getRight() - mAlarmButton.getPaddingRight()
+ val translationX = (Math.max(mDismissButton.getLeft() - alarmRight, 0) +
+ Math.min(mDismissButton.getRight() - alarmLeft, 0)).toFloat()
+ getAlarmBounceAnimator(translationX, if (translationX < 0.0f) {
+ R.string.description_direction_left
+ } else {
+ R.string.description_direction_right
+ }).start()
+ }
+
+ /**
+ * Set animators to initial values and restart pulse on alarm button.
+ */
+ private fun resetAnimations() {
+ // Set the animators to their initial values.
+ setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */)
+ // Restart the pulse.
+ mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE)
+ if (!mPulseAnimator.isStarted()) {
+ mPulseAnimator.start()
+ }
+ }
+
+ /**
+ * Perform snooze animation and send snooze intent.
+ */
+ private fun snooze() {
+ mAlarmHandled = true
+ LOGGER.v("Snoozed: %s", mAlarmInstance)
+
+ val colorAccent = ThemeUtils.resolveColor(this, R.attr.colorAccent)
+ setAnimatedFractions(1.0f /* snoozeFraction */, 0.0f /* dismissFraction */)
+
+ val snoozeMinutes = DataModel.dataModel.snoozeLength
+ val infoText: String = getResources().getQuantityString(
+ R.plurals.alarm_alert_snooze_duration, snoozeMinutes, snoozeMinutes)
+ val accessibilityText: String = getResources().getQuantityString(
+ R.plurals.alarm_alert_snooze_set, snoozeMinutes, snoozeMinutes)
+
+ getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText,
+ accessibilityText, colorAccent, colorAccent).start()
+
+ AlarmStateManager.setSnoozeState(this, mAlarmInstance!!, false /* showToast */)
+
+ Events.sendAlarmEvent(R.string.action_snooze, R.string.label_deskclock)
+
+ // Unbind here, otherwise alarm will keep ringing until activity finishes.
+ unbindAlarmService()
+ }
+
+ /**
+ * Perform dismiss animation and send dismiss intent.
+ */
+ private fun dismiss() {
+ mAlarmHandled = true
+ LOGGER.v("Dismissed: %s", mAlarmInstance)
+
+ setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */)
+
+ getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */,
+ getString(R.string.alarm_alert_off_text) /* accessibilityText */,
+ Color.WHITE, mCurrentHourColor).start()
+
+ AlarmStateManager.deleteInstanceAndUpdateParent(this, mAlarmInstance!!)
+
+ Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_deskclock)
+
+ // Unbind here, otherwise alarm will keep ringing until activity finishes.
+ unbindAlarmService()
+ }
+
+ /**
+ * Bind AlarmService if not yet bound.
+ */
+ private fun bindAlarmService() {
+ if (!mServiceBound) {
+ val intent = Intent(this, AlarmService::class.java)
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
+ mServiceBound = true
+ }
+ }
+
+ /**
+ * Unbind AlarmService if bound.
+ */
+ private fun unbindAlarmService() {
+ if (mServiceBound) {
+ unbindService(mConnection)
+ mServiceBound = false
+ }
+ }
+
+ private fun setAnimatedFractions(snoozeFraction: Float, dismissFraction: Float) {
+ val alarmFraction = Math.max(snoozeFraction, dismissFraction)
+ AnimatorUtils.setAnimatedFraction(mAlarmAnimator, alarmFraction)
+ AnimatorUtils.setAnimatedFraction(mSnoozeAnimator, snoozeFraction)
+ AnimatorUtils.setAnimatedFraction(mDismissAnimator, dismissFraction)
+ }
+
+ private fun getFraction(x0: Float, x1: Float, x: Float): Float {
+ return Math.max(Math.min((x - x0) / (x1 - x0), 1.0f), 0.0f)
+ }
+
+ private fun getButtonAnimator(button: ImageView?, tintColor: Int): ValueAnimator {
+ return ObjectAnimator.ofPropertyValuesHolder(button,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, BUTTON_SCALE_DEFAULT, 1.0f),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, BUTTON_SCALE_DEFAULT, 1.0f),
+ PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255),
+ PropertyValuesHolder.ofInt(AnimatorUtils.DRAWABLE_ALPHA,
+ BUTTON_DRAWABLE_ALPHA_DEFAULT, 255),
+ PropertyValuesHolder.ofObject(AnimatorUtils.DRAWABLE_TINT,
+ AnimatorUtils.ARGB_EVALUATOR, Color.WHITE, tintColor))
+ }
+
+ private fun getAlarmBounceAnimator(translationX: Float, hintResId: Int): ValueAnimator {
+ val bounceAnimator: ValueAnimator = ObjectAnimator.ofFloat(mAlarmButton,
+ View.TRANSLATION_X, mAlarmButton.getTranslationX(), translationX, 0.0f)
+ bounceAnimator.setInterpolator(AnimatorUtils.DECELERATE_ACCELERATE_INTERPOLATOR)
+ bounceAnimator.setDuration(ALARM_BOUNCE_DURATION_MILLIS.toLong())
+ bounceAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator?) {
+ mHintView.setText(hintResId)
+ if (mHintView.getVisibility() != View.VISIBLE) {
+ mHintView.setVisibility(View.VISIBLE)
+ ObjectAnimator.ofFloat(mHintView, View.ALPHA, 0.0f, 1.0f).start()
+ }
+ }
+ })
+ return bounceAnimator
+ }
+
+ private fun getAlertAnimator(
+ source: View,
+ titleResId: Int,
+ infoText: String?,
+ accessibilityText: String,
+ revealColor: Int,
+ backgroundColor: Int
+ ): Animator {
+ val containerView: ViewGroup = findViewById(android.R.id.content) as ViewGroup
+
+ val sourceBounds = Rect(0, 0, source.getHeight(), source.getWidth())
+ containerView.offsetDescendantRectToMyCoords(source, sourceBounds)
+
+ val centerX: Int = sourceBounds.centerX()
+ val centerY: Int = sourceBounds.centerY()
+
+ val xMax = max(centerX, containerView.getWidth() - centerX)
+ val yMax = max(centerY, containerView.getHeight() - centerY)
+
+ val startRadius: Float = max(sourceBounds.width(), sourceBounds.height()) / 2.0f
+ val endRadius = sqrt(xMax * xMax + yMax * yMax.toDouble()).toFloat()
+
+ val revealView = CircleView(this)
+ .setCenterX(centerX.toFloat())
+ .setCenterY(centerY.toFloat())
+ .setFillColor(revealColor)
+ containerView.addView(revealView)
+
+ // TODO: Fade out source icon over the reveal (like LOLLIPOP version).
+
+ val revealAnimator: Animator = ObjectAnimator.ofFloat(
+ revealView, CircleView.RADIUS, startRadius, endRadius)
+ revealAnimator.setDuration(ALERT_REVEAL_DURATION_MILLIS.toLong())
+ revealAnimator.setInterpolator(REVEAL_INTERPOLATOR)
+ revealAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator?) {
+ mAlertView.setVisibility(View.VISIBLE)
+ mAlertTitleView.setText(titleResId)
+ if (infoText != null) {
+ mAlertInfoView.setText(infoText)
+ mAlertInfoView.setVisibility(View.VISIBLE)
+ }
+ mContentView.setVisibility(View.GONE)
+ getWindow().setBackgroundDrawable(ColorDrawable(backgroundColor))
+ }
+ })
+
+ val fadeAnimator: ValueAnimator = ObjectAnimator.ofFloat(revealView, View.ALPHA, 0.0f)
+ fadeAnimator.setDuration(ALERT_FADE_DURATION_MILLIS.toLong())
+ fadeAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ containerView.removeView(revealView)
+ }
+ })
+
+ val alertAnimator = AnimatorSet()
+ alertAnimator.play(revealAnimator).before(fadeAnimator)
+ alertAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator?) {
+ mAlertView.announceForAccessibility(accessibilityText)
+ mHandler.postDelayed(Runnable { finish() }, ALERT_DISMISS_DELAY_MILLIS.toLong())
+ }
+ })
+
+ return alertAnimator
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("AlarmActivity")
+
+ private val PULSE_INTERPOLATOR: TimeInterpolator =
+ PathInterpolatorCompat.create(0.4f, 0.0f, 0.2f, 1.0f)
+ private val REVEAL_INTERPOLATOR: TimeInterpolator =
+ PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f)
+
+ private const val PULSE_DURATION_MILLIS = 1000
+ private const val ALARM_BOUNCE_DURATION_MILLIS = 500
+ private const val ALERT_REVEAL_DURATION_MILLIS = 500
+ private const val ALERT_FADE_DURATION_MILLIS = 500
+ private const val ALERT_DISMISS_DELAY_MILLIS = 2000
+
+ private const val BUTTON_SCALE_DEFAULT = 0.7f
+ private const val BUTTON_DRAWABLE_ALPHA_DEFAULT = 165
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmKlaxon.java b/src/com/android/deskclock/alarms/AlarmKlaxon.java
deleted file mode 100644
index a162423..0000000
--- a/src/com/android/deskclock/alarms/AlarmKlaxon.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.os.Build;
-import android.os.Vibrator;
-
-import com.android.deskclock.AsyncRingtonePlayer;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.provider.AlarmInstance;
-
-/**
- * Manages playing alarm ringtones and vibrating the device.
- */
-final class AlarmKlaxon {
-
- private static final long[] VIBRATE_PATTERN = {500, 500};
-
- private static boolean sStarted = false;
- private static AsyncRingtonePlayer sAsyncRingtonePlayer;
-
- private AlarmKlaxon() {}
-
- public static void stop(Context context) {
- if (sStarted) {
- LogUtils.v("AlarmKlaxon.stop()");
- sStarted = false;
- getAsyncRingtonePlayer(context).stop();
- ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).cancel();
- }
- }
-
- public static void start(Context context, AlarmInstance instance) {
- // Make sure we are stopped before starting
- stop(context);
- LogUtils.v("AlarmKlaxon.start()");
-
- if (!AlarmInstance.NO_RINGTONE_URI.equals(instance.mRingtone)) {
- final long crescendoDuration = DataModel.getDataModel().getAlarmCrescendoDuration();
- getAsyncRingtonePlayer(context).play(instance.mRingtone, crescendoDuration);
- }
-
- if (instance.mVibrate) {
- final Vibrator vibrator = getVibrator(context);
- if (Utils.isLOrLater()) {
- vibrateLOrLater(vibrator);
- } else {
- vibrator.vibrate(VIBRATE_PATTERN, 0);
- }
- }
-
- sStarted = true;
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private static void vibrateLOrLater(Vibrator vibrator) {
- vibrator.vibrate(VIBRATE_PATTERN, 0, new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build());
- }
-
- private static Vibrator getVibrator(Context context) {
- return ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE));
- }
-
- private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
- if (sAsyncRingtonePlayer == null) {
- sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
- }
-
- return sAsyncRingtonePlayer;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmKlaxon.kt b/src/com/android/deskclock/alarms/AlarmKlaxon.kt
new file mode 100644
index 0000000..8ccf4fb
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmKlaxon.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.media.AudioAttributes
+import android.os.Build
+import android.os.Vibrator
+
+import com.android.deskclock.AsyncRingtonePlayer
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+
+/**
+ * Manages playing alarm ringtones and vibrating the device.
+ */
+internal object AlarmKlaxon {
+
+ private val VIBRATE_PATTERN = longArrayOf(500, 500)
+
+ private var sStarted = false
+ private var sAsyncRingtonePlayer: AsyncRingtonePlayer? = null
+
+ @JvmStatic
+ fun stop(context: Context) {
+ if (sStarted) {
+ LogUtils.v("AlarmKlaxon.stop()")
+ sStarted = false
+ getAsyncRingtonePlayer(context)!!.stop()
+ (context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).cancel()
+ }
+ }
+
+ @JvmStatic
+ fun start(context: Context, instance: AlarmInstance) {
+ // Make sure we are stopped before starting
+ stop(context)
+ LogUtils.v("AlarmKlaxon.start()")
+
+ if (!AlarmSettingColumns.NO_RINGTONE_URI.equals(instance.mRingtone)) {
+ val crescendoDuration = DataModel.dataModel.alarmCrescendoDuration
+ getAsyncRingtonePlayer(context)!!.play(instance.mRingtone, crescendoDuration)
+ }
+
+ if (instance.mVibrate) {
+ val vibrator: Vibrator = getVibrator(context)
+ if (Utils.isLOrLater) {
+ vibrateLOrLater(vibrator)
+ } else {
+ vibrator.vibrate(VIBRATE_PATTERN, 0)
+ }
+ }
+
+ sStarted = true
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private fun vibrateLOrLater(vibrator: Vibrator) {
+ vibrator.vibrate(VIBRATE_PATTERN, 0, AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build())
+ }
+
+ private fun getVibrator(context: Context): Vibrator {
+ return context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ }
+
+ @Synchronized
+ private fun getAsyncRingtonePlayer(context: Context): AsyncRingtonePlayer? {
+ if (sAsyncRingtonePlayer == null) {
+ sAsyncRingtonePlayer = AsyncRingtonePlayer(context.getApplicationContext())
+ }
+
+ return sAsyncRingtonePlayer
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmNotifications.java b/src/com/android/deskclock/alarms/AlarmNotifications.java
deleted file mode 100644
index 5dc44c9..0000000
--- a/src/com/android/deskclock/alarms/AlarmNotifications.java
+++ /dev/null
@@ -1,580 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.alarms;
-
-import android.annotation.TargetApi;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Build;
-import android.service.notification.StatusBarNotification;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Locale;
-import java.util.Objects;
-
-final class AlarmNotifications {
- static final String EXTRA_NOTIFICATION_ID = "extra_notification_id";
-
- /**
- * Notification channel containing all low priority notifications.
- */
- private static final String ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID =
- "alarmLowPriorityNotification";
-
- /**
- * Notification channel containing all high priority notifications.
- */
- private static final String ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID =
- "alarmHighPriorityNotification";
-
- /**
- * Notification channel containing all snooze notifications.
- */
- private static final String ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID =
- "alarmSnoozeNotification";
-
- /**
- * Notification channel containing all missed notifications.
- */
- private static final String ALARM_MISSED_NOTIFICATION_CHANNEL_ID =
- "alarmMissedNotification";
-
- /**
- * Notification channel containing all alarm notifications.
- */
- private static final String ALARM_NOTIFICATION_CHANNEL_ID = "alarmNotification";
-
- /**
- * Formats times such that chronological order and lexicographical order agree.
- */
- private static final DateFormat SORT_KEY_FORMAT =
- new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
-
- /**
- * This value is coordinated with group ids from
- * {@link com.android.deskclock.data.NotificationModel}
- */
- private static final String UPCOMING_GROUP_KEY = "1";
-
- /**
- * This value is coordinated with group ids from
- * {@link com.android.deskclock.data.NotificationModel}
- */
- private static final String MISSED_GROUP_KEY = "4";
-
- /**
- * This value is coordinated with notification ids from
- * {@link com.android.deskclock.data.NotificationModel}
- */
- private static final int ALARM_GROUP_NOTIFICATION_ID = Integer.MAX_VALUE - 4;
-
- /**
- * This value is coordinated with notification ids from
- * {@link com.android.deskclock.data.NotificationModel}
- */
- private static final int ALARM_GROUP_MISSED_NOTIFICATION_ID = Integer.MAX_VALUE - 5;
-
- /**
- * This value is coordinated with notification ids from
- * {@link com.android.deskclock.data.NotificationModel}
- */
- private static final int ALARM_FIRING_NOTIFICATION_ID = Integer.MAX_VALUE - 7;
-
- static synchronized void showLowPriorityNotification(Context context,
- AlarmInstance instance) {
- LogUtils.v("Displaying low priority notification for alarm instance: " + instance.mId);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(
- context, ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID)
- .setShowWhen(false)
- .setContentTitle(context.getString(
- R.string.alarm_alert_predismiss_title))
- .setContentText(AlarmUtils.getAlarmText(
- context, instance, true /* includeLabel */))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .setSmallIcon(R.drawable.stat_notify_alarm)
- .setAutoCancel(false)
- .setSortKey(createSortKey(instance))
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setLocalOnly(true);
-
- if (Utils.isNOrLater()) {
- builder.setGroup(UPCOMING_GROUP_KEY);
- }
-
- // Setup up hide notification
- Intent hideIntent = AlarmStateManager.createStateChangeIntent(context,
- AlarmStateManager.ALARM_DELETE_TAG, instance,
- AlarmInstance.HIDE_NOTIFICATION_STATE);
- final int id = instance.hashCode();
- builder.setDeleteIntent(PendingIntent.getService(context, id,
- hideIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- // Setup up dismiss action
- Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
- AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.PREDISMISSED_STATE);
- builder.addAction(R.drawable.ic_alarm_off_24dp,
- context.getString(R.string.alarm_alert_dismiss_text),
- PendingIntent.getService(context, id,
- dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- // Setup content action if instance is owned by alarm
- Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
- builder.setContentIntent(PendingIntent.getActivity(context, id,
- viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- nm.createNotificationChannel(channel);
- }
- final Notification notification = builder.build();
- nm.notify(id, notification);
- updateUpcomingAlarmGroupNotification(context, -1, notification);
- }
-
- static synchronized void showHighPriorityNotification(Context context,
- AlarmInstance instance) {
- LogUtils.v("Displaying high priority notification for alarm instance: " + instance.mId);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(
- context, ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID)
- .setShowWhen(false)
- .setContentTitle(context.getString(
- R.string.alarm_alert_predismiss_title))
- .setContentText(AlarmUtils.getAlarmText(
- context, instance, true /* includeLabel */))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .setSmallIcon(R.drawable.stat_notify_alarm)
- .setAutoCancel(false)
- .setSortKey(createSortKey(instance))
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setLocalOnly(true);
-
- if (Utils.isNOrLater()) {
- builder.setGroup(UPCOMING_GROUP_KEY);
- }
-
- // Setup up dismiss action
- Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
- AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.PREDISMISSED_STATE);
- final int id = instance.hashCode();
- builder.addAction(R.drawable.ic_alarm_off_24dp,
- context.getString(R.string.alarm_alert_dismiss_text),
- PendingIntent.getService(context, id,
- dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- // Setup content action if instance is owned by alarm
- Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
- builder.setContentIntent(PendingIntent.getActivity(context, id,
- viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- nm.createNotificationChannel(channel);
- }
- final Notification notification = builder.build();
- nm.notify(id, notification);
- updateUpcomingAlarmGroupNotification(context, -1, notification);
- }
-
- @TargetApi(Build.VERSION_CODES.N)
- private static boolean isGroupSummary(Notification n) {
- return (n.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY;
- }
-
- /**
- * Method which returns the first active notification for a given group. If a notification was
- * just posted, provide it to make sure it is included as a potential result. If a notification
- * was just canceled, provide the id so that it is not included as a potential result. These
- * extra parameters are needed due to a race condition which exists in
- * {@link NotificationManager#getActiveNotifications()}.
- *
- * @param context Context from which to grab the NotificationManager
- * @param group The group key to query for notifications
- * @param canceledNotificationId The id of the just-canceled notification (-1 if none)
- * @param postedNotification The notification that was just posted
- * @return The first active notification for the group
- */
- @TargetApi(Build.VERSION_CODES.N)
- private static Notification getFirstActiveNotification(Context context, String group,
- int canceledNotificationId, Notification postedNotification) {
- final NotificationManager nm =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- final StatusBarNotification[] notifications = nm.getActiveNotifications();
- Notification firstActiveNotification = postedNotification;
- for (StatusBarNotification statusBarNotification : notifications) {
- final Notification n = statusBarNotification.getNotification();
- if (!isGroupSummary(n)
- && group.equals(n.getGroup())
- && statusBarNotification.getId() != canceledNotificationId) {
- if (firstActiveNotification == null
- || n.getSortKey().compareTo(firstActiveNotification.getSortKey()) < 0) {
- firstActiveNotification = n;
- }
- }
- }
- return firstActiveNotification;
- }
-
- @TargetApi(Build.VERSION_CODES.N)
- private static Notification getActiveGroupSummaryNotification(Context context, String group) {
- final NotificationManager nm =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- final StatusBarNotification[] notifications = nm.getActiveNotifications();
- for (StatusBarNotification statusBarNotification : notifications) {
- final Notification n = statusBarNotification.getNotification();
- if (isGroupSummary(n) && group.equals(n.getGroup())) {
- return n;
- }
- }
- return null;
- }
-
- private static void updateUpcomingAlarmGroupNotification(Context context,
- int canceledNotificationId, Notification postedNotification) {
- if (!Utils.isNOrLater()) {
- return;
- }
-
- final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- ALARM_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- nm.createNotificationChannel(channel);
- }
-
- final Notification firstUpcoming = getFirstActiveNotification(context, UPCOMING_GROUP_KEY,
- canceledNotificationId, postedNotification);
- if (firstUpcoming == null) {
- nm.cancel(ALARM_GROUP_NOTIFICATION_ID);
- return;
- }
-
- Notification summary = getActiveGroupSummaryNotification(context, UPCOMING_GROUP_KEY);
- if (summary == null
- || !Objects.equals(summary.contentIntent, firstUpcoming.contentIntent)) {
- summary = new NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
- .setShowWhen(false)
- .setContentIntent(firstUpcoming.contentIntent)
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .setSmallIcon(R.drawable.stat_notify_alarm)
- .setGroup(UPCOMING_GROUP_KEY)
- .setGroupSummary(true)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setLocalOnly(true)
- .build();
- nm.notify(ALARM_GROUP_NOTIFICATION_ID, summary);
- }
- }
-
- private static void updateMissedAlarmGroupNotification(Context context,
- int canceledNotificationId, Notification postedNotification) {
- if (!Utils.isNOrLater()) {
- return;
- }
-
- final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- ALARM_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- nm.createNotificationChannel(channel);
- }
-
- final Notification firstMissed = getFirstActiveNotification(context, MISSED_GROUP_KEY,
- canceledNotificationId, postedNotification);
- if (firstMissed == null) {
- nm.cancel(ALARM_GROUP_MISSED_NOTIFICATION_ID);
- return;
- }
-
- Notification summary = getActiveGroupSummaryNotification(context, MISSED_GROUP_KEY);
- if (summary == null
- || !Objects.equals(summary.contentIntent, firstMissed.contentIntent)) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- nm.createNotificationChannel(channel);
- }
- summary = new NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
- .setShowWhen(false)
- .setContentIntent(firstMissed.contentIntent)
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .setSmallIcon(R.drawable.stat_notify_alarm)
- .setGroup(MISSED_GROUP_KEY)
- .setGroupSummary(true)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setLocalOnly(true)
- .build();
- nm.notify(ALARM_GROUP_MISSED_NOTIFICATION_ID, summary);
- }
- }
-
- static synchronized void showSnoozeNotification(Context context,
- AlarmInstance instance) {
- LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(
- context, ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID)
- .setShowWhen(false)
- .setContentTitle(instance.getLabelOrDefault(context))
- .setContentText(context.getString(R.string.alarm_alert_snooze_until,
- AlarmUtils.getFormattedTime(context, instance.getAlarmTime())))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .setSmallIcon(R.drawable.stat_notify_alarm)
- .setAutoCancel(false)
- .setSortKey(createSortKey(instance))
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setLocalOnly(true);
-
- if (Utils.isNOrLater()) {
- builder.setGroup(UPCOMING_GROUP_KEY);
- }
-
- // Setup up dismiss action
- Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
- AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
- final int id = instance.hashCode();
- builder.addAction(R.drawable.ic_alarm_off_24dp,
- context.getString(R.string.alarm_alert_dismiss_text),
- PendingIntent.getService(context, id,
- dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- // Setup content action if instance is owned by alarm
- Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
- builder.setContentIntent(PendingIntent.getActivity(context, id,
- viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- nm.createNotificationChannel(channel);
- }
- final Notification notification = builder.build();
- nm.notify(id, notification);
- updateUpcomingAlarmGroupNotification(context, -1, notification);
- }
-
- static synchronized void showMissedNotification(Context context,
- AlarmInstance instance) {
- LogUtils.v("Displaying missed notification for alarm instance: " + instance.mId);
-
- String label = instance.mLabel;
- String alarmTime = AlarmUtils.getFormattedTime(context, instance.getAlarmTime());
- NotificationCompat.Builder builder = new NotificationCompat.Builder(
- context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
- .setShowWhen(false)
- .setContentTitle(context.getString(R.string.alarm_missed_title))
- .setContentText(instance.mLabel.isEmpty() ? alarmTime :
- context.getString(R.string.alarm_missed_text, alarmTime, label))
- .setColor(ContextCompat.getColor(context, R.color.default_background))
- .setSortKey(createSortKey(instance))
- .setSmallIcon(R.drawable.stat_notify_alarm)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setLocalOnly(true);
-
- if (Utils.isNOrLater()) {
- builder.setGroup(MISSED_GROUP_KEY);
- }
-
- final int id = instance.hashCode();
-
- // Setup dismiss intent
- Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
- AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
- builder.setDeleteIntent(PendingIntent.getService(context, id,
- dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- // Setup content intent
- Intent showAndDismiss = AlarmInstance.createIntent(context, AlarmStateManager.class,
- instance.mId);
- showAndDismiss.putExtra(EXTRA_NOTIFICATION_ID, id);
- showAndDismiss.setAction(AlarmStateManager.SHOW_AND_DISMISS_ALARM_ACTION);
- builder.setContentIntent(PendingIntent.getBroadcast(context, id,
- showAndDismiss, PendingIntent.FLAG_UPDATE_CURRENT));
-
- NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- nm.createNotificationChannel(channel);
- }
- final Notification notification = builder.build();
- nm.notify(id, notification);
- updateMissedAlarmGroupNotification(context, -1, notification);
- }
-
- static synchronized void showAlarmNotification(Service service, AlarmInstance instance) {
- LogUtils.v("Displaying alarm notification for alarm instance: " + instance.mId);
-
- Resources resources = service.getResources();
- NotificationCompat.Builder notification = new NotificationCompat.Builder(
- service, ALARM_NOTIFICATION_CHANNEL_ID)
- .setContentTitle(instance.getLabelOrDefault(service))
- .setContentText(AlarmUtils.getFormattedTime(
- service, instance.getAlarmTime()))
- .setColor(ContextCompat.getColor(service, R.color.default_background))
- .setSmallIcon(R.drawable.stat_notify_alarm)
- .setOngoing(true)
- .setAutoCancel(false)
- .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
- .setWhen(0)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setLocalOnly(true);
-
- // Setup Snooze Action
- Intent snoozeIntent = AlarmStateManager.createStateChangeIntent(service,
- AlarmStateManager.ALARM_SNOOZE_TAG, instance, AlarmInstance.SNOOZE_STATE);
- snoozeIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true);
- PendingIntent snoozePendingIntent = PendingIntent.getService(service,
- ALARM_FIRING_NOTIFICATION_ID, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- notification.addAction(R.drawable.ic_snooze_24dp,
- resources.getString(R.string.alarm_alert_snooze_text), snoozePendingIntent);
-
- // Setup Dismiss Action
- Intent dismissIntent = AlarmStateManager.createStateChangeIntent(service,
- AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
- dismissIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true);
- PendingIntent dismissPendingIntent = PendingIntent.getService(service,
- ALARM_FIRING_NOTIFICATION_ID, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- notification.addAction(R.drawable.ic_alarm_off_24dp,
- resources.getString(R.string.alarm_alert_dismiss_text),
- dismissPendingIntent);
-
- // Setup Content Action
- Intent contentIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
- instance.mId);
- notification.setContentIntent(PendingIntent.getActivity(service,
- ALARM_FIRING_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
-
- // Setup fullscreen intent
- Intent fullScreenIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
- instance.mId);
- // set action, so we can be different then content pending intent
- fullScreenIntent.setAction("fullscreen_activity");
- fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
- Intent.FLAG_ACTIVITY_NO_USER_ACTION);
- notification.setFullScreenIntent(PendingIntent.getActivity(service,
- ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT),
- true);
- notification.setPriority(NotificationCompat.PRIORITY_MAX);
-
- clearNotification(service, instance);
- service.startForeground(ALARM_FIRING_NOTIFICATION_ID, notification.build());
- }
-
- static synchronized void clearNotification(Context context, AlarmInstance instance) {
- LogUtils.v("Clearing notifications for alarm instance: " + instance.mId);
- NotificationManagerCompat nm = NotificationManagerCompat.from(context);
- final int id = instance.hashCode();
- nm.cancel(id);
- updateUpcomingAlarmGroupNotification(context, id, null);
- updateMissedAlarmGroupNotification(context, id, null);
- }
-
- /**
- * Updates the notification for an existing alarm. Use if the label has changed.
- */
- static void updateNotification(Context context, AlarmInstance instance) {
- switch (instance.mAlarmState) {
- case AlarmInstance.LOW_NOTIFICATION_STATE:
- showLowPriorityNotification(context, instance);
- break;
- case AlarmInstance.HIGH_NOTIFICATION_STATE:
- showHighPriorityNotification(context, instance);
- break;
- case AlarmInstance.SNOOZE_STATE:
- showSnoozeNotification(context, instance);
- break;
- case AlarmInstance.MISSED_STATE:
- showMissedNotification(context, instance);
- break;
- default:
- LogUtils.d("No notification to update");
- }
- }
-
- static Intent createViewAlarmIntent(Context context, AlarmInstance instance) {
- final long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
- return Alarm.createIntent(context, DeskClock.class, alarmId)
- .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- }
-
- /**
- * Alarm notifications are sorted chronologically. Missed alarms are sorted chronologically
- * <strong>after</strong> all upcoming/snoozed alarms by including the "MISSED" prefix on the
- * sort key.
- *
- * @param instance the alarm instance for which the notification is generated
- * @return the sort key that specifies the order of this alarm notification
- */
- private static String createSortKey(AlarmInstance instance) {
- final String timeKey = SORT_KEY_FORMAT.format(instance.getAlarmTime().getTime());
- final boolean missedAlarm = instance.mAlarmState == AlarmInstance.MISSED_STATE;
- return missedAlarm ? ("MISSED " + timeKey) : timeKey;
- }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmNotifications.kt b/src/com/android/deskclock/alarms/AlarmNotifications.kt
new file mode 100644
index 0000000..ed7d8ea
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmNotifications.kt
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.annotation.TargetApi
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.service.notification.StatusBarNotification
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.DeskClock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+internal object AlarmNotifications {
+ const val EXTRA_NOTIFICATION_ID = "extra_notification_id"
+
+ /**
+ * Notification channel containing all low priority notifications.
+ */
+ private const val ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID = "alarmLowPriorityNotification"
+
+ /**
+ * Notification channel containing all high priority notifications.
+ */
+ private const val ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID = "alarmHighPriorityNotification"
+
+ /**
+ * Notification channel containing all snooze notifications.
+ */
+ private const val ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID = "alarmSnoozeNotification"
+
+ /**
+ * Notification channel containing all missed notifications.
+ */
+ private const val ALARM_MISSED_NOTIFICATION_CHANNEL_ID = "alarmMissedNotification"
+
+ /**
+ * Notification channel containing all alarm notifications.
+ */
+ private const val ALARM_NOTIFICATION_CHANNEL_ID = "alarmNotification"
+
+ /**
+ * Formats times such that chronological order and lexicographical order agree.
+ */
+ private val SORT_KEY_FORMAT: DateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
+
+ /**
+ * This value is coordinated with group ids from
+ * [com.android.deskclock.data.NotificationModel]
+ */
+ private const val UPCOMING_GROUP_KEY = "1"
+
+ /**
+ * This value is coordinated with group ids from
+ * [com.android.deskclock.data.NotificationModel]
+ */
+ private const val MISSED_GROUP_KEY = "4"
+
+ /**
+ * This value is coordinated with notification ids from
+ * [com.android.deskclock.data.NotificationModel]
+ */
+ private const val ALARM_GROUP_NOTIFICATION_ID = Int.MAX_VALUE - 4
+
+ /**
+ * This value is coordinated with notification ids from
+ * [com.android.deskclock.data.NotificationModel]
+ */
+ private const val ALARM_GROUP_MISSED_NOTIFICATION_ID = Int.MAX_VALUE - 5
+
+ /**
+ * This value is coordinated with notification ids from
+ * [com.android.deskclock.data.NotificationModel]
+ */
+ private const val ALARM_FIRING_NOTIFICATION_ID = Int.MAX_VALUE - 7
+
+ @JvmStatic
+ @Synchronized
+ fun showLowPriorityNotification(
+ context: Context,
+ instance: AlarmInstance
+ ) {
+ LogUtils.v("Displaying low priority notification for alarm instance: " + instance.mId)
+
+ val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+ context, ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID)
+ .setShowWhen(false)
+ .setContentTitle(context.getString(
+ R.string.alarm_alert_predismiss_title))
+ .setContentText(AlarmUtils.getAlarmText(
+ context, instance, true /* includeLabel */))
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+ .setSmallIcon(R.drawable.stat_notify_alarm)
+ .setAutoCancel(false)
+ .setSortKey(createSortKey(instance))
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setCategory(NotificationCompat.CATEGORY_EVENT)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+
+ if (Utils.isNOrLater) {
+ builder.setGroup(UPCOMING_GROUP_KEY)
+ }
+
+ // Setup up hide notification
+ val hideIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+ AlarmStateManager.ALARM_DELETE_TAG, instance,
+ InstancesColumns.HIDE_NOTIFICATION_STATE)
+ val id = instance.hashCode()
+ builder.setDeleteIntent(PendingIntent.getService(context, id,
+ hideIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ // Setup up dismiss action
+ val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+ AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.PREDISMISSED_STATE)
+ builder.addAction(R.drawable.ic_alarm_off_24dp,
+ context.getString(R.string.alarm_alert_dismiss_text),
+ PendingIntent.getService(context, id,
+ dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ // Setup content action if instance is owned by alarm
+ val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+ builder.setContentIntent(PendingIntent.getActivity(context, id,
+ viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_DEFAULT)
+ nm.createNotificationChannel(channel)
+ }
+ val notification: Notification = builder.build()
+ nm.notify(id, notification)
+ updateUpcomingAlarmGroupNotification(context, -1, notification)
+ }
+
+ @JvmStatic
+ @Synchronized
+ fun showHighPriorityNotification(
+ context: Context,
+ instance: AlarmInstance
+ ) {
+ LogUtils.v("Displaying high priority notification for alarm instance: " + instance.mId)
+
+ val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+ context, ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID)
+ .setShowWhen(false)
+ .setContentTitle(context.getString(
+ R.string.alarm_alert_predismiss_title))
+ .setContentText(AlarmUtils.getAlarmText(
+ context, instance, true /* includeLabel */))
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+ .setSmallIcon(R.drawable.stat_notify_alarm)
+ .setAutoCancel(false)
+ .setSortKey(createSortKey(instance))
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_EVENT)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+
+ if (Utils.isNOrLater) {
+ builder.setGroup(UPCOMING_GROUP_KEY)
+ }
+
+ // Setup up dismiss action
+ val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+ AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.PREDISMISSED_STATE)
+ val id = instance.hashCode()
+ builder.addAction(R.drawable.ic_alarm_off_24dp,
+ context.getString(R.string.alarm_alert_dismiss_text),
+ PendingIntent.getService(context, id,
+ dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ // Setup content action if instance is owned by alarm
+ val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+ builder.setContentIntent(PendingIntent.getActivity(context, id,
+ viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_HIGH)
+ nm.createNotificationChannel(channel)
+ }
+ val notification: Notification = builder.build()
+ nm.notify(id, notification)
+ updateUpcomingAlarmGroupNotification(context, -1, notification)
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private fun isGroupSummary(n: Notification): Boolean {
+ return n.flags and Notification.FLAG_GROUP_SUMMARY == Notification.FLAG_GROUP_SUMMARY
+ }
+
+ /**
+ * Method which returns the first active notification for a given group. If a notification was
+ * just posted, provide it to make sure it is included as a potential result. If a notification
+ * was just canceled, provide the id so that it is not included as a potential result. These
+ * extra parameters are needed due to a race condition which exists in
+ * [NotificationManager.getActiveNotifications].
+ *
+ * @param context Context from which to grab the NotificationManager
+ * @param group The group key to query for notifications
+ * @param canceledNotificationId The id of the just-canceled notification (-1 if none)
+ * @param postedNotification The notification that was just posted
+ * @return The first active notification for the group
+ */
+ @TargetApi(Build.VERSION_CODES.N)
+ private fun getFirstActiveNotification(
+ context: Context,
+ group: String,
+ canceledNotificationId: Int,
+ postedNotification: Notification?
+ ): Notification? {
+ val nm: NotificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val notifications: Array<StatusBarNotification> = nm.getActiveNotifications()
+ var firstActiveNotification: Notification? = postedNotification
+ for (statusBarNotification in notifications) {
+ val n: Notification = statusBarNotification.getNotification()
+ if (!isGroupSummary(n) && group == n.getGroup() &&
+ statusBarNotification.getId() != canceledNotificationId) {
+ if (firstActiveNotification == null ||
+ n.getSortKey().compareTo(firstActiveNotification.getSortKey()) < 0) {
+ firstActiveNotification = n
+ }
+ }
+ }
+ return firstActiveNotification
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private fun getActiveGroupSummaryNotification(context: Context, group: String): Notification? {
+ val nm: NotificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val notifications: Array<StatusBarNotification> = nm.getActiveNotifications()
+ for (statusBarNotification in notifications) {
+ val n: Notification = statusBarNotification.getNotification()
+ if (isGroupSummary(n) && group == n.getGroup()) {
+ return n
+ }
+ }
+ return null
+ }
+
+ private fun updateUpcomingAlarmGroupNotification(
+ context: Context,
+ canceledNotificationId: Int,
+ postedNotification: Notification?
+ ) {
+ if (!Utils.isNOrLater) {
+ return
+ }
+
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ ALARM_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_HIGH)
+ nm.createNotificationChannel(channel)
+ }
+
+ val firstUpcoming: Notification? = getFirstActiveNotification(context, UPCOMING_GROUP_KEY,
+ canceledNotificationId, postedNotification)
+ if (firstUpcoming == null) {
+ nm.cancel(ALARM_GROUP_NOTIFICATION_ID)
+ return
+ }
+
+ var summary: Notification? = getActiveGroupSummaryNotification(context, UPCOMING_GROUP_KEY)
+ if (summary == null ||
+ summary.contentIntent != firstUpcoming.contentIntent) {
+ summary = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
+ .setShowWhen(false)
+ .setContentIntent(firstUpcoming.contentIntent)
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+ .setSmallIcon(R.drawable.stat_notify_alarm)
+ .setGroup(UPCOMING_GROUP_KEY)
+ .setGroupSummary(true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_EVENT)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+ .build()
+ nm.notify(ALARM_GROUP_NOTIFICATION_ID, summary)
+ }
+ }
+
+ private fun updateMissedAlarmGroupNotification(
+ context: Context,
+ canceledNotificationId: Int,
+ postedNotification: Notification?
+ ) {
+ if (!Utils.isNOrLater) {
+ return
+ }
+
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ ALARM_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_HIGH)
+ nm.createNotificationChannel(channel)
+ }
+
+ val firstMissed: Notification? = getFirstActiveNotification(context, MISSED_GROUP_KEY,
+ canceledNotificationId, postedNotification)
+ if (firstMissed == null) {
+ nm.cancel(ALARM_GROUP_MISSED_NOTIFICATION_ID)
+ return
+ }
+
+ var summary: Notification? = getActiveGroupSummaryNotification(context, MISSED_GROUP_KEY)
+ if (summary == null ||
+ summary.contentIntent != firstMissed.contentIntent) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_HIGH)
+ nm.createNotificationChannel(channel)
+ }
+ summary = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
+ .setShowWhen(false)
+ .setContentIntent(firstMissed.contentIntent)
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+ .setSmallIcon(R.drawable.stat_notify_alarm)
+ .setGroup(MISSED_GROUP_KEY)
+ .setGroupSummary(true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_EVENT)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+ .build()
+ nm.notify(ALARM_GROUP_MISSED_NOTIFICATION_ID, summary)
+ }
+ }
+
+ @JvmStatic
+ @Synchronized
+ fun showSnoozeNotification(
+ context: Context,
+ instance: AlarmInstance
+ ) {
+ LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId)
+
+ val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+ context, ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID)
+ .setShowWhen(false)
+ .setContentTitle(instance.getLabelOrDefault(context))
+ .setContentText(context.getString(R.string.alarm_alert_snooze_until,
+ AlarmUtils.getFormattedTime(context, instance.alarmTime)))
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+ .setSmallIcon(R.drawable.stat_notify_alarm)
+ .setAutoCancel(false)
+ .setSortKey(createSortKey(instance))
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setCategory(NotificationCompat.CATEGORY_EVENT)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+
+ if (Utils.isNOrLater) {
+ builder.setGroup(UPCOMING_GROUP_KEY)
+ }
+
+ // Setup up dismiss action
+ val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+ AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+ val id = instance.hashCode()
+ builder.addAction(R.drawable.ic_alarm_off_24dp,
+ context.getString(R.string.alarm_alert_dismiss_text),
+ PendingIntent.getService(context, id,
+ dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ // Setup content action if instance is owned by alarm
+ val viewAlarmIntent: Intent = createViewAlarmIntent(context, instance)
+ builder.setContentIntent(PendingIntent.getActivity(context, id,
+ viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_DEFAULT)
+ nm.createNotificationChannel(channel)
+ }
+ val notification: Notification = builder.build()
+ nm.notify(id, notification)
+ updateUpcomingAlarmGroupNotification(context, -1, notification)
+ }
+
+ @JvmStatic
+ @Synchronized
+ fun showMissedNotification(
+ context: Context,
+ instance: AlarmInstance
+ ) {
+ LogUtils.v("Displaying missed notification for alarm instance: " + instance.mId)
+
+ val label = instance.mLabel
+ val alarmTime: String = AlarmUtils.getFormattedTime(context, instance.alarmTime)
+ val builder: NotificationCompat.Builder = NotificationCompat.Builder(
+ context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
+ .setShowWhen(false)
+ .setContentTitle(context.getString(R.string.alarm_missed_title))
+ .setContentText(if (instance.mLabel!!.isEmpty()) {
+ alarmTime
+ } else {
+ context.getString(R.string.alarm_missed_text, alarmTime, label)
+ })
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+ .setSortKey(createSortKey(instance))
+ .setSmallIcon(R.drawable.stat_notify_alarm)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_EVENT)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+
+ if (Utils.isNOrLater) {
+ builder.setGroup(MISSED_GROUP_KEY)
+ }
+
+ val id = instance.hashCode()
+
+ // Setup dismiss intent
+ val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(context,
+ AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+ builder.setDeleteIntent(PendingIntent.getService(context, id,
+ dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ // Setup content intent
+ val showAndDismiss: Intent = AlarmInstance.createIntent(context,
+ AlarmStateManager::class.java, instance.mId)
+ showAndDismiss.putExtra(EXTRA_NOTIFICATION_ID, id)
+ showAndDismiss.setAction(AlarmStateManager.SHOW_AND_DISMISS_ALARM_ACTION)
+ builder.setContentIntent(PendingIntent.getBroadcast(context, id,
+ showAndDismiss, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_DEFAULT)
+ nm.createNotificationChannel(channel)
+ }
+ val notification: Notification = builder.build()
+ nm.notify(id, notification)
+ updateMissedAlarmGroupNotification(context, -1, notification)
+ }
+
+ @Synchronized
+ fun showAlarmNotification(service: Service, instance: AlarmInstance) {
+ LogUtils.v("Displaying alarm notification for alarm instance: " + instance.mId)
+
+ val resources: Resources = service.getResources()
+ val notification: NotificationCompat.Builder = NotificationCompat.Builder(
+ service, ALARM_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(instance.getLabelOrDefault(service))
+ .setContentText(AlarmUtils.getFormattedTime(
+ service, instance.alarmTime))
+ .setColor(ContextCompat.getColor(service, R.color.default_background))
+ .setSmallIcon(R.drawable.stat_notify_alarm)
+ .setOngoing(true)
+ .setAutoCancel(false)
+ .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
+ .setWhen(0)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+
+ // Setup Snooze Action
+ val snoozeIntent: Intent = AlarmStateManager.createStateChangeIntent(service,
+ AlarmStateManager.ALARM_SNOOZE_TAG, instance, InstancesColumns.SNOOZE_STATE)
+ snoozeIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true)
+ val snoozePendingIntent: PendingIntent = PendingIntent.getService(service,
+ ALARM_FIRING_NOTIFICATION_ID, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+ notification.addAction(R.drawable.ic_snooze_24dp,
+ resources.getString(R.string.alarm_alert_snooze_text), snoozePendingIntent)
+
+ // Setup Dismiss Action
+ val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(service,
+ AlarmStateManager.ALARM_DISMISS_TAG, instance, InstancesColumns.DISMISSED_STATE)
+ dismissIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true)
+ val dismissPendingIntent: PendingIntent = PendingIntent.getService(service,
+ ALARM_FIRING_NOTIFICATION_ID, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+ notification.addAction(R.drawable.ic_alarm_off_24dp,
+ resources.getString(R.string.alarm_alert_dismiss_text),
+ dismissPendingIntent)
+
+ // Setup Content Action
+ val contentIntent: Intent = AlarmInstance.createIntent(service, AlarmActivity::class.java,
+ instance.mId)
+ notification.setContentIntent(PendingIntent.getActivity(service,
+ ALARM_FIRING_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+
+ // Setup fullscreen intent
+ val fullScreenIntent: Intent =
+ AlarmInstance.createIntent(service, AlarmActivity::class.java, instance.mId)
+ // set action, so we can be different then content pending intent
+ fullScreenIntent.setAction("fullscreen_activity")
+ fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_NO_USER_ACTION)
+ notification.setFullScreenIntent(PendingIntent.getActivity(service,
+ ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT),
+ true)
+ notification.setPriority(NotificationCompat.PRIORITY_MAX)
+
+ clearNotification(service, instance)
+ service.startForeground(ALARM_FIRING_NOTIFICATION_ID, notification.build())
+ }
+
+ @JvmStatic
+ @Synchronized
+ fun clearNotification(context: Context, instance: AlarmInstance) {
+ LogUtils.v("Clearing notifications for alarm instance: " + instance.mId)
+ val nm: NotificationManagerCompat = NotificationManagerCompat.from(context)
+ val id = instance.hashCode()
+ nm.cancel(id)
+ updateUpcomingAlarmGroupNotification(context, id, null)
+ updateMissedAlarmGroupNotification(context, id, null)
+ }
+
+ /**
+ * Updates the notification for an existing alarm. Use if the label has changed.
+ */
+ @JvmStatic
+ fun updateNotification(context: Context, instance: AlarmInstance) {
+ when (instance.mAlarmState) {
+ InstancesColumns.LOW_NOTIFICATION_STATE -> {
+ showLowPriorityNotification(context, instance)
+ }
+ InstancesColumns.HIGH_NOTIFICATION_STATE -> {
+ showHighPriorityNotification(context, instance)
+ }
+ InstancesColumns.SNOOZE_STATE -> showSnoozeNotification(context, instance)
+ InstancesColumns.MISSED_STATE -> showMissedNotification(context, instance)
+ else -> LogUtils.d("No notification to update")
+ }
+ }
+
+ @JvmStatic
+ fun createViewAlarmIntent(context: Context?, instance: AlarmInstance): Intent {
+ val alarmId = instance.mAlarmId ?: Alarm.INVALID_ID
+ return Alarm.createIntent(context, DeskClock::class.java, alarmId)
+ .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ /**
+ * Alarm notifications are sorted chronologically. Missed alarms are sorted chronologically
+ * **after** all upcoming/snoozed alarms by including the "MISSED" prefix on the
+ * sort key.
+ *
+ * @param instance the alarm instance for which the notification is generated
+ * @return the sort key that specifies the order of this alarm notification
+ */
+ private fun createSortKey(instance: AlarmInstance): String {
+ val timeKey = SORT_KEY_FORMAT.format(instance.alarmTime.time)
+ val missedAlarm = instance.mAlarmState == InstancesColumns.MISSED_STATE
+ return if (missedAlarm) "MISSED $timeKey" else timeKey
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmService.java b/src/com/android/deskclock/alarms/AlarmService.java
deleted file mode 100644
index b9a97db..0000000
--- a/src/com/android/deskclock/alarms/AlarmService.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.alarms;
-
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Binder;
-import android.os.IBinder;
-import android.telephony.PhoneStateListener;
-import android.telephony.TelephonyManager;
-
-import com.android.deskclock.AlarmAlertWakeLock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.AlarmInstance;
-
-/**
- * This service is in charge of starting/stopping the alarm. It will bring up and manage the
- * {@link AlarmActivity} as well as {@link AlarmKlaxon}.
- *
- * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver
- * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents.
- */
-public class AlarmService extends Service {
- /**
- * AlarmActivity and AlarmService (when unbound) listen for this broadcast intent
- * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before
- * ALARM_DONE_ACTION).
- */
- public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
-
- /**
- * AlarmActivity and AlarmService listen for this broadcast intent so that other
- * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
- */
- public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
-
- /** A public action sent by AlarmService when the alarm has started. */
- public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT";
-
- /** A public action sent by AlarmService when the alarm has stopped for any reason. */
- public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE";
-
- /** Private action used to stop an alarm with this service. */
- public static final String STOP_ALARM_ACTION = "STOP_ALARM";
-
- /** Binder given to AlarmActivity. */
- private final IBinder mBinder = new Binder();
-
- /** Whether the service is currently bound to AlarmActivity */
- private boolean mIsBound = false;
-
- /** Listener for changes in phone state. */
- private final PhoneStateChangeListener mPhoneStateListener = new PhoneStateChangeListener();
-
- /** Whether the receiver is currently registered */
- private boolean mIsRegistered = false;
-
- @Override
- public IBinder onBind(Intent intent) {
- mIsBound = true;
- return mBinder;
- }
-
- @Override
- public boolean onUnbind(Intent intent) {
- mIsBound = false;
- return super.onUnbind(intent);
- }
-
- /**
- * Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
- * or using a different instance.
- *
- * @param context application context
- * @param instance you are trying to stop
- */
- public static void stopAlarm(Context context, AlarmInstance instance) {
- final Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId)
- .setAction(STOP_ALARM_ACTION);
-
- // We don't need a wake lock here, since we are trying to kill an alarm
- context.startService(intent);
- }
-
- private TelephonyManager mTelephonyManager;
- private AlarmInstance mCurrentAlarm = null;
-
- private void startAlarm(AlarmInstance instance) {
- LogUtils.v("AlarmService.start with instance: " + instance.mId);
- if (mCurrentAlarm != null) {
- AlarmStateManager.setMissedState(this, mCurrentAlarm);
- stopCurrentAlarm();
- }
-
- AlarmAlertWakeLock.acquireCpuWakeLock(this);
-
- mCurrentAlarm = instance;
- AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
- mTelephonyManager.listen(mPhoneStateListener.init(), PhoneStateListener.LISTEN_CALL_STATE);
- AlarmKlaxon.start(this, mCurrentAlarm);
- sendBroadcast(new Intent(ALARM_ALERT_ACTION));
- }
-
- private void stopCurrentAlarm() {
- if (mCurrentAlarm == null) {
- LogUtils.v("There is no current alarm to stop");
- return;
- }
-
- final long instanceId = mCurrentAlarm.mId;
- LogUtils.v("AlarmService.stop with instance: %s", instanceId);
-
- AlarmKlaxon.stop(this);
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
- sendBroadcast(new Intent(ALARM_DONE_ACTION));
-
- stopForeground(true /* removeNotification */);
-
- mCurrentAlarm = null;
- AlarmAlertWakeLock.releaseCpuLock();
- }
-
- private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- final String action = intent.getAction();
- LogUtils.i("AlarmService received intent %s", action);
- if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) {
- LogUtils.i("No valid firing alarm");
- return;
- }
-
- if (mIsBound) {
- LogUtils.i("AlarmActivity bound; AlarmService no-op");
- return;
- }
-
- switch (action) {
- case ALARM_SNOOZE_ACTION:
- // Set the alarm state to snoozed.
- // If this broadcast receiver is handling the snooze intent then AlarmActivity
- // must not be showing, so always show snooze toast.
- AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */);
- Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent);
- break;
- case ALARM_DISMISS_ACTION:
- // Set the alarm state to dismissed.
- AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm);
- Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent);
- break;
- }
- }
- };
-
- @Override
- public void onCreate() {
- super.onCreate();
- mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
-
- // Register the broadcast receiver
- final IntentFilter filter = new IntentFilter(ALARM_SNOOZE_ACTION);
- filter.addAction(ALARM_DISMISS_ACTION);
- registerReceiver(mActionsReceiver, filter);
- mIsRegistered = true;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- LogUtils.v("AlarmService.onStartCommand() with %s", intent);
- if (intent == null) {
- return Service.START_NOT_STICKY;
- }
-
- final long instanceId = AlarmInstance.getId(intent.getData());
- switch (intent.getAction()) {
- case AlarmStateManager.CHANGE_STATE_ACTION:
- AlarmStateManager.handleIntent(this, intent);
-
- // If state is changed to firing, actually fire the alarm!
- final int alarmState = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1);
- if (alarmState == AlarmInstance.FIRED_STATE) {
- final ContentResolver cr = this.getContentResolver();
- final AlarmInstance instance = AlarmInstance.getInstance(cr, instanceId);
- if (instance == null) {
- LogUtils.e("No instance found to start alarm: %d", instanceId);
- if (mCurrentAlarm != null) {
- // Only release lock if we are not firing alarm
- AlarmAlertWakeLock.releaseCpuLock();
- }
- break;
- }
-
- if (mCurrentAlarm != null && mCurrentAlarm.mId == instanceId) {
- LogUtils.e("Alarm already started for instance: %d", instanceId);
- break;
- }
- startAlarm(instance);
- }
- break;
- case STOP_ALARM_ACTION:
- if (mCurrentAlarm != null && mCurrentAlarm.mId != instanceId) {
- LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d",
- instanceId, mCurrentAlarm.mId);
- break;
- }
- stopCurrentAlarm();
- stopSelf();
- }
-
- return Service.START_NOT_STICKY;
- }
-
- @Override
- public void onDestroy() {
- LogUtils.v("AlarmService.onDestroy() called");
- super.onDestroy();
- if (mCurrentAlarm != null) {
- stopCurrentAlarm();
- }
-
- if (mIsRegistered) {
- unregisterReceiver(mActionsReceiver);
- mIsRegistered = false;
- }
- }
-
- private final class PhoneStateChangeListener extends PhoneStateListener {
-
- private int mPhoneCallState;
-
- PhoneStateChangeListener init() {
- mPhoneCallState = -1;
- return this;
- }
-
- @Override
- public void onCallStateChanged(int state, String ignored) {
- if (mPhoneCallState == -1) {
- mPhoneCallState = state;
- }
-
- if (state != TelephonyManager.CALL_STATE_IDLE && state != mPhoneCallState) {
- startService(AlarmStateManager.createStateChangeIntent(AlarmService.this,
- "AlarmService", mCurrentAlarm, AlarmInstance.MISSED_STATE));
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmService.kt b/src/com/android/deskclock/alarms/AlarmService.kt
new file mode 100644
index 0000000..8cd1651
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmService.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Binder
+import android.os.IBinder
+import android.telephony.PhoneStateListener
+import android.telephony.TelephonyManager
+
+import com.android.deskclock.AlarmAlertWakeLock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+/**
+ * This service is in charge of starting/stopping the alarm. It will bring up and manage the
+ * [AlarmActivity] as well as [AlarmKlaxon].
+ *
+ * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver
+ * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents.
+ */
+class AlarmService : Service() {
+ /** Binder given to AlarmActivity. */
+ private val mBinder: IBinder = Binder()
+
+ /** Whether the service is currently bound to AlarmActivity */
+ private var mIsBound = false
+
+ /** Listener for changes in phone state. */
+ private val mPhoneStateListener = PhoneStateChangeListener()
+
+ /** Whether the receiver is currently registered */
+ private var mIsRegistered = false
+
+ override fun onBind(intent: Intent?): IBinder {
+ mIsBound = true
+ return mBinder
+ }
+
+ override fun onUnbind(intent: Intent?): Boolean {
+ mIsBound = false
+ return super.onUnbind(intent)
+ }
+
+ private lateinit var mTelephonyManager: TelephonyManager
+ private var mCurrentAlarm: AlarmInstance? = null
+
+ private fun startAlarm(instance: AlarmInstance) {
+ LogUtils.v("AlarmService.start with instance: " + instance.mId)
+ if (mCurrentAlarm != null) {
+ AlarmStateManager.setMissedState(this, mCurrentAlarm!!)
+ stopCurrentAlarm()
+ }
+
+ AlarmAlertWakeLock.acquireCpuWakeLock(this)
+
+ mCurrentAlarm = instance
+ AlarmNotifications.showAlarmNotification(this, mCurrentAlarm!!)
+ mTelephonyManager.listen(mPhoneStateListener.init(), PhoneStateListener.LISTEN_CALL_STATE)
+ AlarmKlaxon.start(this, mCurrentAlarm!!)
+ sendBroadcast(Intent(ALARM_ALERT_ACTION))
+ }
+
+ private fun stopCurrentAlarm() {
+ if (mCurrentAlarm == null) {
+ LogUtils.v("There is no current alarm to stop")
+ return
+ }
+
+ val instanceId = mCurrentAlarm!!.mId
+ LogUtils.v("AlarmService.stop with instance: %s", instanceId)
+
+ AlarmKlaxon.stop(this)
+ mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE)
+ sendBroadcast(Intent(ALARM_DONE_ACTION))
+
+ stopForeground(true /* removeNotification */)
+
+ mCurrentAlarm = null
+ AlarmAlertWakeLock.releaseCpuLock()
+ }
+
+ private val mActionsReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val action: String? = intent.getAction()
+ LogUtils.i("AlarmService received intent %s", action)
+ if (mCurrentAlarm == null ||
+ mCurrentAlarm!!.mAlarmState != InstancesColumns.FIRED_STATE) {
+ LogUtils.i("No valid firing alarm")
+ return
+ }
+
+ if (mIsBound) {
+ LogUtils.i("AlarmActivity bound; AlarmService no-op")
+ return
+ }
+
+ when (action) {
+ ALARM_SNOOZE_ACTION -> {
+ // Set the alarm state to snoozed.
+ // If this broadcast receiver is handling the snooze intent then AlarmActivity
+ // must not be showing, so always show snooze toast.
+ AlarmStateManager.setSnoozeState(context, mCurrentAlarm!!, true /* showToast */)
+ Events.sendAlarmEvent(R.string.action_snooze, R.string.label_intent)
+ }
+ ALARM_DISMISS_ACTION -> {
+ // Set the alarm state to dismissed.
+ AlarmStateManager.deleteInstanceAndUpdateParent(context, mCurrentAlarm!!)
+ Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_intent)
+ }
+ }
+ }
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ mTelephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+
+ // Register the broadcast receiver
+ val filter = IntentFilter(ALARM_SNOOZE_ACTION)
+ filter.addAction(ALARM_DISMISS_ACTION)
+ registerReceiver(mActionsReceiver, filter)
+ mIsRegistered = true
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ LogUtils.v("AlarmService.onStartCommand() with %s", intent)
+ if (intent == null) {
+ return Service.START_NOT_STICKY
+ }
+
+ val instanceId = AlarmInstance.getId(intent.getData()!!)
+ when (intent.getAction()) {
+ AlarmStateManager.CHANGE_STATE_ACTION -> {
+ AlarmStateManager.handleIntent(this, intent)
+
+ // If state is changed to firing, actually fire the alarm!
+ val alarmState: Int = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1)
+ if (alarmState == InstancesColumns.FIRED_STATE) {
+ val cr: ContentResolver = this.getContentResolver()
+ val instance: AlarmInstance? = AlarmInstance.getInstance(cr, instanceId)
+ if (instance == null) {
+ LogUtils.e("No instance found to start alarm: %d", instanceId)
+ if (mCurrentAlarm != null) {
+ // Only release lock if we are not firing alarm
+ AlarmAlertWakeLock.releaseCpuLock()
+ }
+ } else if (mCurrentAlarm != null && mCurrentAlarm!!.mId == instanceId) {
+ LogUtils.e("Alarm already started for instance: %d", instanceId)
+ } else {
+ startAlarm(instance)
+ }
+ }
+ }
+ STOP_ALARM_ACTION -> {
+ if (mCurrentAlarm != null && mCurrentAlarm!!.mId != instanceId) {
+ LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d",
+ instanceId, mCurrentAlarm!!.mId)
+ } else {
+ stopCurrentAlarm()
+ stopSelf()
+ }
+ }
+ }
+
+ return Service.START_NOT_STICKY
+ }
+
+ override fun onDestroy() {
+ LogUtils.v("AlarmService.onDestroy() called")
+ super.onDestroy()
+ if (mCurrentAlarm != null) {
+ stopCurrentAlarm()
+ }
+
+ if (mIsRegistered) {
+ unregisterReceiver(mActionsReceiver)
+ mIsRegistered = false
+ }
+ }
+
+ private inner class PhoneStateChangeListener : PhoneStateListener() {
+ private var mPhoneCallState = 0
+
+ fun init(): PhoneStateChangeListener {
+ mPhoneCallState = -1
+ return this
+ }
+
+ override fun onCallStateChanged(state: Int, ignored: String?) {
+ if (mPhoneCallState == -1) {
+ mPhoneCallState = state
+ }
+
+ if (state != TelephonyManager.CALL_STATE_IDLE && state != mPhoneCallState) {
+ startService(AlarmStateManager.createStateChangeIntent(this@AlarmService,
+ "AlarmService", mCurrentAlarm!!, InstancesColumns.MISSED_STATE))
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * AlarmActivity and AlarmService (when unbound) listen for this broadcast intent
+ * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before
+ * ALARM_DONE_ACTION).
+ */
+ const val ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE"
+
+ /**
+ * AlarmActivity and AlarmService listen for this broadcast intent so that other
+ * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
+ */
+ const val ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS"
+
+ /** A public action sent by AlarmService when the alarm has started. */
+ const val ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT"
+
+ /** A public action sent by AlarmService when the alarm has stopped for any reason. */
+ const val ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE"
+
+ /** Private action used to stop an alarm with this service. */
+ const val STOP_ALARM_ACTION = "STOP_ALARM"
+
+ /**
+ * Utility method to help stop an alarm properly. Nothing will happen, if alarm is not firing
+ * or using a different instance.
+ *
+ * @param context application context
+ * @param instance you are trying to stop
+ */
+ @JvmStatic
+ fun stopAlarm(context: Context, instance: AlarmInstance) {
+ val intent: Intent =
+ AlarmInstance.createIntent(context, AlarmService::class.java, instance.mId)
+ .setAction(STOP_ALARM_ACTION)
+
+ // We don't need a wake lock here, since we are trying to kill an alarm
+ context.startService(intent)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmStateManager.java b/src/com/android/deskclock/alarms/AlarmStateManager.java
deleted file mode 100644
index 0f70a0f..0000000
--- a/src/com/android/deskclock/alarms/AlarmStateManager.java
+++ /dev/null
@@ -1,1037 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.alarms;
-
-import android.annotation.TargetApi;
-import android.app.AlarmManager;
-import android.app.AlarmManager.AlarmClockInfo;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.provider.Settings;
-import androidx.core.app.NotificationManagerCompat;
-import android.text.format.DateFormat;
-import android.widget.Toast;
-
-import com.android.deskclock.AlarmAlertWakeLock;
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.AsyncHandler;
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-import static android.content.Context.ALARM_SERVICE;
-import static android.provider.Settings.System.NEXT_ALARM_FORMATTED;
-
-/**
- * This class handles all the state changes for alarm instances. You need to
- * register all alarm instances with the state manager if you want them to
- * be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE),
- * then you must also re-register instances to fix their states.
- *
- * Please see {@link #registerInstance) for special transitions when major time changes
- * occur.
- *
- * Following states:
- *
- * SILENT_STATE:
- * This state is used when the alarm is activated, but doesn't need to display anything. It
- * is in charge of changing the alarm instance state to a LOW_NOTIFICATION_STATE.
- *
- * LOW_NOTIFICATION_STATE:
- * This state is used to notify the user that the alarm will go off
- * {@link AlarmInstance#LOW_NOTIFICATION_HOUR_OFFSET}. This
- * state handles the state changes to HIGH_NOTIFICATION_STATE, HIDE_NOTIFICATION_STATE and
- * DISMISS_STATE.
- *
- * HIDE_NOTIFICATION_STATE:
- * This is a transient state of the LOW_NOTIFICATION_STATE, where the user wants to hide the
- * notification. This will sit and wait until the HIGH_PRIORITY_NOTIFICATION should go off.
- *
- * HIGH_NOTIFICATION_STATE:
- * This state behaves like the LOW_NOTIFICATION_STATE, but doesn't allow the user to hide it.
- * This state is in charge of triggering a FIRED_STATE or DISMISS_STATE.
- *
- * SNOOZED_STATE:
- * The SNOOZED_STATE behaves like a HIGH_NOTIFICATION_STATE, but with a different message. It
- * also increments the alarm time in the instance to reflect the new snooze time.
- *
- * FIRED_STATE:
- * The FIRED_STATE is used when the alarm is firing. It will start the AlarmService, and wait
- * until the user interacts with the alarm via SNOOZED_STATE or DISMISS_STATE change. If the user
- * doesn't then it might be change to MISSED_STATE if auto-silenced was enabled.
- *
- * MISSED_STATE:
- * The MISSED_STATE is used when the alarm already fired, but the user could not interact with
- * it. At this point the alarm instance is dead and we check the parent alarm to see if we need
- * to disable or schedule a new alarm_instance. There is also a notification shown to the user
- * that he/she missed the alarm and that stays for
- * {@link AlarmInstance#MISSED_TIME_TO_LIVE_HOUR_OFFSET} or until the user acknownledges it.
- *
- * DISMISS_STATE:
- * This is really a transient state that will properly delete the alarm instance. Use this state,
- * whenever you want to get rid of the alarm instance. This state will also check the alarm
- * parent to see if it should disable or schedule a new alarm instance.
- */
-public final class AlarmStateManager extends BroadcastReceiver {
- // Intent action to trigger an instance state change.
- public static final String CHANGE_STATE_ACTION = "change_state";
-
- // Intent action to show the alarm and dismiss the instance
- public static final String SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm";
-
- // Intent action for an AlarmManager alarm serving only to set the next alarm indicators
- private static final String INDICATOR_ACTION = "indicator";
-
- // System intent action to notify AppWidget that we changed the alarm text.
- public static final String ACTION_ALARM_CHANGED = "com.android.deskclock.ALARM_CHANGED";
-
- // Extra key to set the desired state change.
- public static final String ALARM_STATE_EXTRA = "intent.extra.alarm.state";
-
- // Extra key to indicate the state change was launched from a notification.
- public static final String FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification";
-
- // Extra key to set the global broadcast id.
- private static final String ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id";
-
- // Intent category tags used to dismiss, snooze or delete an alarm
- public static final String ALARM_DISMISS_TAG = "DISMISS_TAG";
- public static final String ALARM_SNOOZE_TAG = "SNOOZE_TAG";
- public static final String ALARM_DELETE_TAG = "DELETE_TAG";
-
- // Intent category tag used when schedule state change intents in alarm manager.
- private static final String ALARM_MANAGER_TAG = "ALARM_MANAGER";
-
- // Buffer time in seconds to fire alarm instead of marking it missed.
- public static final int ALARM_FIRE_BUFFER = 15;
-
- // A factory for the current time; can be mocked for testing purposes.
- private static CurrentTimeFactory sCurrentTimeFactory;
-
- // Schedules alarm state transitions; can be mocked for testing purposes.
- private static StateChangeScheduler sStateChangeScheduler =
- new AlarmManagerStateChangeScheduler();
-
- private static Calendar getCurrentTime() {
- return sCurrentTimeFactory == null
- ? DataModel.getDataModel().getCalendar()
- : sCurrentTimeFactory.getCurrentTime();
- }
-
- static void setCurrentTimeFactory(CurrentTimeFactory currentTimeFactory) {
- sCurrentTimeFactory = currentTimeFactory;
- }
-
- static void setStateChangeScheduler(StateChangeScheduler stateChangeScheduler) {
- if (stateChangeScheduler == null) {
- stateChangeScheduler = new AlarmManagerStateChangeScheduler();
- }
- sStateChangeScheduler = stateChangeScheduler;
- }
-
- /**
- * Update the next alarm stored in framework. This value is also displayed in digital widgets
- * and the clock tab in this app.
- */
- private static void updateNextAlarm(Context context) {
- final AlarmInstance nextAlarm = getNextFiringAlarm(context);
-
- if (Utils.isPreL()) {
- updateNextAlarmInSystemSettings(context, nextAlarm);
- } else {
- updateNextAlarmInAlarmManager(context, nextAlarm);
- }
- }
-
- /**
- * Returns an alarm instance of an alarm that's going to fire next.
- *
- * @param context application context
- * @return an alarm instance that will fire earliest relative to current time.
- */
- public static AlarmInstance getNextFiringAlarm(Context context) {
- final ContentResolver cr = context.getContentResolver();
- final String activeAlarmQuery = AlarmInstance.ALARM_STATE + "<" + AlarmInstance.FIRED_STATE;
- final List<AlarmInstance> alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery);
-
- AlarmInstance nextAlarm = null;
- for (AlarmInstance instance : alarmInstances) {
- if (nextAlarm == null || instance.getAlarmTime().before(nextAlarm.getAlarmTime())) {
- nextAlarm = instance;
- }
- }
- return nextAlarm;
- }
-
- /**
- * Used in pre-L devices, where "next alarm" is stored in system settings.
- */
- @SuppressWarnings("deprecation")
- @TargetApi(Build.VERSION_CODES.KITKAT)
- private static void updateNextAlarmInSystemSettings(Context context, AlarmInstance nextAlarm) {
- // Format the next alarm time if an alarm is scheduled.
- String time = "";
- if (nextAlarm != null) {
- time = AlarmUtils.getFormattedTime(context, nextAlarm.getAlarmTime());
- }
-
- try {
- // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions
- Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time);
-
- LogUtils.i("Updated next alarm time to: \'" + time + '\'');
-
- // Send broadcast message so pre-L AppWidgets will recognize an update.
- context.sendBroadcast(new Intent(ACTION_ALARM_CHANGED));
- } catch (SecurityException se) {
- // The user has most likely revoked WRITE_SETTINGS.
- LogUtils.e("Unable to update next alarm to: \'" + time + '\'', se);
- }
- }
-
- /**
- * Used in L and later devices where "next alarm" is stored in the Alarm Manager.
- */
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private static void updateNextAlarmInAlarmManager(Context context, AlarmInstance nextAlarm) {
- // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the
- // alarm that is going to fire next. The operation is constructed such that it is ignored
- // by AlarmStateManager.
-
- final AlarmManager alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
-
- final int flags = nextAlarm == null ? PendingIntent.FLAG_NO_CREATE : 0;
- final PendingIntent operation = PendingIntent.getBroadcast(context, 0 /* requestCode */,
- AlarmStateManager.createIndicatorIntent(context), flags);
-
- if (nextAlarm != null) {
- LogUtils.i("Setting upcoming AlarmClockInfo for alarm: " + nextAlarm.mId);
- long alarmTime = nextAlarm.getAlarmTime().getTimeInMillis();
-
- // Create an intent that can be used to show or edit details of the next alarm.
- PendingIntent viewIntent = PendingIntent.getActivity(context, nextAlarm.hashCode(),
- AlarmNotifications.createViewAlarmIntent(context, nextAlarm),
- PendingIntent.FLAG_UPDATE_CURRENT);
-
- final AlarmClockInfo info = new AlarmClockInfo(alarmTime, viewIntent);
- Utils.updateNextAlarm(alarmManager, info, operation);
- } else if (operation != null) {
- LogUtils.i("Canceling upcoming AlarmClockInfo");
- alarmManager.cancel(operation);
- }
- }
-
- /**
- * Used by dismissed and missed states, to update parent alarm. This will either
- * disable, delete or reschedule parent alarm.
- *
- * @param context application context
- * @param instance to update parent for
- */
- private static void updateParentAlarm(Context context, AlarmInstance instance) {
- ContentResolver cr = context.getContentResolver();
- Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
- if (alarm == null) {
- LogUtils.e("Parent has been deleted with instance: " + instance.toString());
- return;
- }
-
- if (!alarm.daysOfWeek.isRepeating()) {
- if (alarm.deleteAfterUse) {
- LogUtils.i("Deleting parent alarm: " + alarm.id);
- Alarm.deleteAlarm(cr, alarm.id);
- } else {
- LogUtils.i("Disabling parent alarm: " + alarm.id);
- alarm.enabled = false;
- Alarm.updateAlarm(cr, alarm);
- }
- } else {
- // Schedule the next repeating instance which may be before the current instance if a
- // time jump has occurred. Otherwise, if the current instance is the next instance
- // and has already been fired, schedule the subsequent instance.
- AlarmInstance nextRepeatedInstance = alarm.createInstanceAfter(getCurrentTime());
- if (instance.mAlarmState > AlarmInstance.FIRED_STATE
- && nextRepeatedInstance.getAlarmTime().equals(instance.getAlarmTime())) {
- nextRepeatedInstance = alarm.createInstanceAfter(instance.getAlarmTime());
- }
-
- LogUtils.i("Creating new instance for repeating alarm " + alarm.id + " at " +
- AlarmUtils.getFormattedTime(context, nextRepeatedInstance.getAlarmTime()));
- AlarmInstance.addInstance(cr, nextRepeatedInstance);
- registerInstance(context, nextRepeatedInstance, true);
- }
- }
-
- /**
- * Utility method to create a proper change state intent.
- *
- * @param context application context
- * @param tag used to make intent differ from other state change intents.
- * @param instance to change state to
- * @param state to change to.
- * @return intent that can be used to change an alarm instance state
- */
- public static Intent createStateChangeIntent(Context context, String tag,
- AlarmInstance instance, Integer state) {
- // This intent is directed to AlarmService, though the actual handling of it occurs here
- // in AlarmStateManager. The reason is that evidence exists showing the jump between the
- // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by the
- // Out Of Memory killer. If clock is killed during that jump, firing an alarm can fail to
- // occur. To be safer, the call begins in AlarmService, which has the power to display the
- // firing alarm if needed, so no jump is needed.
- Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId);
- intent.setAction(CHANGE_STATE_ACTION);
- intent.addCategory(tag);
- intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.getDataModel().getGlobalIntentId());
- if (state != null) {
- intent.putExtra(ALARM_STATE_EXTRA, state.intValue());
- }
- return intent;
- }
-
- /**
- * Schedule alarm instance state changes with {@link AlarmManager}.
- *
- * @param ctx application context
- * @param time to trigger state change
- * @param instance to change state to
- * @param newState to change to
- */
- private static void scheduleInstanceStateChange(Context ctx, Calendar time,
- AlarmInstance instance, int newState) {
- sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState);
- }
-
- /**
- * Cancel all {@link AlarmManager} timers for instance.
- *
- * @param ctx application context
- * @param instance to disable all {@link AlarmManager} timers
- */
- private static void cancelScheduledInstanceStateChange(Context ctx, AlarmInstance instance) {
- sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance);
- }
-
-
- /**
- * This will set the alarm instance to the SILENT_STATE and update
- * the application notifications and schedule any state changes that need
- * to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setSilentState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting silent state to instance " + instance.mId);
-
- // Update alarm in db
- ContentResolver contentResolver = context.getContentResolver();
- instance.mAlarmState = AlarmInstance.SILENT_STATE;
- AlarmInstance.updateInstance(contentResolver, instance);
-
- // Setup instance notification and scheduling timers
- AlarmNotifications.clearNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getLowNotificationTime(),
- instance, AlarmInstance.LOW_NOTIFICATION_STATE);
- }
-
- /**
- * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update
- * the application notifications and schedule any state changes that need
- * to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setLowNotificationState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting low notification state to instance " + instance.mId);
-
- // Update alarm state in db
- ContentResolver contentResolver = context.getContentResolver();
- instance.mAlarmState = AlarmInstance.LOW_NOTIFICATION_STATE;
- AlarmInstance.updateInstance(contentResolver, instance);
-
- // Setup instance notification and scheduling timers
- AlarmNotifications.showLowPriorityNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
- instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
- }
-
- /**
- * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update
- * the application notifications and schedule any state changes that need
- * to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setHideNotificationState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting hide notification state to instance " + instance.mId);
-
- // Update alarm state in db
- ContentResolver contentResolver = context.getContentResolver();
- instance.mAlarmState = AlarmInstance.HIDE_NOTIFICATION_STATE;
- AlarmInstance.updateInstance(contentResolver, instance);
-
- // Setup instance notification and scheduling timers
- AlarmNotifications.clearNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getHighNotificationTime(),
- instance, AlarmInstance.HIGH_NOTIFICATION_STATE);
- }
-
- /**
- * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update
- * the application notifications and schedule any state changes that need
- * to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setHighNotificationState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting high notification state to instance " + instance.mId);
-
- // Update alarm state in db
- ContentResolver contentResolver = context.getContentResolver();
- instance.mAlarmState = AlarmInstance.HIGH_NOTIFICATION_STATE;
- AlarmInstance.updateInstance(contentResolver, instance);
-
- // Setup instance notification and scheduling timers
- AlarmNotifications.showHighPriorityNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getAlarmTime(),
- instance, AlarmInstance.FIRED_STATE);
- }
-
- /**
- * This will set the alarm instance to the FIRED_STATE and update
- * the application notifications and schedule any state changes that need
- * to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setFiredState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting fire state to instance " + instance.mId);
-
- // Update alarm state in db
- ContentResolver contentResolver = context.getContentResolver();
- instance.mAlarmState = AlarmInstance.FIRED_STATE;
- AlarmInstance.updateInstance(contentResolver, instance);
-
- if (instance.mAlarmId != null) {
- // if the time changed *backward* and pushed an instance from missed back to fired,
- // remove any other scheduled instances that may exist
- AlarmInstance.deleteOtherInstances(context, contentResolver, instance.mAlarmId,
- instance.mId);
- }
-
- Events.sendAlarmEvent(R.string.action_fire, 0);
-
- Calendar timeout = instance.getTimeout();
- if (timeout != null) {
- scheduleInstanceStateChange(context, timeout, instance, AlarmInstance.MISSED_STATE);
- }
-
- // Instance not valid anymore, so find next alarm that will fire and notify system
- updateNextAlarm(context);
- }
-
- /**
- * This will set the alarm instance to the SNOOZE_STATE and update
- * the application notifications and schedule any state changes that need
- * to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setSnoozeState(final Context context, AlarmInstance instance,
- boolean showToast) {
- // Stop alarm if this instance is firing it
- AlarmService.stopAlarm(context, instance);
-
- // Calculate the new snooze alarm time
- final int snoozeMinutes = DataModel.getDataModel().getSnoozeLength();
- Calendar newAlarmTime = Calendar.getInstance();
- newAlarmTime.add(Calendar.MINUTE, snoozeMinutes);
-
- // Update alarm state and new alarm time in db.
- LogUtils.i("Setting snoozed state to instance " + instance.mId + " for "
- + AlarmUtils.getFormattedTime(context, newAlarmTime));
- instance.setAlarmTime(newAlarmTime);
- instance.mAlarmState = AlarmInstance.SNOOZE_STATE;
- AlarmInstance.updateInstance(context.getContentResolver(), instance);
-
- // Setup instance notification and scheduling timers
- AlarmNotifications.showSnoozeNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getAlarmTime(),
- instance, AlarmInstance.FIRED_STATE);
-
- // Display the snooze minutes in a toast.
- if (showToast) {
- final Handler mainHandler = new Handler(context.getMainLooper());
- final Runnable myRunnable = new Runnable() {
- @Override
- public void run() {
- String displayTime = String.format(context.getResources().getQuantityText
- (R.plurals.alarm_alert_snooze_set, snoozeMinutes).toString(),
- snoozeMinutes);
- Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show();
- }
- };
- mainHandler.post(myRunnable);
- }
-
- // Instance time changed, so find next alarm that will fire and notify system
- updateNextAlarm(context);
- }
-
- /**
- * This will set the alarm instance to the MISSED_STATE and update
- * the application notifications and schedule any state changes that need
- * to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setMissedState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting missed state to instance " + instance.mId);
- // Stop alarm if this instance is firing it
- AlarmService.stopAlarm(context, instance);
-
- // Check parent if it needs to reschedule, disable or delete itself
- if (instance.mAlarmId != null) {
- updateParentAlarm(context, instance);
- }
-
- // Update alarm state
- ContentResolver contentResolver = context.getContentResolver();
- instance.mAlarmState = AlarmInstance.MISSED_STATE;
- AlarmInstance.updateInstance(contentResolver, instance);
-
- // Setup instance notification and scheduling timers
- AlarmNotifications.showMissedNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getMissedTimeToLive(),
- instance, AlarmInstance.DISMISSED_STATE);
-
- // Instance is not valid anymore, so find next alarm that will fire and notify system
- updateNextAlarm(context);
- }
-
- /**
- * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state
- * change to DISMISSED_STATE at the regularly scheduled firing time.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void setPreDismissState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting predismissed state to instance " + instance.mId);
-
- // Update alarm in db
- final ContentResolver contentResolver = context.getContentResolver();
- instance.mAlarmState = AlarmInstance.PREDISMISSED_STATE;
- AlarmInstance.updateInstance(contentResolver, instance);
-
- // Setup instance notification and scheduling timers
- AlarmNotifications.clearNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getAlarmTime(), instance,
- AlarmInstance.DISMISSED_STATE);
-
- // Check parent if it needs to reschedule, disable or delete itself
- if (instance.mAlarmId != null) {
- updateParentAlarm(context, instance);
- }
-
- updateNextAlarm(context);
- }
-
- /**
- * This just sets the alarm instance to DISMISSED_STATE.
- */
- public static void setDismissState(Context context, AlarmInstance instance) {
- LogUtils.i("Setting dismissed state to instance " + instance.mId);
- instance.mAlarmState = AlarmInstance.DISMISSED_STATE;
- final ContentResolver contentResolver = context.getContentResolver();
- AlarmInstance.updateInstance(contentResolver, instance);
- }
-
- /**
- * This will delete the alarm instance, update the application notifications, and schedule
- * any state changes that need to occur in the future.
- *
- * @param context application context
- * @param instance to set state to
- */
- public static void deleteInstanceAndUpdateParent(Context context, AlarmInstance instance) {
- LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm.");
-
- // Remove all other timers and notifications associated to it
- unregisterInstance(context, instance);
-
- // Check parent if it needs to reschedule, disable or delete itself
- if (instance.mAlarmId != null) {
- updateParentAlarm(context, instance);
- }
-
- // Delete instance as it is not needed anymore
- AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
-
- // Instance is not valid anymore, so find next alarm that will fire and notify system
- updateNextAlarm(context);
- }
-
- /**
- * This will set the instance state to DISMISSED_STATE and remove its notifications and
- * alarm timers.
- *
- * @param context application context
- * @param instance to unregister
- */
- public static void unregisterInstance(Context context, AlarmInstance instance) {
- LogUtils.i("Unregistering instance " + instance.mId);
- // Stop alarm if this instance is firing it
- AlarmService.stopAlarm(context, instance);
- AlarmNotifications.clearNotification(context, instance);
- cancelScheduledInstanceStateChange(context, instance);
- setDismissState(context, instance);
- }
-
- /**
- * This registers the AlarmInstance to the state manager. This will look at the instance
- * and choose the most appropriate state to put it in. This is primarily used by new
- * alarms, but it can also be called when the system time changes.
- *
- * Most state changes are handled by the states themselves, but during major time changes we
- * have to correct the alarm instance state. This means we have to handle special cases as
- * describe below:
- *
- * <ul>
- * <li>Make sure all dismissed alarms are never re-activated</li>
- * <li>Make sure pre-dismissed alarms stay predismissed</li>
- * <li>Make sure firing alarms stayed fired unless they should be auto-silenced</li>
- * <li>Missed instance that have parents should be re-enabled if we went back in time</li>
- * <li>If alarm was SNOOZED, then show the notification but don't update time</li>
- * <li>If low priority notification was hidden, then make sure it stays hidden</li>
- * </ul>
- *
- * If none of these special case are found, then we just check the time and see what is the
- * proper state for the instance.
- *
- * @param context application context
- * @param instance to register
- */
- public static void registerInstance(Context context, AlarmInstance instance,
- boolean updateNextAlarm) {
- LogUtils.i("Registering instance: " + instance.mId);
- final ContentResolver cr = context.getContentResolver();
- final Alarm alarm = Alarm.getAlarm(cr, instance.mAlarmId);
- final Calendar currentTime = getCurrentTime();
- final Calendar alarmTime = instance.getAlarmTime();
- final Calendar timeoutTime = instance.getTimeout();
- final Calendar lowNotificationTime = instance.getLowNotificationTime();
- final Calendar highNotificationTime = instance.getHighNotificationTime();
- final Calendar missedTTL = instance.getMissedTimeToLive();
-
- // Handle special use cases here
- if (instance.mAlarmState == AlarmInstance.DISMISSED_STATE) {
- // This should never happen, but add a quick check here
- LogUtils.e("Alarm Instance is dismissed, but never deleted");
- deleteInstanceAndUpdateParent(context, instance);
- return;
- } else if (instance.mAlarmState == AlarmInstance.FIRED_STATE) {
- // Keep alarm firing, unless it should be timed out
- boolean hasTimeout = timeoutTime != null && currentTime.after(timeoutTime);
- if (!hasTimeout) {
- setFiredState(context, instance);
- return;
- }
- } else if (instance.mAlarmState == AlarmInstance.MISSED_STATE) {
- if (currentTime.before(alarmTime)) {
- if (instance.mAlarmId == null) {
- LogUtils.i("Cannot restore missed instance for one-time alarm");
- // This instance parent got deleted (ie. deleteAfterUse), so
- // we should not re-activate it.-
- deleteInstanceAndUpdateParent(context, instance);
- return;
- }
-
- // TODO: This will re-activate missed snoozed alarms, but will
- // use our normal notifications. This is not ideal, but very rare use-case.
- // We should look into fixing this in the future.
-
- // Make sure we re-enable the parent alarm of the instance
- // because it will get activated by by the below code
- alarm.enabled = true;
- Alarm.updateAlarm(cr, alarm);
- }
- } else if (instance.mAlarmState == AlarmInstance.PREDISMISSED_STATE) {
- if (currentTime.before(alarmTime)) {
- setPreDismissState(context, instance);
- } else {
- deleteInstanceAndUpdateParent(context, instance);
- }
- return;
- }
-
- // Fix states that are time sensitive
- if (currentTime.after(missedTTL)) {
- // Alarm is so old, just dismiss it
- deleteInstanceAndUpdateParent(context, instance);
- } else if (currentTime.after(alarmTime)) {
- // There is a chance that the TIME_SET occurred right when the alarm should go off, so
- // we need to add a check to see if we should fire the alarm instead of marking it
- // missed.
- Calendar alarmBuffer = Calendar.getInstance();
- alarmBuffer.setTime(alarmTime.getTime());
- alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER);
- if (currentTime.before(alarmBuffer)) {
- setFiredState(context, instance);
- } else {
- setMissedState(context, instance);
- }
- } else if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) {
- // We only want to display snooze notification and not update the time,
- // so handle showing the notification directly
- AlarmNotifications.showSnoozeNotification(context, instance);
- scheduleInstanceStateChange(context, instance.getAlarmTime(),
- instance, AlarmInstance.FIRED_STATE);
- } else if (currentTime.after(highNotificationTime)) {
- setHighNotificationState(context, instance);
- } else if (currentTime.after(lowNotificationTime)) {
- // Only show low notification if it wasn't hidden in the past
- if (instance.mAlarmState == AlarmInstance.HIDE_NOTIFICATION_STATE) {
- setHideNotificationState(context, instance);
- } else {
- setLowNotificationState(context, instance);
- }
- } else {
- // Alarm is still active, so initialize as a silent alarm
- setSilentState(context, instance);
- }
-
- // The caller prefers to handle updateNextAlarm for optimization
- if (updateNextAlarm) {
- updateNextAlarm(context);
- }
- }
-
- /**
- * This will delete and unregister all instances associated with alarmId, without affect
- * the alarm itself. This should be used whenever modifying or deleting an alarm.
- *
- * @param context application context
- * @param alarmId to find instances to delete.
- */
- public static void deleteAllInstances(Context context, long alarmId) {
- LogUtils.i("Deleting all instances of alarm: " + alarmId);
- ContentResolver cr = context.getContentResolver();
- List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId);
- for (AlarmInstance instance : instances) {
- unregisterInstance(context, instance);
- AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
- }
- updateNextAlarm(context);
- }
-
- /**
- * Delete and unregister all instances unless they are snoozed. This is used whenever an alarm
- * is modified superficially (label, vibrate, or ringtone change).
- */
- public static void deleteNonSnoozeInstances(Context context, long alarmId) {
- LogUtils.i("Deleting all non-snooze instances of alarm: " + alarmId);
- ContentResolver cr = context.getContentResolver();
- List<AlarmInstance> instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId);
- for (AlarmInstance instance : instances) {
- if (instance.mAlarmState == AlarmInstance.SNOOZE_STATE) {
- continue;
- }
- unregisterInstance(context, instance);
- AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId);
- }
- updateNextAlarm(context);
- }
-
- /**
- * Fix and update all alarm instance when a time change event occurs.
- *
- * @param context application context
- */
- public static void fixAlarmInstances(Context context) {
- LogUtils.i("Fixing alarm instances");
- // Register all instances after major time changes or when phone restarts
- final ContentResolver contentResolver = context.getContentResolver();
- final Calendar currentTime = getCurrentTime();
-
- // Sort the instances in reverse chronological order so that later instances are fixed or
- // deleted before re-scheduling prior instances (which may re-create or update the later
- // instances).
- final List<AlarmInstance> instances = AlarmInstance.getInstances(
- contentResolver, null /* selection */);
- Collections.sort(instances, new Comparator<AlarmInstance>() {
- @Override
- public int compare(AlarmInstance lhs, AlarmInstance rhs) {
- return rhs.getAlarmTime().compareTo(lhs.getAlarmTime());
- }
- });
-
- for (AlarmInstance instance : instances) {
- final Alarm alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId);
- if (alarm == null) {
- unregisterInstance(context, instance);
- AlarmInstance.deleteInstance(contentResolver, instance.mId);
- LogUtils.e("Found instance without matching alarm; deleting instance %s", instance);
- continue;
- }
- final Calendar priorAlarmTime = alarm.getPreviousAlarmTime(instance.getAlarmTime());
- final Calendar missedTTLTime = instance.getMissedTimeToLive();
- if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) {
- final Calendar oldAlarmTime = instance.getAlarmTime();
- final Calendar newAlarmTime = alarm.getNextAlarmTime(currentTime);
- final CharSequence oldTime = DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime);
- final CharSequence newTime = DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime);
- LogUtils.i("A time change has caused an existing alarm scheduled to fire at %s to" +
- " be replaced by a new alarm scheduled to fire at %s", oldTime, newTime);
-
- // The time change is so dramatic the AlarmInstance doesn't make any sense;
- // remove it and schedule the new appropriate instance.
- AlarmStateManager.deleteInstanceAndUpdateParent(context, instance);
- } else {
- registerInstance(context, instance, false /* updateNextAlarm */);
- }
- }
-
- updateNextAlarm(context);
- }
-
- /**
- * Utility method to set alarm instance state via constants.
- *
- * @param context application context
- * @param instance to change state on
- * @param state to change to
- */
- private static void setAlarmState(Context context, AlarmInstance instance, int state) {
- if (instance == null) {
- LogUtils.e("Null alarm instance while setting state to %d", state);
- return;
- }
- switch (state) {
- case AlarmInstance.SILENT_STATE:
- setSilentState(context, instance);
- break;
- case AlarmInstance.LOW_NOTIFICATION_STATE:
- setLowNotificationState(context, instance);
- break;
- case AlarmInstance.HIDE_NOTIFICATION_STATE:
- setHideNotificationState(context, instance);
- break;
- case AlarmInstance.HIGH_NOTIFICATION_STATE:
- setHighNotificationState(context, instance);
- break;
- case AlarmInstance.FIRED_STATE:
- setFiredState(context, instance);
- break;
- case AlarmInstance.SNOOZE_STATE:
- setSnoozeState(context, instance, true /* showToast */);
- break;
- case AlarmInstance.MISSED_STATE:
- setMissedState(context, instance);
- break;
- case AlarmInstance.PREDISMISSED_STATE:
- setPreDismissState(context, instance);
- break;
- case AlarmInstance.DISMISSED_STATE:
- deleteInstanceAndUpdateParent(context, instance);
- break;
- default:
- LogUtils.e("Trying to change to unknown alarm state: " + state);
- }
- }
-
- @Override
- public void onReceive(final Context context, final Intent intent) {
- if (INDICATOR_ACTION.equals(intent.getAction())) {
- return;
- }
-
- final PendingResult result = goAsync();
- final PowerManager.WakeLock wl = AlarmAlertWakeLock.createPartialWakeLock(context);
- wl.acquire();
- AsyncHandler.post(new Runnable() {
- @Override
- public void run() {
- handleIntent(context, intent);
- result.finish();
- wl.release();
- }
- });
- }
-
- public static void handleIntent(Context context, Intent intent) {
- final String action = intent.getAction();
- LogUtils.v("AlarmStateManager received intent " + intent);
- if (CHANGE_STATE_ACTION.equals(action)) {
- Uri uri = intent.getData();
- AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
- AlarmInstance.getId(uri));
- if (instance == null) {
- LogUtils.e("Can not change state for unknown instance: " + uri);
- return;
- }
-
- int globalId = DataModel.getDataModel().getGlobalIntentId();
- int intentId = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1);
- int alarmState = intent.getIntExtra(ALARM_STATE_EXTRA, -1);
- if (intentId != globalId) {
- LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId + " AlarmState: " +
- alarmState);
- // Allows dismiss/snooze requests to go through
- if (!intent.hasCategory(ALARM_DISMISS_TAG) &&
- !intent.hasCategory(ALARM_SNOOZE_TAG)) {
- LogUtils.i("Ignoring old Intent");
- return;
- }
- }
-
- if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
- if (intent.hasCategory(ALARM_DISMISS_TAG)) {
- Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification);
- } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) {
- Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification);
- }
- }
-
- if (alarmState >= 0) {
- setAlarmState(context, instance, alarmState);
- } else {
- registerInstance(context, instance, true);
- }
- } else if (SHOW_AND_DISMISS_ALARM_ACTION.equals(action)) {
- Uri uri = intent.getData();
- AlarmInstance instance = AlarmInstance.getInstance(context.getContentResolver(),
- AlarmInstance.getId(uri));
-
- if (instance == null) {
- LogUtils.e("Null alarminstance for SHOW_AND_DISMISS");
- // dismiss the notification
- final int id = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1);
- if (id != -1) {
- NotificationManagerCompat.from(context).cancel(id);
- }
- return;
- }
-
- long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
- final Intent viewAlarmIntent = Alarm.createIntent(context, DeskClock.class, alarmId)
- .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- // Open DeskClock which is now positioned on the alarms tab.
- context.startActivity(viewAlarmIntent);
-
- deleteInstanceAndUpdateParent(context, instance);
- }
- }
-
- /**
- * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm
- * indicators.
- */
- public static Intent createIndicatorIntent(Context context) {
- return new Intent(context, AlarmStateManager.class).setAction(INDICATOR_ACTION);
- }
-
- /**
- * Abstract away how the current time is computed. If no implementation of this interface is
- * given the default is to return {@link Calendar#getInstance()}. Otherwise, the factory
- * instance is consulted for the current time.
- */
- interface CurrentTimeFactory {
- Calendar getCurrentTime();
- }
-
- /**
- * Abstracts away how state changes are scheduled. The {@link AlarmManagerStateChangeScheduler}
- * implementation schedules callbacks within the system AlarmManager. Alternate
- * implementations, such as test case mocks can subvert this behavior.
- */
- interface StateChangeScheduler {
- void scheduleInstanceStateChange(Context context, Calendar time,
- AlarmInstance instance, int newState);
-
- void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance);
- }
-
- /**
- * Schedules state change callbacks within the AlarmManager.
- */
- private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler {
- @Override
- public void scheduleInstanceStateChange(Context context, Calendar time,
- AlarmInstance instance, int newState) {
- final long timeInMillis = time.getTimeInMillis();
- LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
- instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis);
- final Intent stateChangeIntent =
- createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState);
- // Treat alarm state change as high priority, use foreground broadcasts
- stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
- stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- final AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
- if (Utils.isMOrLater()) {
- // Ensure the alarm fires even if the device is dozing.
- am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
- } else {
- am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
- }
- }
-
- @Override
- public void cancelScheduledInstanceStateChange(Context context, AlarmInstance instance) {
- LogUtils.v("Canceling instance " + instance.mId + " timers");
-
- // Create a PendingIntent that will match any one set for this instance
- PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
- createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null),
- PendingIntent.FLAG_NO_CREATE);
-
- if (pendingIntent != null) {
- AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
- am.cancel(pendingIntent);
- pendingIntent.cancel();
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmStateManager.kt b/src/com/android/deskclock/alarms/AlarmStateManager.kt
new file mode 100644
index 0000000..2478ef5
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmStateManager.kt
@@ -0,0 +1,1011 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.annotation.TargetApi
+import android.app.AlarmManager
+import android.app.AlarmManager.AlarmClockInfo
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Context.ALARM_SERVICE
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Handler
+import android.os.PowerManager
+import android.provider.Settings
+import android.provider.Settings.System.NEXT_ALARM_FORMATTED
+import android.text.format.DateFormat
+import android.widget.Toast
+import androidx.core.app.NotificationManagerCompat
+
+import com.android.deskclock.AlarmAlertWakeLock
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.AsyncHandler
+import com.android.deskclock.DeskClock
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.LogUtils
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.util.Calendar
+
+/**
+ * This class handles all the state changes for alarm instances. You need to
+ * register all alarm instances with the state manager if you want them to
+ * be activated. If a major time change has occurred (ie. TIMEZONE_CHANGE, TIMESET_CHANGE),
+ * then you must also re-register instances to fix their states.
+ *
+ * Please see [) for special transitions when major time changes][.registerInstance]
+ */
+class AlarmStateManager : BroadcastReceiver() {
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (INDICATOR_ACTION == intent.getAction()) {
+ return
+ }
+
+ val result: PendingResult = goAsync()
+ val wl: PowerManager.WakeLock = AlarmAlertWakeLock.createPartialWakeLock(context)
+ wl.acquire()
+ AsyncHandler.post {
+ handleIntent(context, intent)
+ result.finish()
+ wl.release()
+ }
+ }
+
+ /**
+ * Abstract away how the current time is computed. If no implementation of this interface is
+ * given the default is to return [Calendar.getInstance]. Otherwise, the factory
+ * instance is consulted for the current time.
+ */
+ interface CurrentTimeFactory {
+ val currentTime: Calendar
+ }
+
+ /**
+ * Abstracts away how state changes are scheduled. The [AlarmManagerStateChangeScheduler]
+ * implementation schedules callbacks within the system AlarmManager. Alternate
+ * implementations, such as test case mocks can subvert this behavior.
+ */
+ interface StateChangeScheduler {
+ fun scheduleInstanceStateChange(
+ context: Context,
+ time: Calendar,
+ instance: AlarmInstance,
+ newState: Int
+ )
+
+ fun cancelScheduledInstanceStateChange(context: Context, instance: AlarmInstance)
+ }
+
+ /**
+ * Schedules state change callbacks within the AlarmManager.
+ */
+ private class AlarmManagerStateChangeScheduler : StateChangeScheduler {
+ override fun scheduleInstanceStateChange(
+ context: Context,
+ time: Calendar,
+ instance: AlarmInstance,
+ newState: Int
+ ) {
+ val timeInMillis = time.timeInMillis
+ LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
+ instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis)
+ val stateChangeIntent: Intent =
+ createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState)
+ // Treat alarm state change as high priority, use foreground broadcasts
+ stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ val pendingIntent: PendingIntent =
+ PendingIntent.getService(context, instance.hashCode(),
+ stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val am: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager
+ if (Utils.isMOrLater) {
+ // Ensure the alarm fires even if the device is dozing.
+ am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
+ } else {
+ am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent)
+ }
+ }
+
+ override fun cancelScheduledInstanceStateChange(context: Context, instance: AlarmInstance) {
+ LogUtils.v("Canceling instance " + instance.mId + " timers")
+
+ // Create a PendingIntent that will match any one set for this instance
+ val pendingIntent: PendingIntent? =
+ PendingIntent.getService(context, instance.hashCode(),
+ createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, null),
+ PendingIntent.FLAG_NO_CREATE)
+
+ pendingIntent?.let {
+ val am: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager
+ am.cancel(it)
+ it.cancel()
+ }
+ }
+ }
+
+ companion object {
+ // Intent action to trigger an instance state change.
+ const val CHANGE_STATE_ACTION = "change_state"
+
+ // Intent action to show the alarm and dismiss the instance
+ const val SHOW_AND_DISMISS_ALARM_ACTION = "show_and_dismiss_alarm"
+
+ // Intent action for an AlarmManager alarm serving only to set the next alarm indicators
+ private const val INDICATOR_ACTION = "indicator"
+
+ // System intent action to notify AppWidget that we changed the alarm text.
+ const val ACTION_ALARM_CHANGED = "com.android.deskclock.ALARM_CHANGED"
+
+ // Extra key to set the desired state change.
+ const val ALARM_STATE_EXTRA = "intent.extra.alarm.state"
+
+ // Extra key to indicate the state change was launched from a notification.
+ const val FROM_NOTIFICATION_EXTRA = "intent.extra.from.notification"
+
+ // Extra key to set the global broadcast id.
+ private const val ALARM_GLOBAL_ID_EXTRA = "intent.extra.alarm.global.id"
+
+ // Intent category tags used to dismiss, snooze or delete an alarm
+ const val ALARM_DISMISS_TAG = "DISMISS_TAG"
+ const val ALARM_SNOOZE_TAG = "SNOOZE_TAG"
+ const val ALARM_DELETE_TAG = "DELETE_TAG"
+
+ // Intent category tag used when schedule state change intents in alarm manager.
+ private const val ALARM_MANAGER_TAG = "ALARM_MANAGER"
+
+ // Buffer time in seconds to fire alarm instead of marking it missed.
+ const val ALARM_FIRE_BUFFER = 15
+
+ // A factory for the current time; can be mocked for testing purposes.
+ private var sCurrentTimeFactory: CurrentTimeFactory? = null
+
+ // Schedules alarm state transitions; can be mocked for testing purposes.
+ private var sStateChangeScheduler: StateChangeScheduler = AlarmManagerStateChangeScheduler()
+
+ private val currentTime: Calendar
+ get() = (if (sCurrentTimeFactory == null) {
+ DataModel.dataModel.calendar
+ } else {
+ sCurrentTimeFactory!!.currentTime
+ })
+
+ fun setCurrentTimeFactory(currentTimeFactory: CurrentTimeFactory?) {
+ sCurrentTimeFactory = currentTimeFactory
+ }
+
+ fun setStateChangeScheduler(stateChangeScheduler: StateChangeScheduler?) {
+ sStateChangeScheduler = stateChangeScheduler ?: AlarmManagerStateChangeScheduler()
+ }
+
+ /**
+ * Update the next alarm stored in framework. This value is also displayed in digital
+ * widgets and the clock tab in this app.
+ */
+ private fun updateNextAlarm(context: Context) {
+ val nextAlarm = getNextFiringAlarm(context)
+
+ if (Utils.isPreL) {
+ updateNextAlarmInSystemSettings(context, nextAlarm)
+ } else {
+ updateNextAlarmInAlarmManager(context, nextAlarm)
+ }
+ }
+
+ /**
+ * Returns an alarm instance of an alarm that's going to fire next.
+ *
+ * @param context application context
+ * @return an alarm instance that will fire earliest relative to current time.
+ */
+ @JvmStatic
+ fun getNextFiringAlarm(context: Context): AlarmInstance? {
+ val cr: ContentResolver = context.getContentResolver()
+ val activeAlarmQuery: String =
+ InstancesColumns.ALARM_STATE + "<" + InstancesColumns.FIRED_STATE
+ val alarmInstances = AlarmInstance.getInstances(cr, activeAlarmQuery)
+
+ var nextAlarm: AlarmInstance? = null
+ for (instance in alarmInstances) {
+ if (nextAlarm == null || instance.alarmTime.before(nextAlarm.alarmTime)) {
+ nextAlarm = instance
+ }
+ }
+ return nextAlarm
+ }
+
+ /**
+ * Used in pre-L devices, where "next alarm" is stored in system settings.
+ */
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ private fun updateNextAlarmInSystemSettings(context: Context, nextAlarm: AlarmInstance?) {
+ // Format the next alarm time if an alarm is scheduled.
+ var time = ""
+ if (nextAlarm != null) {
+ time = AlarmUtils.getFormattedTime(context, nextAlarm.alarmTime)
+ }
+
+ try {
+ // Write directly to NEXT_ALARM_FORMATTED in all pre-L versions
+ Settings.System.putString(context.getContentResolver(), NEXT_ALARM_FORMATTED, time)
+ LogUtils.i("Updated next alarm time to: '$time'")
+
+ // Send broadcast message so pre-L AppWidgets will recognize an update.
+ context.sendBroadcast(Intent(ACTION_ALARM_CHANGED))
+ } catch (se: SecurityException) {
+ // The user has most likely revoked WRITE_SETTINGS.
+ LogUtils.e("Unable to update next alarm to: '$time'", se)
+ }
+ }
+
+ /**
+ * Used in L and later devices where "next alarm" is stored in the Alarm Manager.
+ */
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private fun updateNextAlarmInAlarmManager(context: Context, nextAlarm: AlarmInstance?) {
+ // Sets a surrogate alarm with alarm manager that provides the AlarmClockInfo for the
+ // alarm that is going to fire next. The operation is constructed such that it is
+ // ignored by AlarmStateManager.
+
+ val alarmManager: AlarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager
+
+ val flags = if (nextAlarm == null) PendingIntent.FLAG_NO_CREATE else 0
+ val operation: PendingIntent? = PendingIntent.getBroadcast(context, 0 /* requestCode */,
+ createIndicatorIntent(context), flags)
+
+ if (nextAlarm != null) {
+ LogUtils.i("Setting upcoming AlarmClockInfo for alarm: " + nextAlarm.mId)
+ val alarmTime: Long = nextAlarm.alarmTime.timeInMillis
+
+ // Create an intent that can be used to show or edit details of the next alarm.
+ val viewIntent: PendingIntent =
+ PendingIntent.getActivity(context, nextAlarm.hashCode(),
+ AlarmNotifications.createViewAlarmIntent(context, nextAlarm),
+ PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val info = AlarmClockInfo(alarmTime, viewIntent)
+ Utils.updateNextAlarm(alarmManager, info, operation)
+ } else if (operation != null) {
+ LogUtils.i("Canceling upcoming AlarmClockInfo")
+ alarmManager.cancel(operation)
+ }
+ }
+
+ /**
+ * Used by dismissed and missed states, to update parent alarm. This will either
+ * disable, delete or reschedule parent alarm.
+ *
+ * @param context application context
+ * @param instance to update parent for
+ */
+ private fun updateParentAlarm(context: Context, instance: AlarmInstance) {
+ val cr: ContentResolver = context.getContentResolver()
+ val alarm = Alarm.getAlarm(cr, instance.mAlarmId!!)
+ if (alarm == null) {
+ LogUtils.e("Parent has been deleted with instance: $instance")
+ return
+ }
+
+ if (!alarm.daysOfWeek.isRepeating) {
+ if (alarm.deleteAfterUse) {
+ LogUtils.i("Deleting parent alarm: " + alarm.id)
+ Alarm.deleteAlarm(cr, alarm.id)
+ } else {
+ LogUtils.i("Disabling parent alarm: " + alarm.id)
+ alarm.enabled = false
+ Alarm.updateAlarm(cr, alarm)
+ }
+ } else {
+ // Schedule the next repeating instance which may be before the current instance if
+ // a time jump has occurred. Otherwise, if the current instance is the next instance
+ // and has already been fired, schedule the subsequent instance.
+ var nextRepeatedInstance = alarm.createInstanceAfter(currentTime)
+ if (instance.mAlarmState > InstancesColumns.FIRED_STATE &&
+ nextRepeatedInstance.alarmTime == instance.alarmTime) {
+ nextRepeatedInstance = alarm.createInstanceAfter(instance.alarmTime)
+ }
+
+ LogUtils.i("Creating new instance for repeating alarm " + alarm.id +
+ " at " +
+ AlarmUtils.getFormattedTime(context, nextRepeatedInstance.alarmTime))
+ AlarmInstance.addInstance(cr, nextRepeatedInstance)
+ registerInstance(context, nextRepeatedInstance, true)
+ }
+ }
+
+ /**
+ * Utility method to create a proper change state intent.
+ *
+ * @param context application context
+ * @param tag used to make intent differ from other state change intents.
+ * @param instance to change state to
+ * @param state to change to.
+ * @return intent that can be used to change an alarm instance state
+ */
+ fun createStateChangeIntent(
+ context: Context?,
+ tag: String?,
+ instance: AlarmInstance,
+ state: Int?
+ ): Intent {
+ // This intent is directed to AlarmService, though the actual handling of it occurs here
+ // in AlarmStateManager. The reason is that evidence exists showing the jump between the
+ // broadcast receiver (AlarmStateManager) and service (AlarmService) can be thwarted by
+ // the Out Of Memory killer. If clock is killed during that jump, firing an alarm can
+ // fail to occur. To be safer, the call begins in AlarmService, which has the power to
+ // display the firing alarm if needed, so no jump is needed.
+ val intent: Intent =
+ AlarmInstance.createIntent(context, AlarmService::class.java, instance.mId)
+ intent.setAction(CHANGE_STATE_ACTION)
+ intent.addCategory(tag)
+ intent.putExtra(ALARM_GLOBAL_ID_EXTRA, DataModel.dataModel.globalIntentId)
+ if (state != null) {
+ intent.putExtra(ALARM_STATE_EXTRA, state.toInt())
+ }
+ return intent
+ }
+
+ /**
+ * Schedule alarm instance state changes with [AlarmManager].
+ *
+ * @param ctx application context
+ * @param time to trigger state change
+ * @param instance to change state to
+ * @param newState to change to
+ */
+ private fun scheduleInstanceStateChange(
+ ctx: Context,
+ time: Calendar,
+ instance: AlarmInstance,
+ newState: Int
+ ) {
+ sStateChangeScheduler.scheduleInstanceStateChange(ctx, time, instance, newState)
+ }
+
+ /**
+ * Cancel all [AlarmManager] timers for instance.
+ *
+ * @param ctx application context
+ * @param instance to disable all [AlarmManager] timers
+ */
+ private fun cancelScheduledInstanceStateChange(ctx: Context, instance: AlarmInstance) {
+ sStateChangeScheduler.cancelScheduledInstanceStateChange(ctx, instance)
+ }
+
+ /**
+ * This will set the alarm instance to the SILENT_STATE and update
+ * the application notifications and schedule any state changes that need
+ * to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ private fun setSilentState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting silent state to instance " + instance.mId)
+
+ // Update alarm in db
+ val contentResolver: ContentResolver = context.getContentResolver()
+ instance.mAlarmState = InstancesColumns.SILENT_STATE
+ AlarmInstance.updateInstance(contentResolver, instance)
+
+ // Setup instance notification and scheduling timers
+ AlarmNotifications.clearNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.lowNotificationTime,
+ instance, InstancesColumns.LOW_NOTIFICATION_STATE)
+ }
+
+ /**
+ * This will set the alarm instance to the LOW_NOTIFICATION_STATE and update
+ * the application notifications and schedule any state changes that need
+ * to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ private fun setLowNotificationState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting low notification state to instance " + instance.mId)
+
+ // Update alarm state in db
+ val contentResolver: ContentResolver = context.getContentResolver()
+ instance.mAlarmState = InstancesColumns.LOW_NOTIFICATION_STATE
+ AlarmInstance.updateInstance(contentResolver, instance)
+
+ // Setup instance notification and scheduling timers
+ AlarmNotifications.showLowPriorityNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.highNotificationTime,
+ instance, InstancesColumns.HIGH_NOTIFICATION_STATE)
+ }
+
+ /**
+ * This will set the alarm instance to the HIDE_NOTIFICATION_STATE and update
+ * the application notifications and schedule any state changes that need
+ * to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ private fun setHideNotificationState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting hide notification state to instance " + instance.mId)
+
+ // Update alarm state in db
+ val contentResolver: ContentResolver = context.getContentResolver()
+ instance.mAlarmState = InstancesColumns.HIDE_NOTIFICATION_STATE
+ AlarmInstance.updateInstance(contentResolver, instance)
+
+ // Setup instance notification and scheduling timers
+ AlarmNotifications.clearNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.highNotificationTime,
+ instance, InstancesColumns.HIGH_NOTIFICATION_STATE)
+ }
+
+ /**
+ * This will set the alarm instance to the HIGH_NOTIFICATION_STATE and update
+ * the application notifications and schedule any state changes that need
+ * to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ private fun setHighNotificationState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting high notification state to instance " + instance.mId)
+
+ // Update alarm state in db
+ val contentResolver: ContentResolver = context.getContentResolver()
+ instance.mAlarmState = InstancesColumns.HIGH_NOTIFICATION_STATE
+ AlarmInstance.updateInstance(contentResolver, instance)
+
+ // Setup instance notification and scheduling timers
+ AlarmNotifications.showHighPriorityNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.alarmTime,
+ instance, InstancesColumns.FIRED_STATE)
+ }
+
+ /**
+ * This will set the alarm instance to the FIRED_STATE and update
+ * the application notifications and schedule any state changes that need
+ * to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ private fun setFiredState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting fire state to instance " + instance.mId)
+
+ // Update alarm state in db
+ val contentResolver: ContentResolver = context.getContentResolver()
+ instance.mAlarmState = InstancesColumns.FIRED_STATE
+ AlarmInstance.updateInstance(contentResolver, instance)
+
+ instance.mAlarmId?.let {
+ // if the time changed *backward* and pushed an instance from missed back to fired,
+ // remove any other scheduled instances that may exist
+ AlarmInstance.deleteOtherInstances(context, contentResolver, it, instance.mId)
+ }
+
+ Events.sendAlarmEvent(R.string.action_fire, 0)
+
+ val timeout: Calendar? = instance.timeout
+ timeout?.let {
+ scheduleInstanceStateChange(context, it, instance, InstancesColumns.MISSED_STATE)
+ }
+
+ // Instance not valid anymore, so find next alarm that will fire and notify system
+ updateNextAlarm(context)
+ }
+
+ /**
+ * This will set the alarm instance to the SNOOZE_STATE and update
+ * the application notifications and schedule any state changes that need
+ * to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ @JvmStatic
+ fun setSnoozeState(
+ context: Context,
+ instance: AlarmInstance,
+ showToast: Boolean
+ ) {
+ // Stop alarm if this instance is firing it
+ AlarmService.stopAlarm(context, instance)
+
+ // Calculate the new snooze alarm time
+ val snoozeMinutes = DataModel.dataModel.snoozeLength
+ val newAlarmTime = Calendar.getInstance()
+ newAlarmTime.add(Calendar.MINUTE, snoozeMinutes)
+
+ // Update alarm state and new alarm time in db.
+ LogUtils.i("Setting snoozed state to instance " + instance.mId + " for " +
+ AlarmUtils.getFormattedTime(context, newAlarmTime))
+ instance.alarmTime = newAlarmTime
+ instance.mAlarmState = InstancesColumns.SNOOZE_STATE
+ AlarmInstance.updateInstance(context.getContentResolver(), instance)
+
+ // Setup instance notification and scheduling timers
+ AlarmNotifications.showSnoozeNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.alarmTime,
+ instance, InstancesColumns.FIRED_STATE)
+
+ // Display the snooze minutes in a toast.
+ if (showToast) {
+ val mainHandler = Handler(context.getMainLooper())
+ val myRunnable = Runnable {
+ val displayTime =
+ String.format(
+ context
+ .getResources()
+ .getQuantityText(R.plurals.alarm_alert_snooze_set,
+ snoozeMinutes)
+ .toString(),
+ snoozeMinutes)
+ Toast.makeText(context, displayTime, Toast.LENGTH_LONG).show()
+ }
+ mainHandler.post(myRunnable)
+ }
+
+ // Instance time changed, so find next alarm that will fire and notify system
+ updateNextAlarm(context)
+ }
+
+ /**
+ * This will set the alarm instance to the MISSED_STATE and update
+ * the application notifications and schedule any state changes that need
+ * to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ fun setMissedState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting missed state to instance " + instance.mId)
+ // Stop alarm if this instance is firing it
+ AlarmService.stopAlarm(context, instance)
+
+ // Check parent if it needs to reschedule, disable or delete itself
+ if (instance.mAlarmId != null) {
+ updateParentAlarm(context, instance)
+ }
+
+ // Update alarm state
+ val contentResolver: ContentResolver = context.getContentResolver()
+ instance.mAlarmState = InstancesColumns.MISSED_STATE
+ AlarmInstance.updateInstance(contentResolver, instance)
+
+ // Setup instance notification and scheduling timers
+ AlarmNotifications.showMissedNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.missedTimeToLive,
+ instance, InstancesColumns.DISMISSED_STATE)
+
+ // Instance is not valid anymore, so find next alarm that will fire and notify system
+ updateNextAlarm(context)
+ }
+
+ /**
+ * This will set the alarm instance to the PREDISMISSED_STATE and schedule an instance state
+ * change to DISMISSED_STATE at the regularly scheduled firing time.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ @JvmStatic
+ fun setPreDismissState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting predismissed state to instance " + instance.mId)
+
+ // Update alarm in db
+ val contentResolver: ContentResolver = context.getContentResolver()
+ instance.mAlarmState = InstancesColumns.PREDISMISSED_STATE
+ AlarmInstance.updateInstance(contentResolver, instance)
+
+ // Setup instance notification and scheduling timers
+ AlarmNotifications.clearNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.alarmTime, instance,
+ InstancesColumns.DISMISSED_STATE)
+
+ // Check parent if it needs to reschedule, disable or delete itself
+ if (instance.mAlarmId != null) {
+ updateParentAlarm(context, instance)
+ }
+
+ updateNextAlarm(context)
+ }
+
+ /**
+ * This just sets the alarm instance to DISMISSED_STATE.
+ */
+ private fun setDismissState(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Setting dismissed state to instance " + instance.mId)
+ instance.mAlarmState = InstancesColumns.DISMISSED_STATE
+ val contentResolver: ContentResolver = context.getContentResolver()
+ AlarmInstance.updateInstance(contentResolver, instance)
+ }
+
+ /**
+ * This will delete the alarm instance, update the application notifications, and schedule
+ * any state changes that need to occur in the future.
+ *
+ * @param context application context
+ * @param instance to set state to
+ */
+ @JvmStatic
+ fun deleteInstanceAndUpdateParent(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Deleting instance " + instance.mId + " and updating parent alarm.")
+
+ // Remove all other timers and notifications associated to it
+ unregisterInstance(context, instance)
+
+ // Check parent if it needs to reschedule, disable or delete itself
+ if (instance.mAlarmId != null) {
+ updateParentAlarm(context, instance)
+ }
+
+ // Delete instance as it is not needed anymore
+ AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId)
+
+ // Instance is not valid anymore, so find next alarm that will fire and notify system
+ updateNextAlarm(context)
+ }
+
+ /**
+ * This will set the instance state to DISMISSED_STATE and remove its notifications and
+ * alarm timers.
+ *
+ * @param context application context
+ * @param instance to unregister
+ */
+ fun unregisterInstance(context: Context, instance: AlarmInstance) {
+ LogUtils.i("Unregistering instance " + instance.mId)
+ // Stop alarm if this instance is firing it
+ AlarmService.stopAlarm(context, instance)
+ AlarmNotifications.clearNotification(context, instance)
+ cancelScheduledInstanceStateChange(context, instance)
+ setDismissState(context, instance)
+ }
+
+ /**
+ * This registers the AlarmInstance to the state manager. This will look at the instance
+ * and choose the most appropriate state to put it in. This is primarily used by new
+ * alarms, but it can also be called when the system time changes.
+ *
+ * Most state changes are handled by the states themselves, but during major time changes we
+ * have to correct the alarm instance state. This means we have to handle special cases as
+ * describe below:
+ *
+ *
+ * * Make sure all dismissed alarms are never re-activated
+ * * Make sure pre-dismissed alarms stay predismissed
+ * * Make sure firing alarms stayed fired unless they should be auto-silenced
+ * * Missed instance that have parents should be re-enabled if we went back in time
+ * * If alarm was SNOOZED, then show the notification but don't update time
+ * * If low priority notification was hidden, then make sure it stays hidden
+ *
+ *
+ * If none of these special case are found, then we just check the time and see what is the
+ * proper state for the instance.
+ *
+ * @param context application context
+ * @param instance to register
+ */
+ @JvmStatic
+ fun registerInstance(
+ context: Context,
+ instance: AlarmInstance,
+ updateNextAlarm: Boolean
+ ) {
+ LogUtils.i("Registering instance: " + instance.mId)
+ val cr: ContentResolver = context.getContentResolver()
+ val alarm = Alarm.getAlarm(cr, instance.mAlarmId!!)
+ val currentTime = currentTime
+ val alarmTime: Calendar = instance.alarmTime
+ val timeoutTime: Calendar? = instance.timeout
+ val lowNotificationTime: Calendar = instance.lowNotificationTime
+ val highNotificationTime: Calendar = instance.highNotificationTime
+ val missedTTL: Calendar = instance.missedTimeToLive
+
+ // Handle special use cases here
+ if (instance.mAlarmState == InstancesColumns.DISMISSED_STATE) {
+ // This should never happen, but add a quick check here
+ LogUtils.e("Alarm Instance is dismissed, but never deleted")
+ deleteInstanceAndUpdateParent(context, instance)
+ return
+ } else if (instance.mAlarmState == InstancesColumns.FIRED_STATE) {
+ // Keep alarm firing, unless it should be timed out
+ val hasTimeout = timeoutTime != null && currentTime.after(timeoutTime)
+ if (!hasTimeout) {
+ setFiredState(context, instance)
+ return
+ }
+ } else if (instance.mAlarmState == InstancesColumns.MISSED_STATE) {
+ if (currentTime.before(alarmTime)) {
+ if (instance.mAlarmId == null) {
+ LogUtils.i("Cannot restore missed instance for one-time alarm")
+ // This instance parent got deleted (ie. deleteAfterUse), so
+ // we should not re-activate it.-
+ deleteInstanceAndUpdateParent(context, instance)
+ return
+ }
+
+ // TODO: This will re-activate missed snoozed alarms, but will
+ // use our normal notifications. This is not ideal, but very rare use-case.
+ // We should look into fixing this in the future.
+
+ // Make sure we re-enable the parent alarm of the instance
+ // because it will get activated by by the below code
+ alarm!!.enabled = true
+ Alarm.updateAlarm(cr, alarm)
+ }
+ } else if (instance.mAlarmState == InstancesColumns.PREDISMISSED_STATE) {
+ if (currentTime.before(alarmTime)) {
+ setPreDismissState(context, instance)
+ } else {
+ deleteInstanceAndUpdateParent(context, instance)
+ }
+ return
+ }
+
+ // Fix states that are time sensitive
+ if (currentTime.after(missedTTL)) {
+ // Alarm is so old, just dismiss it
+ deleteInstanceAndUpdateParent(context, instance)
+ } else if (currentTime.after(alarmTime)) {
+ // There is a chance that the TIME_SET occurred right when the alarm should go off,
+ // so we need to add a check to see if we should fire the alarm instead of marking
+ // it missed.
+ val alarmBuffer = Calendar.getInstance()
+ alarmBuffer.time = alarmTime.time
+ alarmBuffer.add(Calendar.SECOND, ALARM_FIRE_BUFFER)
+ if (currentTime.before(alarmBuffer)) {
+ setFiredState(context, instance)
+ } else {
+ setMissedState(context, instance)
+ }
+ } else if (instance.mAlarmState == InstancesColumns.SNOOZE_STATE) {
+ // We only want to display snooze notification and not update the time,
+ // so handle showing the notification directly
+ AlarmNotifications.showSnoozeNotification(context, instance)
+ scheduleInstanceStateChange(context, instance.alarmTime,
+ instance, InstancesColumns.FIRED_STATE)
+ } else if (currentTime.after(highNotificationTime)) {
+ setHighNotificationState(context, instance)
+ } else if (currentTime.after(lowNotificationTime)) {
+ // Only show low notification if it wasn't hidden in the past
+ if (instance.mAlarmState == InstancesColumns.HIDE_NOTIFICATION_STATE) {
+ setHideNotificationState(context, instance)
+ } else {
+ setLowNotificationState(context, instance)
+ }
+ } else {
+ // Alarm is still active, so initialize as a silent alarm
+ setSilentState(context, instance)
+ }
+
+ // The caller prefers to handle updateNextAlarm for optimization
+ if (updateNextAlarm) {
+ updateNextAlarm(context)
+ }
+ }
+
+ /**
+ * This will delete and unregister all instances associated with alarmId, without affect
+ * the alarm itself. This should be used whenever modifying or deleting an alarm.
+ *
+ * @param context application context
+ * @param alarmId to find instances to delete.
+ */
+ @JvmStatic
+ fun deleteAllInstances(context: Context, alarmId: Long) {
+ LogUtils.i("Deleting all instances of alarm: $alarmId")
+ val cr: ContentResolver = context.getContentResolver()
+ val instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId)
+ for (instance in instances) {
+ unregisterInstance(context, instance)
+ AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId)
+ }
+ updateNextAlarm(context)
+ }
+
+ /**
+ * Delete and unregister all instances unless they are snoozed. This is used whenever an
+ * alarm is modified superficially (label, vibrate, or ringtone change).
+ */
+ fun deleteNonSnoozeInstances(context: Context, alarmId: Long) {
+ LogUtils.i("Deleting all non-snooze instances of alarm: $alarmId")
+ val cr: ContentResolver = context.getContentResolver()
+ val instances = AlarmInstance.getInstancesByAlarmId(cr, alarmId)
+ for (instance in instances) {
+ if (instance.mAlarmState == InstancesColumns.SNOOZE_STATE) {
+ continue
+ }
+ unregisterInstance(context, instance)
+ AlarmInstance.deleteInstance(context.getContentResolver(), instance.mId)
+ }
+ updateNextAlarm(context)
+ }
+
+ /**
+ * Fix and update all alarm instance when a time change event occurs.
+ *
+ * @param context application context
+ */
+ @JvmStatic
+ fun fixAlarmInstances(context: Context) {
+ LogUtils.i("Fixing alarm instances")
+ // Register all instances after major time changes or when phone restarts
+ val contentResolver: ContentResolver = context.getContentResolver()
+ val currentTime = currentTime
+
+ // Sort the instances in reverse chronological order so that later instances are fixed
+ // or deleted before re-scheduling prior instances (which may re-create or update the
+ // later instances).
+ val instances = AlarmInstance.getInstances(
+ contentResolver, null /* selection */)
+ instances.sortWith(Comparator { lhs, rhs -> rhs.alarmTime.compareTo(lhs.alarmTime) })
+
+ for (instance in instances) {
+ val alarm = Alarm.getAlarm(contentResolver, instance.mAlarmId!!)
+ if (alarm == null) {
+ unregisterInstance(context, instance)
+ AlarmInstance.deleteInstance(contentResolver, instance.mId)
+ LogUtils.e("Found instance without matching alarm; deleting instance %s",
+ instance)
+ continue
+ }
+ val priorAlarmTime = alarm.getPreviousAlarmTime(instance.alarmTime)
+ val missedTTLTime: Calendar = instance.missedTimeToLive
+ if (currentTime.before(priorAlarmTime) || currentTime.after(missedTTLTime)) {
+ val oldAlarmTime: Calendar = instance.alarmTime
+ val newAlarmTime = alarm.getNextAlarmTime(currentTime)
+ val oldTime: CharSequence =
+ DateFormat.format("MM/dd/yyyy hh:mm a", oldAlarmTime)
+ val newTime: CharSequence =
+ DateFormat.format("MM/dd/yyyy hh:mm a", newAlarmTime)
+ LogUtils.i("A time change has caused an existing alarm scheduled" +
+ " to fire at %s to be replaced by a new alarm scheduled to fire at %s",
+ oldTime, newTime)
+
+ // The time change is so dramatic the AlarmInstance doesn't make any sense;
+ // remove it and schedule the new appropriate instance.
+ deleteInstanceAndUpdateParent(context, instance)
+ } else {
+ registerInstance(context, instance, false /* updateNextAlarm */)
+ }
+ }
+
+ updateNextAlarm(context)
+ }
+
+ /**
+ * Utility method to set alarm instance state via constants.
+ *
+ * @param context application context
+ * @param instance to change state on
+ * @param state to change to
+ */
+ private fun setAlarmState(context: Context, instance: AlarmInstance?, state: Int) {
+ if (instance == null) {
+ LogUtils.e("Null alarm instance while setting state to %d", state)
+ return
+ }
+ when (state) {
+ InstancesColumns.SILENT_STATE -> setSilentState(context, instance)
+ InstancesColumns.LOW_NOTIFICATION_STATE -> {
+ setLowNotificationState(context, instance)
+ }
+ InstancesColumns.HIDE_NOTIFICATION_STATE -> {
+ setHideNotificationState(context, instance)
+ }
+ InstancesColumns.HIGH_NOTIFICATION_STATE -> {
+ setHighNotificationState(context, instance)
+ }
+ InstancesColumns.FIRED_STATE -> setFiredState(context, instance)
+ InstancesColumns.SNOOZE_STATE -> {
+ setSnoozeState(context, instance, true /* showToast */)
+ }
+ InstancesColumns.MISSED_STATE -> setMissedState(context, instance)
+ InstancesColumns.PREDISMISSED_STATE -> setPreDismissState(context, instance)
+ InstancesColumns.DISMISSED_STATE -> deleteInstanceAndUpdateParent(context, instance)
+ else -> LogUtils.e("Trying to change to unknown alarm state: $state")
+ }
+ }
+
+ fun handleIntent(context: Context, intent: Intent) {
+ val action: String? = intent.getAction()
+ LogUtils.v("AlarmStateManager received intent $intent")
+ if (CHANGE_STATE_ACTION == action) {
+ val uri: Uri = intent.getData()!!
+ val instance: AlarmInstance? =
+ AlarmInstance.getInstance(context.getContentResolver(),
+ AlarmInstance.getId(uri))
+ if (instance == null) {
+ LogUtils.e("Can not change state for unknown instance: $uri")
+ return
+ }
+
+ val globalId = DataModel.dataModel.globalIntentId
+ val intentId: Int = intent.getIntExtra(ALARM_GLOBAL_ID_EXTRA, -1)
+ val alarmState: Int = intent.getIntExtra(ALARM_STATE_EXTRA, -1)
+ if (intentId != globalId) {
+ LogUtils.i("IntentId: " + intentId + " GlobalId: " + globalId +
+ " AlarmState: " + alarmState)
+ // Allows dismiss/snooze requests to go through
+ if (!intent.hasCategory(ALARM_DISMISS_TAG) &&
+ !intent.hasCategory(ALARM_SNOOZE_TAG)) {
+ LogUtils.i("Ignoring old Intent")
+ return
+ }
+ }
+
+ if (intent.getBooleanExtra(FROM_NOTIFICATION_EXTRA, false)) {
+ if (intent.hasCategory(ALARM_DISMISS_TAG)) {
+ Events.sendAlarmEvent(R.string.action_dismiss, R.string.label_notification)
+ } else if (intent.hasCategory(ALARM_SNOOZE_TAG)) {
+ Events.sendAlarmEvent(R.string.action_snooze, R.string.label_notification)
+ }
+ }
+
+ if (alarmState >= 0) {
+ setAlarmState(context, instance, alarmState)
+ } else {
+ registerInstance(context, instance, true)
+ }
+ } else if (SHOW_AND_DISMISS_ALARM_ACTION == action) {
+ val uri: Uri = intent.getData()!!
+ val instance: AlarmInstance? =
+ AlarmInstance.getInstance(context.getContentResolver(),
+ AlarmInstance.getId(uri))
+
+ if (instance == null) {
+ LogUtils.e("Null alarminstance for SHOW_AND_DISMISS")
+ // dismiss the notification
+ val id: Int = intent.getIntExtra(AlarmNotifications.EXTRA_NOTIFICATION_ID, -1)
+ if (id != -1) {
+ NotificationManagerCompat.from(context).cancel(id)
+ }
+ return
+ }
+
+ val alarmId = instance.mAlarmId ?: Alarm.INVALID_ID
+ val viewAlarmIntent: Intent =
+ Alarm.createIntent(context, DeskClock::class.java, alarmId)
+ .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ // Open DeskClock which is now positioned on the alarms tab.
+ context.startActivity(viewAlarmIntent)
+
+ deleteInstanceAndUpdateParent(context, instance)
+ }
+ }
+
+ /**
+ * Creates an intent that can be used to set an AlarmManager alarm to set the next alarm
+ * indicators.
+ */
+ private fun createIndicatorIntent(context: Context?): Intent {
+ return Intent(context, AlarmStateManager::class.java).setAction(INDICATOR_ACTION)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
deleted file mode 100644
index 6c94649..0000000
--- a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms;
-
-import android.app.Fragment;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Vibrator;
-
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.LabelDialogFragment;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.alarms.dataadapter.AlarmItemHolder;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Weekdays;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.ringtone.RingtonePickerActivity;
-
-import java.util.Calendar;
-
-/**
- * Click handler for an alarm time item.
- */
-public final class AlarmTimeClickHandler {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AlarmTimeClickHandler");
-
- private static final String KEY_PREVIOUS_DAY_MAP = "previousDayMap";
-
- private final Fragment mFragment;
- private final Context mContext;
- private final AlarmUpdateHandler mAlarmUpdateHandler;
- private final ScrollHandler mScrollHandler;
-
- private Alarm mSelectedAlarm;
- private Bundle mPreviousDaysOfWeekMap;
-
- public AlarmTimeClickHandler(Fragment fragment, Bundle savedState,
- AlarmUpdateHandler alarmUpdateHandler, ScrollHandler smoothScrollController) {
- mFragment = fragment;
- mContext = mFragment.getActivity().getApplicationContext();
- mAlarmUpdateHandler = alarmUpdateHandler;
- mScrollHandler = smoothScrollController;
- if (savedState != null) {
- mPreviousDaysOfWeekMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP);
- }
- if (mPreviousDaysOfWeekMap == null) {
- mPreviousDaysOfWeekMap = new Bundle();
- }
- }
-
- public void setSelectedAlarm(Alarm selectedAlarm) {
- mSelectedAlarm = selectedAlarm;
- }
-
- public void saveInstance(Bundle outState) {
- outState.putBundle(KEY_PREVIOUS_DAY_MAP, mPreviousDaysOfWeekMap);
- }
-
- public void setAlarmEnabled(Alarm alarm, boolean newState) {
- if (newState != alarm.enabled) {
- alarm.enabled = newState;
- Events.sendAlarmEvent(newState ? R.string.action_enable : R.string.action_disable,
- R.string.label_deskclock);
- mAlarmUpdateHandler.asyncUpdateAlarm(alarm, alarm.enabled, false);
- LOGGER.d("Updating alarm enabled state to " + newState);
- }
- }
-
- public void setAlarmVibrationEnabled(Alarm alarm, boolean newState) {
- if (newState != alarm.vibrate) {
- alarm.vibrate = newState;
- Events.sendAlarmEvent(R.string.action_toggle_vibrate, R.string.label_deskclock);
- mAlarmUpdateHandler.asyncUpdateAlarm(alarm, false, true);
- LOGGER.d("Updating vibrate state to " + newState);
-
- if (newState) {
- // Buzz the vibrator to preview the alarm firing behavior.
- final Vibrator v = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
- if (v.hasVibrator()) {
- v.vibrate(300);
- }
- }
- }
- }
-
- public void setAlarmRepeatEnabled(Alarm alarm, boolean isEnabled) {
- final Calendar now = Calendar.getInstance();
- final Calendar oldNextAlarmTime = alarm.getNextAlarmTime(now);
- final String alarmId = String.valueOf(alarm.id);
- if (isEnabled) {
- // Set all previously set days
- // or
- // Set all days if no previous.
- final int bitSet = mPreviousDaysOfWeekMap.getInt(alarmId);
- alarm.daysOfWeek = Weekdays.fromBits(bitSet);
- if (!alarm.daysOfWeek.isRepeating()) {
- alarm.daysOfWeek = Weekdays.ALL;
- }
- } else {
- // Remember the set days in case the user wants it back.
- final int bitSet = alarm.daysOfWeek.getBits();
- mPreviousDaysOfWeekMap.putInt(alarmId, bitSet);
-
- // Remove all repeat days
- alarm.daysOfWeek = Weekdays.NONE;
- }
-
- // if the change altered the next scheduled alarm time, tell the user
- final Calendar newNextAlarmTime = alarm.getNextAlarmTime(now);
- final boolean popupToast = !oldNextAlarmTime.equals(newNextAlarmTime);
-
- Events.sendAlarmEvent(R.string.action_toggle_repeat_days, R.string.label_deskclock);
- mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, false);
- }
-
- public void setDayOfWeekEnabled(Alarm alarm, boolean checked, int index) {
- final Calendar now = Calendar.getInstance();
- final Calendar oldNextAlarmTime = alarm.getNextAlarmTime(now);
-
- final int weekday = DataModel.getDataModel().getWeekdayOrder().getCalendarDays().get(index);
- alarm.daysOfWeek = alarm.daysOfWeek.setBit(weekday, checked);
-
- // if the change altered the next scheduled alarm time, tell the user
- final Calendar newNextAlarmTime = alarm.getNextAlarmTime(now);
- final boolean popupToast = !oldNextAlarmTime.equals(newNextAlarmTime);
- mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, false);
- }
-
- public void onDeleteClicked(AlarmItemHolder itemHolder) {
- if (mFragment instanceof AlarmClockFragment) {
- ((AlarmClockFragment) mFragment).removeItem(itemHolder);
- }
- final Alarm alarm = itemHolder.item;
- Events.sendAlarmEvent(R.string.action_delete, R.string.label_deskclock);
- mAlarmUpdateHandler.asyncDeleteAlarm(alarm);
- LOGGER.d("Deleting alarm.");
- }
-
- public void onClockClicked(Alarm alarm) {
- mSelectedAlarm = alarm;
- Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock);
- TimePickerDialogFragment.show(mFragment, alarm.hour, alarm.minutes);
- }
-
- public void dismissAlarmInstance(AlarmInstance alarmInstance) {
- final Intent dismissIntent = AlarmStateManager.createStateChangeIntent(
- mContext, AlarmStateManager.ALARM_DISMISS_TAG, alarmInstance,
- AlarmInstance.PREDISMISSED_STATE);
- mContext.startService(dismissIntent);
- mAlarmUpdateHandler.showPredismissToast(alarmInstance);
- }
-
- public void onRingtoneClicked(Context context, Alarm alarm) {
- mSelectedAlarm = alarm;
- Events.sendAlarmEvent(R.string.action_set_ringtone, R.string.label_deskclock);
-
- final Intent intent =
- RingtonePickerActivity.createAlarmRingtonePickerIntent(context, alarm);
- context.startActivity(intent);
- }
-
- public void onEditLabelClicked(Alarm alarm) {
- Events.sendAlarmEvent(R.string.action_set_label, R.string.label_deskclock);
- final LabelDialogFragment fragment =
- LabelDialogFragment.newInstance(alarm, alarm.label, mFragment.getTag());
- LabelDialogFragment.show(mFragment.getFragmentManager(), fragment);
- }
-
- public void onTimeSet(int hourOfDay, int minute) {
- if (mSelectedAlarm == null) {
- // If mSelectedAlarm is null then we're creating a new alarm.
- final Alarm a = new Alarm();
- a.hour = hourOfDay;
- a.minutes = minute;
- a.enabled = true;
- mAlarmUpdateHandler.asyncAddAlarm(a);
- } else {
- mSelectedAlarm.hour = hourOfDay;
- mSelectedAlarm.minutes = minute;
- mSelectedAlarm.enabled = true;
- mScrollHandler.setSmoothScrollStableId(mSelectedAlarm.id);
- mAlarmUpdateHandler.asyncUpdateAlarm(mSelectedAlarm, true, false);
- mSelectedAlarm = null;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt
new file mode 100644
index 0000000..a2a58cf
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmTimeClickHandler.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Vibrator
+import androidx.fragment.app.Fragment
+
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.LabelDialogFragment
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.alarms.dataadapter.AlarmItemHolder
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Weekdays
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.ringtone.RingtonePickerActivity
+
+import java.util.Calendar
+
+/**
+ * Click handler for an alarm time item.
+ */
+class AlarmTimeClickHandler(
+ private val mFragment: Fragment,
+ savedState: Bundle?,
+ private val mAlarmUpdateHandler: AlarmUpdateHandler,
+ private val mScrollHandler: ScrollHandler
+) {
+
+ private val mContext: Context = mFragment.requireActivity().getApplicationContext()
+ private var mSelectedAlarm: Alarm? = null
+ private var mPreviousDaysOfWeekMap: Bundle? = null
+
+ init {
+ if (savedState != null) {
+ mPreviousDaysOfWeekMap = savedState.getBundle(KEY_PREVIOUS_DAY_MAP)
+ }
+ if (mPreviousDaysOfWeekMap == null) {
+ mPreviousDaysOfWeekMap = Bundle()
+ }
+ }
+
+ fun setSelectedAlarm(selectedAlarm: Alarm?) {
+ mSelectedAlarm = selectedAlarm
+ }
+
+ fun saveInstance(outState: Bundle) {
+ outState.putBundle(KEY_PREVIOUS_DAY_MAP, mPreviousDaysOfWeekMap)
+ }
+
+ fun setAlarmEnabled(alarm: Alarm, newState: Boolean) {
+ if (newState != alarm.enabled) {
+ alarm.enabled = newState
+ Events.sendAlarmEvent(if (newState) R.string.action_enable else R.string.action_disable,
+ R.string.label_deskclock)
+ mAlarmUpdateHandler.asyncUpdateAlarm(alarm, alarm.enabled, minorUpdate = false)
+ LOGGER.d("Updating alarm enabled state to $newState")
+ }
+ }
+
+ fun setAlarmVibrationEnabled(alarm: Alarm, newState: Boolean) {
+ if (newState != alarm.vibrate) {
+ alarm.vibrate = newState
+ Events.sendAlarmEvent(R.string.action_toggle_vibrate, R.string.label_deskclock)
+ mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+ LOGGER.d("Updating vibrate state to $newState")
+
+ if (newState) {
+ // Buzz the vibrator to preview the alarm firing behavior.
+ val v: Vibrator = mContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ if (v.hasVibrator()) {
+ v.vibrate(300)
+ }
+ }
+ }
+ }
+
+ fun setAlarmRepeatEnabled(alarm: Alarm, isEnabled: Boolean) {
+ val now = Calendar.getInstance()
+ val oldNextAlarmTime = alarm.getNextAlarmTime(now)
+ val alarmId = alarm.id.toString()
+ if (isEnabled) {
+ // Set all previously set days
+ // or
+ // Set all days if no previous.
+ val bitSet: Int = mPreviousDaysOfWeekMap!!.getInt(alarmId)
+ alarm.daysOfWeek = Weekdays.fromBits(bitSet)
+ if (!alarm.daysOfWeek.isRepeating) {
+ alarm.daysOfWeek = Weekdays.ALL
+ }
+ } else {
+ // Remember the set days in case the user wants it back.
+ val bitSet = alarm.daysOfWeek.bits
+ mPreviousDaysOfWeekMap!!.putInt(alarmId, bitSet)
+
+ // Remove all repeat days
+ alarm.daysOfWeek = Weekdays.NONE
+ }
+
+ // if the change altered the next scheduled alarm time, tell the user
+ val newNextAlarmTime = alarm.getNextAlarmTime(now)
+ val popupToast = oldNextAlarmTime != newNextAlarmTime
+ Events.sendAlarmEvent(R.string.action_toggle_repeat_days, R.string.label_deskclock)
+ mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, minorUpdate = false)
+ }
+
+ fun setDayOfWeekEnabled(alarm: Alarm, checked: Boolean, index: Int) {
+ val now = Calendar.getInstance()
+ val oldNextAlarmTime = alarm.getNextAlarmTime(now)
+
+ val weekday = DataModel.dataModel.weekdayOrder.calendarDays[index]
+ alarm.daysOfWeek = alarm.daysOfWeek.setBit(weekday, checked)
+
+ // if the change altered the next scheduled alarm time, tell the user
+ val newNextAlarmTime = alarm.getNextAlarmTime(now)
+ val popupToast = oldNextAlarmTime != newNextAlarmTime
+ mAlarmUpdateHandler.asyncUpdateAlarm(alarm, popupToast, minorUpdate = false)
+ }
+
+ fun onDeleteClicked(itemHolder: AlarmItemHolder) {
+ if (mFragment is AlarmClockFragment) {
+ (mFragment as AlarmClockFragment).removeItem(itemHolder)
+ }
+ val alarm = itemHolder.item
+ Events.sendAlarmEvent(R.string.action_delete, R.string.label_deskclock)
+ mAlarmUpdateHandler.asyncDeleteAlarm(alarm)
+ LOGGER.d("Deleting alarm.")
+ }
+
+ fun onClockClicked(alarm: Alarm) {
+ mSelectedAlarm = alarm
+ Events.sendAlarmEvent(R.string.action_set_time, R.string.label_deskclock)
+ TimePickerDialogFragment.show(mFragment, alarm.hour, alarm.minutes)
+ }
+
+ fun dismissAlarmInstance(alarmInstance: AlarmInstance) {
+ val dismissIntent: Intent = AlarmStateManager.createStateChangeIntent(
+ mContext, AlarmStateManager.ALARM_DISMISS_TAG, alarmInstance,
+ InstancesColumns.PREDISMISSED_STATE)
+ mContext.startService(dismissIntent)
+ mAlarmUpdateHandler.showPredismissToast(alarmInstance)
+ }
+
+ fun onRingtoneClicked(context: Context, alarm: Alarm) {
+ mSelectedAlarm = alarm
+ Events.sendAlarmEvent(R.string.action_set_ringtone, R.string.label_deskclock)
+
+ val intent: Intent = RingtonePickerActivity.createAlarmRingtonePickerIntent(context, alarm)
+ context.startActivity(intent)
+ }
+
+ fun onEditLabelClicked(alarm: Alarm) {
+ Events.sendAlarmEvent(R.string.action_set_label, R.string.label_deskclock)
+ val fragment = LabelDialogFragment.newInstance(alarm, alarm.label, mFragment.getTag())
+ LabelDialogFragment.show(mFragment.getFragmentManager(), fragment)
+ }
+
+ fun onTimeSet(hourOfDay: Int, minute: Int) {
+ if (mSelectedAlarm == null) {
+ // If mSelectedAlarm is null then we're creating a new alarm.
+ val a = Alarm()
+ a.hour = hourOfDay
+ a.minutes = minute
+ a.enabled = true
+ mAlarmUpdateHandler.asyncAddAlarm(a)
+ } else {
+ mSelectedAlarm!!.hour = hourOfDay
+ mSelectedAlarm!!.minutes = minute
+ mSelectedAlarm!!.enabled = true
+ mScrollHandler.setSmoothScrollStableId(mSelectedAlarm!!.id)
+ mAlarmUpdateHandler
+ .asyncUpdateAlarm(mSelectedAlarm!!, popToast = true, minorUpdate = false)
+ mSelectedAlarm = null
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("AlarmTimeClickHandler")
+
+ private const val KEY_PREVIOUS_DAY_MAP = "previousDayMap"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/AlarmUpdateHandler.java b/src/com/android/deskclock/alarms/AlarmUpdateHandler.java
deleted file mode 100644
index 8f6fc5d..0000000
--- a/src/com/android/deskclock/alarms/AlarmUpdateHandler.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.os.AsyncTask;
-import com.google.android.material.snackbar.Snackbar;
-import android.text.format.DateFormat;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.widget.toast.SnackbarManager;
-
-import java.util.Calendar;
-import java.util.List;
-
-/**
- * API for asynchronously mutating a single alarm.
- */
-public final class AlarmUpdateHandler {
-
- private final Context mAppContext;
- private final ScrollHandler mScrollHandler;
- private final View mSnackbarAnchor;
-
- // For undo
- private Alarm mDeletedAlarm;
-
- public AlarmUpdateHandler(Context context, ScrollHandler scrollHandler,
- ViewGroup snackbarAnchor) {
- mAppContext = context.getApplicationContext();
- mScrollHandler = scrollHandler;
- mSnackbarAnchor = snackbarAnchor;
- }
-
- /**
- * Adds a new alarm on the background.
- *
- * @param alarm The alarm to be added.
- */
- public void asyncAddAlarm(final Alarm alarm) {
- final AsyncTask<Void, Void, AlarmInstance> updateTask =
- new AsyncTask<Void, Void, AlarmInstance>() {
- @Override
- protected AlarmInstance doInBackground(Void... parameters) {
- if (alarm != null) {
- Events.sendAlarmEvent(R.string.action_create, R.string.label_deskclock);
- ContentResolver cr = mAppContext.getContentResolver();
-
- // Add alarm to db
- Alarm newAlarm = Alarm.addAlarm(cr, alarm);
-
- // Be ready to scroll to this alarm on UI later.
- mScrollHandler.setSmoothScrollStableId(newAlarm.id);
-
- // Create and add instance to db
- if (newAlarm.enabled) {
- return setupAlarmInstance(newAlarm);
- }
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(AlarmInstance instance) {
- if (instance != null) {
- AlarmUtils.popAlarmSetSnackbar(
- mSnackbarAnchor, instance.getAlarmTime().getTimeInMillis());
- }
- }
- };
- updateTask.execute();
- }
-
- /**
- * Modifies an alarm on the background, and optionally show a toast when done.
- *
- * @param alarm The alarm to be modified.
- * @param popToast whether or not a toast should be displayed when done.
- * @param minorUpdate if true, don't affect any currently snoozed instances.
- */
- public void asyncUpdateAlarm(final Alarm alarm, final boolean popToast,
- final boolean minorUpdate) {
- final AsyncTask<Void, Void, AlarmInstance> updateTask =
- new AsyncTask<Void, Void, AlarmInstance>() {
- @Override
- protected AlarmInstance doInBackground(Void... parameters) {
- ContentResolver cr = mAppContext.getContentResolver();
-
- // Update alarm
- Alarm.updateAlarm(cr, alarm);
-
- if (minorUpdate) {
- // just update the instance in the database and update notifications.
- final List<AlarmInstance> instanceList =
- AlarmInstance.getInstancesByAlarmId(cr, alarm.id);
- for (AlarmInstance instance : instanceList) {
- // Make a copy of the existing instance
- final AlarmInstance newInstance = new AlarmInstance(instance);
- // Copy over minor change data to the instance; we don't know
- // exactly which minor field changed, so just copy them all.
- newInstance.mVibrate = alarm.vibrate;
- newInstance.mRingtone = alarm.alert;
- newInstance.mLabel = alarm.label;
- // Since we copied the mId of the old instance and the mId is used
- // as the primary key in the AlarmInstance table, this will replace
- // the existing instance.
- AlarmInstance.updateInstance(cr, newInstance);
- // Update the notification for this instance.
- AlarmNotifications.updateNotification(mAppContext, newInstance);
- }
- return null;
- }
- // Otherwise, this is a major update and we're going to re-create the alarm
- AlarmStateManager.deleteAllInstances(mAppContext, alarm.id);
-
- return alarm.enabled ? setupAlarmInstance(alarm) : null;
- }
-
- @Override
- protected void onPostExecute(AlarmInstance instance) {
- if (popToast && instance != null) {
- AlarmUtils.popAlarmSetSnackbar(
- mSnackbarAnchor, instance.getAlarmTime().getTimeInMillis());
- }
- }
- };
- updateTask.execute();
- }
-
- /**
- * Deletes an alarm on the background.
- *
- * @param alarm The alarm to be deleted.
- */
- public void asyncDeleteAlarm(final Alarm alarm) {
- final AsyncTask<Void, Void, Boolean> deleteTask = new AsyncTask<Void, Void, Boolean>() {
- @Override
- protected Boolean doInBackground(Void... parameters) {
- // Activity may be closed at this point , make sure data is still valid
- if (alarm == null) {
- // Nothing to do here, just return.
- return false;
- }
- AlarmStateManager.deleteAllInstances(mAppContext, alarm.id);
- return Alarm.deleteAlarm(mAppContext.getContentResolver(), alarm.id);
- }
-
- @Override
- protected void onPostExecute(Boolean deleted) {
- if (deleted) {
- mDeletedAlarm = alarm;
- showUndoBar();
- }
- }
- };
- deleteTask.execute();
- }
-
- /**
- * Show a toast when an alarm is predismissed.
- *
- * @param instance Instance being predismissed.
- */
- public void showPredismissToast(AlarmInstance instance) {
- final String time = DateFormat.getTimeFormat(mAppContext).format(
- instance.getAlarmTime().getTime());
- final String text = mAppContext.getString(R.string.alarm_is_dismissed, time);
- SnackbarManager.show(Snackbar.make(mSnackbarAnchor, text, Snackbar.LENGTH_SHORT));
- }
-
- /**
- * Hides any undo toast.
- */
- public void hideUndoBar() {
- mDeletedAlarm = null;
- SnackbarManager.dismiss();
- }
-
- private void showUndoBar() {
- final Alarm deletedAlarm = mDeletedAlarm;
- final Snackbar snackbar = Snackbar.make(mSnackbarAnchor,
- mAppContext.getString(R.string.alarm_deleted), Snackbar.LENGTH_LONG)
- .setAction(R.string.alarm_undo, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- mDeletedAlarm = null;
- asyncAddAlarm(deletedAlarm);
- }
- });
- SnackbarManager.show(snackbar);
- }
-
- private AlarmInstance setupAlarmInstance(Alarm alarm) {
- final ContentResolver cr = mAppContext.getContentResolver();
- AlarmInstance newInstance = alarm.createInstanceAfter(Calendar.getInstance());
- newInstance = AlarmInstance.addInstance(cr, newInstance);
- // Register instance to state manager
- AlarmStateManager.registerInstance(mAppContext, newInstance, true);
- return newInstance;
- }
-}
diff --git a/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt b/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt
new file mode 100644
index 0000000..9295cbf
--- /dev/null
+++ b/src/com/android/deskclock/alarms/AlarmUpdateHandler.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.content.ContentResolver
+import android.content.Context
+import android.os.AsyncTask
+import android.text.format.DateFormat
+import android.view.ViewGroup
+
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.R
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.widget.toast.SnackbarManager
+
+import com.google.android.material.snackbar.Snackbar
+
+import java.util.Calendar
+
+/**
+ * API for asynchronously mutating a single alarm.
+ */
+// TODO(b/165664115) Replace deprecated AsyncTask calls
+class AlarmUpdateHandler(
+ context: Context,
+ private val mScrollHandler: ScrollHandler?,
+ private val mSnackbarAnchor: ViewGroup?
+) {
+
+ private val mAppContext: Context = context.getApplicationContext()
+
+ // For undo
+ private var mDeletedAlarm: Alarm? = null
+
+ /**
+ * Adds a new alarm on the background.
+ *
+ * @param alarm The alarm to be added.
+ */
+ fun asyncAddAlarm(alarm: Alarm?) {
+ val updateTask: AsyncTask<Void, Void, AlarmInstance> =
+ object : AsyncTask<Void, Void, AlarmInstance>() {
+ override fun doInBackground(vararg parameters: Void): AlarmInstance? {
+ if (alarm != null) {
+ Events.sendAlarmEvent(R.string.action_create, R.string.label_deskclock)
+ val cr: ContentResolver = mAppContext.getContentResolver()
+
+ // Add alarm to db
+ val newAlarm = Alarm.addAlarm(cr, alarm)
+
+ // Be ready to scroll to this alarm on UI later.
+ mScrollHandler?.setSmoothScrollStableId(newAlarm.id)
+
+ // Create and add instance to db
+ if (newAlarm.enabled) {
+ return setupAlarmInstance(newAlarm)
+ }
+ }
+ return null
+ }
+
+ override fun onPostExecute(instance: AlarmInstance?) {
+ if (instance != null) {
+ AlarmUtils.popAlarmSetSnackbar(mSnackbarAnchor!!,
+ instance.alarmTime.timeInMillis)
+ }
+ }
+ }
+ updateTask.execute()
+ }
+
+ /**
+ * Modifies an alarm on the background, and optionally show a toast when done.
+ *
+ * @param alarm The alarm to be modified.
+ * @param popToast whether or not a toast should be displayed when done.
+ * @param minorUpdate if true, don't affect any currently snoozed instances.
+ */
+ fun asyncUpdateAlarm(
+ alarm: Alarm,
+ popToast: Boolean,
+ minorUpdate: Boolean
+ ) {
+ val updateTask: AsyncTask<Void, Void, AlarmInstance> =
+ object : AsyncTask<Void, Void, AlarmInstance>() {
+ override fun doInBackground(vararg parameters: Void): AlarmInstance? {
+ val cr: ContentResolver = mAppContext.getContentResolver()
+
+ // Update alarm
+ Alarm.updateAlarm(cr, alarm)
+ if (minorUpdate) {
+ // just update the instance in the database and update notifications.
+ val instanceList = AlarmInstance.getInstancesByAlarmId(cr, alarm.id)
+ for (instance in instanceList) {
+ // Make a copy of the existing instance
+ val newInstance = AlarmInstance(instance)
+ // Copy over minor change data to the instance; we don't know
+ // exactly which minor field changed, so just copy them all.
+ newInstance.mVibrate = alarm.vibrate
+ newInstance.mRingtone = alarm.alert
+ newInstance.mLabel = alarm.label
+ // Since we copied the mId of the old instance and the mId is used
+ // as the primary key in the AlarmInstance table, this will replace
+ // the existing instance.
+ AlarmInstance.updateInstance(cr, newInstance)
+ // Update the notification for this instance.
+ AlarmNotifications.updateNotification(mAppContext, newInstance)
+ }
+ return null
+ }
+ // Otherwise, this is a major update and we're going to re-create the alarm
+ AlarmStateManager.deleteAllInstances(mAppContext, alarm.id)
+
+ return if (alarm.enabled) setupAlarmInstance(alarm) else null
+ }
+
+ override fun onPostExecute(instance: AlarmInstance?) {
+ if (popToast && instance != null) {
+ AlarmUtils.popAlarmSetSnackbar(
+ mSnackbarAnchor!!, instance.alarmTime.timeInMillis)
+ }
+ }
+ }
+ updateTask.execute()
+ }
+
+ /**
+ * Deletes an alarm on the background.
+ *
+ * @param alarm The alarm to be deleted.
+ */
+ fun asyncDeleteAlarm(alarm: Alarm?) {
+ val deleteTask: AsyncTask<Void, Void, Boolean> = object : AsyncTask<Void, Void, Boolean>() {
+ override fun doInBackground(vararg parameters: Void): Boolean {
+ // Activity may be closed at this point , make sure data is still valid
+ if (alarm == null) {
+ // Nothing to do here, just return.
+ return false
+ }
+ AlarmStateManager.deleteAllInstances(mAppContext, alarm.id)
+ return Alarm.deleteAlarm(mAppContext.getContentResolver(), alarm.id)
+ }
+
+ override fun onPostExecute(deleted: Boolean) {
+ if (deleted) {
+ mDeletedAlarm = alarm
+ showUndoBar()
+ }
+ }
+ }
+ deleteTask.execute()
+ }
+
+ /**
+ * Show a toast when an alarm is predismissed.
+ *
+ * @param instance Instance being predismissed.
+ */
+ fun showPredismissToast(instance: AlarmInstance) {
+ val time: String = DateFormat.getTimeFormat(mAppContext).format(instance.alarmTime.time)
+ val text: String = mAppContext.getString(R.string.alarm_is_dismissed, time)
+ SnackbarManager.show(Snackbar.make(mSnackbarAnchor!!, text, Snackbar.LENGTH_SHORT))
+ }
+
+ /**
+ * Hides any undo toast.
+ */
+ fun hideUndoBar() {
+ mDeletedAlarm = null
+ SnackbarManager.dismiss()
+ }
+
+ private fun showUndoBar() {
+ val deletedAlarm = mDeletedAlarm
+ val snackbar: Snackbar = Snackbar.make(mSnackbarAnchor!!,
+ mAppContext.getString(R.string.alarm_deleted), Snackbar.LENGTH_LONG)
+ .setAction(R.string.alarm_undo, { _ ->
+ mDeletedAlarm = null
+ asyncAddAlarm(deletedAlarm)
+ })
+ SnackbarManager.show(snackbar)
+ }
+
+ private fun setupAlarmInstance(alarm: Alarm): AlarmInstance {
+ val cr: ContentResolver = mAppContext.getContentResolver()
+ var newInstance = alarm.createInstanceAfter(Calendar.getInstance())
+ newInstance = AlarmInstance.addInstance(cr, newInstance)
+ // Register instance to state manager
+ AlarmStateManager.registerInstance(mAppContext, newInstance, true)
+ return newInstance
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/ScrollHandler.java b/src/com/android/deskclock/alarms/ScrollHandler.kt
similarity index 79%
rename from src/com/android/deskclock/alarms/ScrollHandler.java
rename to src/com/android/deskclock/alarms/ScrollHandler.kt
index 168bed9..ddcf5f4 100644
--- a/src/com/android/deskclock/alarms/ScrollHandler.java
+++ b/src/com/android/deskclock/alarms/ScrollHandler.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,20 +14,20 @@
* limitations under the License.
*/
-package com.android.deskclock.alarms;
+package com.android.deskclock.alarms
/**
* API that handles scrolling when an alarm item is expanded/collapsed.
*/
-public interface ScrollHandler {
+interface ScrollHandler {
/**
* Sets the stable id that view should be scrolled to. The view does not actually scroll yet.
*/
- void setSmoothScrollStableId(long stableId);
+ fun setSmoothScrollStableId(stableId: Long)
/**
* Perform smooth scroll to position.
*/
- void smoothScrollTo(int position);
+ fun smoothScrollTo(position: Int)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/TimePickerDialogFragment.java b/src/com/android/deskclock/alarms/TimePickerDialogFragment.java
deleted file mode 100644
index 33fc757..0000000
--- a/src/com/android/deskclock/alarms/TimePickerDialogFragment.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms;
-
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.TimePickerDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import androidx.appcompat.app.AlertDialog;
-import android.text.format.DateFormat;
-import android.widget.TimePicker;
-
-import com.android.deskclock.Utils;
-
-import java.util.Calendar;
-
-/**
- * DialogFragment used to show TimePicker.
- */
-public class TimePickerDialogFragment extends DialogFragment {
-
- /**
- * Tag for timer picker fragment in FragmentManager.
- */
- private static final String TAG = "TimePickerDialogFragment";
-
- private static final String ARG_HOUR = TAG + "_hour";
- private static final String ARG_MINUTE = TAG + "_minute";
-
- @Override
- @SuppressWarnings("deprecation")
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final OnTimeSetListener listener = ((OnTimeSetListener) getParentFragment());
-
- final Calendar now = Calendar.getInstance();
- final Bundle args = getArguments() == null ? Bundle.EMPTY : getArguments();
- final int hour = args.getInt(ARG_HOUR, now.get(Calendar.HOUR_OF_DAY));
- final int minute = args.getInt(ARG_MINUTE, now.get(Calendar.MINUTE));
-
- if (Utils.isLOrLater()) {
- final Context context = getActivity();
- return new TimePickerDialog(context, new TimePickerDialog.OnTimeSetListener() {
- @Override
- public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
- listener.onTimeSet(TimePickerDialogFragment.this, hourOfDay, minute);
- }
- }, hour, minute, DateFormat.is24HourFormat(context));
- } else {
- final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- final Context context = builder.getContext();
-
- final TimePicker timePicker = new TimePicker(context);
- timePicker.setCurrentHour(hour);
- timePicker.setCurrentMinute(minute);
- timePicker.setIs24HourView(DateFormat.is24HourFormat(context));
-
- return builder.setView(timePicker)
- .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- listener.onTimeSet(TimePickerDialogFragment.this,
- timePicker.getCurrentHour(), timePicker.getCurrentMinute());
- }
- }).setNegativeButton(android.R.string.cancel, null /* listener */)
- .create();
- }
- }
-
- public static void show(Fragment fragment) {
- show(fragment, -1 /* hour */, -1 /* minute */);
- }
-
- public static void show(Fragment parentFragment, int hourOfDay, int minute) {
- if (!(parentFragment instanceof OnTimeSetListener)) {
- throw new IllegalArgumentException("Fragment must implement OnTimeSetListener");
- }
-
- final FragmentManager manager = parentFragment.getChildFragmentManager();
- if (manager == null || manager.isDestroyed()) {
- return;
- }
-
- // Make sure the dialog isn't already added.
- removeTimeEditDialog(manager);
-
- final TimePickerDialogFragment fragment = new TimePickerDialogFragment();
-
- final Bundle args = new Bundle();
- if (hourOfDay >= 0 && hourOfDay < 24) {
- args.putInt(ARG_HOUR, hourOfDay);
- }
- if (minute >= 0 && minute < 60) {
- args.putInt(ARG_MINUTE, minute);
- }
-
- fragment.setArguments(args);
- fragment.show(manager, TAG);
- }
-
- public static void removeTimeEditDialog(FragmentManager manager) {
- if (manager != null) {
- final Fragment prev = manager.findFragmentByTag(TAG);
- if (prev != null) {
- manager.beginTransaction().remove(prev).commit();
- }
- }
- }
-
- /**
- * The callback interface used to indicate the user is done filling in the time (e.g. they
- * clicked on the 'OK' button).
- */
- public interface OnTimeSetListener {
- /**
- * Called when the user is done setting a new time and the dialog has closed.
- *
- * @param fragment the fragment associated with this listener
- * @param hourOfDay the hour that was set
- * @param minute the minute that was set
- */
- void onTimeSet(TimePickerDialogFragment fragment, int hourOfDay, int minute);
- }
-}
diff --git a/src/com/android/deskclock/alarms/TimePickerDialogFragment.kt b/src/com/android/deskclock/alarms/TimePickerDialogFragment.kt
new file mode 100644
index 0000000..d419766
--- /dev/null
+++ b/src/com/android/deskclock/alarms/TimePickerDialogFragment.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms
+
+import android.app.Dialog
+import android.app.TimePickerDialog
+import android.content.Context
+import android.os.Bundle
+import android.text.format.DateFormat
+import android.widget.TimePicker
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+
+import com.android.deskclock.Utils
+
+import java.util.Calendar
+
+/**
+ * DialogFragment used to show TimePicker.
+ */
+class TimePickerDialogFragment : DialogFragment() {
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val listener = getParentFragment() as OnTimeSetListener
+
+ val now = Calendar.getInstance()
+ val args: Bundle = arguments ?: Bundle.EMPTY
+ val hour: Int = args.getInt(ARG_HOUR, now[Calendar.HOUR_OF_DAY])
+ val minute: Int = args.getInt(ARG_MINUTE, now[Calendar.MINUTE])
+ return if (Utils.isLOrLater) {
+ val context: Context = requireActivity()
+ TimePickerDialog(context, { _, hourOfDay, minuteOfHour ->
+ listener.onTimeSet(this@TimePickerDialogFragment, hourOfDay, minuteOfHour)
+ }, hour, minute, DateFormat.is24HourFormat(context))
+ } else {
+ val builder: AlertDialog.Builder = AlertDialog.Builder(requireActivity())
+ val context: Context = builder.getContext()
+
+ val timePicker = TimePicker(context)
+ timePicker.setCurrentHour(hour)
+ timePicker.setCurrentMinute(minute)
+ timePicker.setIs24HourView(DateFormat.is24HourFormat(context))
+
+ builder.setView(timePicker)
+ .setPositiveButton(android.R.string.ok, { _, _ ->
+ listener.onTimeSet(this@TimePickerDialogFragment,
+ timePicker.getCurrentHour(), timePicker.getCurrentMinute())
+ }).setNegativeButton(android.R.string.cancel, null /* listener */)
+ .create()
+ }
+ }
+
+ /**
+ * The callback interface used to indicate the user is done filling in the time (e.g. they
+ * clicked on the 'OK' button).
+ */
+ interface OnTimeSetListener {
+ /**
+ * Called when the user is done setting a new time and the dialog has closed.
+ *
+ * @param fragment the fragment associated with this listener
+ * @param hourOfDay the hour that was set
+ * @param minute the minute that was set
+ */
+ fun onTimeSet(fragment: TimePickerDialogFragment?, hourOfDay: Int, minute: Int)
+ }
+
+ companion object {
+ /**
+ * Tag for timer picker fragment in FragmentManager.
+ */
+ private const val TAG = "TimePickerDialogFragment"
+
+ private const val ARG_HOUR = TAG + "_hour"
+ private const val ARG_MINUTE = TAG + "_minute"
+
+ @JvmStatic
+ fun show(fragment: Fragment) {
+ show(fragment, -1 /* hour */, -1 /* minute */)
+ }
+
+ fun show(parentFragment: Fragment, hourOfDay: Int, minute: Int) {
+ require(parentFragment is OnTimeSetListener) {
+ "Fragment must implement OnTimeSetListener"
+ }
+
+ val manager: FragmentManager = parentFragment.getChildFragmentManager()
+ if (manager == null || manager.isDestroyed()) {
+ return
+ }
+
+ // Make sure the dialog isn't already added.
+ removeTimeEditDialog(manager)
+
+ val fragment = TimePickerDialogFragment()
+
+ val args = Bundle()
+ if (hourOfDay in 0..23) {
+ args.putInt(ARG_HOUR, hourOfDay)
+ }
+ if (minute in 0..59) {
+ args.putInt(ARG_MINUTE, minute)
+ }
+
+ fragment.setArguments(args)
+ fragment.show(manager, TAG)
+ }
+
+ @JvmStatic
+ fun removeTimeEditDialog(manager: FragmentManager?) {
+ manager?.let { manager ->
+ val prev: Fragment? = manager.findFragmentByTag(TAG)
+ prev?.let {
+ manager.beginTransaction().remove(it).commit()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.java b/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.java
deleted file mode 100644
index 48748f4..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms.dataadapter;
-
-import android.os.Bundle;
-
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.alarms.AlarmTimeClickHandler;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-public class AlarmItemHolder extends ItemAdapter.ItemHolder<Alarm> {
-
- private static final java.lang.String EXPANDED_KEY = "expanded";
- private final AlarmInstance mAlarmInstance;
- private final AlarmTimeClickHandler mAlarmTimeClickHandler;
- private boolean mExpanded;
-
- public AlarmItemHolder(Alarm alarm, AlarmInstance alarmInstance,
- AlarmTimeClickHandler alarmTimeClickHandler) {
- super(alarm, alarm.id);
- mAlarmInstance = alarmInstance;
- mAlarmTimeClickHandler = alarmTimeClickHandler;
- }
-
- @Override
- public int getItemViewType() {
- return isExpanded() ?
- ExpandedAlarmViewHolder.VIEW_TYPE : CollapsedAlarmViewHolder.VIEW_TYPE;
- }
-
- public AlarmTimeClickHandler getAlarmTimeClickHandler() {
- return mAlarmTimeClickHandler;
- }
-
- public AlarmInstance getAlarmInstance() {
- return mAlarmInstance;
- }
-
- public void expand() {
- if (!isExpanded()) {
- mExpanded = true;
- notifyItemChanged();
- }
- }
-
- public void collapse() {
- if (isExpanded()) {
- mExpanded = false;
- notifyItemChanged();
- }
- }
-
- public boolean isExpanded() {
- return mExpanded;
- }
-
- @Override
- public void onSaveInstanceState(Bundle bundle) {
- super.onSaveInstanceState(bundle);
- bundle.putBoolean(EXPANDED_KEY, mExpanded);
- }
-
- @Override
- public void onRestoreInstanceState(Bundle bundle) {
- super.onRestoreInstanceState(bundle);
- mExpanded = bundle.getBoolean(EXPANDED_KEY);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.kt b/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.kt
new file mode 100644
index 0000000..e794d00
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/AlarmItemHolder.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms.dataadapter
+
+import android.os.Bundle
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.alarms.AlarmTimeClickHandler
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+
+class AlarmItemHolder(
+ alarm: Alarm,
+ val alarmInstance: AlarmInstance?,
+ val alarmTimeClickHandler: AlarmTimeClickHandler
+) : ItemHolder<Alarm>(alarm, alarm.id) {
+ var isExpanded = false
+ private set
+
+ override fun getItemViewType(): Int {
+ return if (isExpanded) {
+ ExpandedAlarmViewHolder.VIEW_TYPE
+ } else {
+ CollapsedAlarmViewHolder.VIEW_TYPE
+ }
+ }
+
+ fun expand() {
+ if (!isExpanded) {
+ isExpanded = true
+ notifyItemChanged()
+ }
+ }
+
+ fun collapse() {
+ if (isExpanded) {
+ isExpanded = false
+ notifyItemChanged()
+ }
+ }
+
+ override fun onSaveInstanceState(bundle: Bundle) {
+ super.onSaveInstanceState(bundle)
+ bundle.putBoolean(EXPANDED_KEY, isExpanded)
+ }
+
+ override fun onRestoreInstanceState(bundle: Bundle) {
+ super.onRestoreInstanceState(bundle)
+ isExpanded = bundle.getBoolean(EXPANDED_KEY)
+ }
+
+ companion object {
+ private const val EXPANDED_KEY = "expanded"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java
deleted file mode 100644
index d9c4986..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms.dataadapter;
-
-import android.content.Context;
-import android.view.View;
-import android.widget.CompoundButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.ItemAnimator;
-import com.android.deskclock.R;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.widget.TextTime;
-
-/**
- * Abstract ViewHolder for alarm time items.
- */
-public abstract class AlarmItemViewHolder extends ItemAdapter.ItemViewHolder<AlarmItemHolder>
- implements ItemAnimator.OnAnimateChangeListener {
-
- private static final float CLOCK_ENABLED_ALPHA = 1f;
- private static final float CLOCK_DISABLED_ALPHA = 0.69f;
-
- public static final float ANIM_STANDARD_DELAY_MULTIPLIER = 1f / 6f;
- public static final float ANIM_LONG_DURATION_MULTIPLIER = 2f / 3f;
- public static final float ANIM_SHORT_DURATION_MULTIPLIER = 1f / 4f;
- public static final float ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER =
- 1f - ANIM_LONG_DURATION_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER;
- public static final float ANIM_LONG_DELAY_INCREMENT_MULTIPLIER =
- 1f - ANIM_STANDARD_DELAY_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER;
-
- public static final String ANIMATE_REPEAT_DAYS = "ANIMATE_REPEAT_DAYS";
-
- public final TextTime clock;
- public final CompoundButton onOff;
- public final ImageView arrow;
- public final TextView preemptiveDismissButton;
-
- public AlarmItemViewHolder(View itemView) {
- super(itemView);
-
- clock = (TextTime) itemView.findViewById(R.id.digital_clock);
- onOff = (CompoundButton) itemView.findViewById(R.id.onoff);
- arrow = (ImageView) itemView.findViewById(R.id.arrow);
- preemptiveDismissButton =
- (TextView) itemView.findViewById(R.id.preemptive_dismiss_button);
- preemptiveDismissButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- final AlarmInstance alarmInstance = getItemHolder().getAlarmInstance();
- if (alarmInstance != null) {
- getItemHolder().getAlarmTimeClickHandler().dismissAlarmInstance(alarmInstance);
- }
- }
- });
- onOff.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
- @Override
- public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
- getItemHolder().getAlarmTimeClickHandler().setAlarmEnabled(
- getItemHolder().item, checked);
- }
- });
- }
-
- @Override
- protected void onBindItemView(final AlarmItemHolder itemHolder) {
- final Alarm alarm = itemHolder.item;
- bindOnOffSwitch(alarm);
- bindClock(alarm);
- final Context context = itemView.getContext();
- itemView.setContentDescription(clock.getText() + " " + alarm.getLabelOrDefault(context));
- }
-
- protected void bindOnOffSwitch(Alarm alarm) {
- if (onOff.isChecked() != alarm.enabled) {
- onOff.setChecked(alarm.enabled);
- }
- }
-
- protected void bindClock(Alarm alarm) {
- clock.setTime(alarm.hour, alarm.minutes);
- clock.setAlpha(alarm.enabled ? CLOCK_ENABLED_ALPHA : CLOCK_DISABLED_ALPHA);
- }
-
- protected boolean bindPreemptiveDismissButton(Context context, Alarm alarm,
- AlarmInstance alarmInstance) {
- final boolean canBind = alarm.canPreemptivelyDismiss() && alarmInstance != null;
- if (canBind) {
- preemptiveDismissButton.setVisibility(View.VISIBLE);
- final String dismissText = alarm.instanceState == AlarmInstance.SNOOZE_STATE
- ? context.getString(R.string.alarm_alert_snooze_until,
- AlarmUtils.getAlarmText(context, alarmInstance, false))
- : context.getString(R.string.alarm_alert_dismiss_text);
- preemptiveDismissButton.setText(dismissText);
- preemptiveDismissButton.setClickable(true);
- } else {
- preemptiveDismissButton.setVisibility(View.GONE);
- preemptiveDismissButton.setClickable(false);
- }
- return canBind;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.kt b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.kt
new file mode 100644
index 0000000..e957761
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/AlarmItemViewHolder.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms.dataadapter
+
+import android.content.Context
+import android.view.View
+import android.widget.CompoundButton
+import android.widget.ImageView
+import android.widget.TextView
+
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.ItemAnimator.OnAnimateChangeListener
+import com.android.deskclock.R
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.provider.AlarmInstance
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.widget.TextTime
+
+/**
+ * Abstract ViewHolder for alarm time items.
+ */
+abstract class AlarmItemViewHolder(itemView: View)
+ : ItemViewHolder<AlarmItemHolder>(itemView), OnAnimateChangeListener {
+ val clock: TextTime = itemView.findViewById(R.id.digital_clock)
+ val onOff: CompoundButton = itemView.findViewById(R.id.onoff) as CompoundButton
+ val arrow: ImageView = itemView.findViewById(R.id.arrow) as ImageView
+ val preemptiveDismissButton: TextView =
+ itemView.findViewById(R.id.preemptive_dismiss_button) as TextView
+
+ init {
+ preemptiveDismissButton.setOnClickListener { _ ->
+ val alarmInstance = itemHolder!!.alarmInstance
+ if (alarmInstance != null) {
+ itemHolder!!.alarmTimeClickHandler.dismissAlarmInstance(alarmInstance)
+ }
+ }
+ onOff.setOnCheckedChangeListener { _, checked ->
+ itemHolder!!.alarmTimeClickHandler.setAlarmEnabled(itemHolder!!.item, checked)
+ }
+ }
+
+ override fun onBindItemView(itemHolder: AlarmItemHolder) {
+ val alarm = itemHolder.item
+ bindOnOffSwitch(alarm)
+ bindClock(alarm)
+ val context: Context = itemView.getContext()
+ itemView.setContentDescription(clock.text.toString() + " " +
+ alarm.getLabelOrDefault(context))
+ }
+
+ protected fun bindOnOffSwitch(alarm: Alarm) {
+ if (onOff.isChecked() != alarm.enabled) {
+ onOff.isChecked = alarm.enabled
+ }
+ }
+
+ protected fun bindClock(alarm: Alarm) {
+ clock.setTime(alarm.hour, alarm.minutes)
+ clock.alpha = if (alarm.enabled) CLOCK_ENABLED_ALPHA else CLOCK_DISABLED_ALPHA
+ }
+
+ protected fun bindPreemptiveDismissButton(
+ context: Context,
+ alarm: Alarm,
+ alarmInstance: AlarmInstance?
+ ): Boolean {
+ val canBind = alarm.canPreemptivelyDismiss() && alarmInstance != null
+ if (canBind) {
+ preemptiveDismissButton.visibility = View.VISIBLE
+ val dismissText: String = if (alarm.instanceState == InstancesColumns.SNOOZE_STATE) {
+ context.getString(R.string.alarm_alert_snooze_until,
+ AlarmUtils.getAlarmText(context, alarmInstance!!, false))
+ } else {
+ context.getString(R.string.alarm_alert_dismiss_text)
+ }
+ preemptiveDismissButton.text = dismissText
+ preemptiveDismissButton.isClickable = true
+ } else {
+ preemptiveDismissButton.visibility = View.GONE
+ preemptiveDismissButton.isClickable = false
+ }
+ return canBind
+ }
+
+ companion object {
+ private const val CLOCK_ENABLED_ALPHA = 1f
+ private const val CLOCK_DISABLED_ALPHA = 0.69f
+
+ const val ANIM_STANDARD_DELAY_MULTIPLIER = 1f / 6f
+ const val ANIM_LONG_DURATION_MULTIPLIER = 2f / 3f
+ const val ANIM_SHORT_DURATION_MULTIPLIER = 1f / 4f
+ const val ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER =
+ 1f - ANIM_LONG_DURATION_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER
+ const val ANIM_LONG_DELAY_INCREMENT_MULTIPLIER =
+ 1f - ANIM_STANDARD_DELAY_MULTIPLIER - ANIM_SHORT_DURATION_MULTIPLIER
+ const val ANIMATE_REPEAT_DAYS = "ANIMATE_REPEAT_DAYS"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java
deleted file mode 100644
index 70ed1bb..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.java
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms.dataadapter;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.graphics.Rect;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.deskclock.AnimatorUtils;
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Weekdays;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-
-import java.util.Calendar;
-import java.util.List;
-
-/**
- * A ViewHolder containing views for an alarm item in collapsed stated.
- */
-public final class CollapsedAlarmViewHolder extends AlarmItemViewHolder {
-
- public static final int VIEW_TYPE = R.layout.alarm_time_collapsed;
-
- private final TextView alarmLabel;
- public final TextView daysOfWeek;
- private final TextView upcomingInstanceLabel;
- private final View hairLine;
-
- private CollapsedAlarmViewHolder(View itemView) {
- super(itemView);
-
- alarmLabel = (TextView) itemView.findViewById(R.id.label);
- daysOfWeek = (TextView) itemView.findViewById(R.id.days_of_week);
- upcomingInstanceLabel = (TextView) itemView.findViewById(R.id.upcoming_instance_label);
- hairLine = itemView.findViewById(R.id.hairline);
-
- // Expand handler
- itemView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock);
- getItemHolder().expand();
- }
- });
- alarmLabel.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock);
- getItemHolder().expand();
- }
- });
- arrow.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Events.sendAlarmEvent(R.string.action_expand, R.string.label_deskclock);
- getItemHolder().expand();
- }
- });
- // Edit time handler
- clock.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- getItemHolder().getAlarmTimeClickHandler().onClockClicked(getItemHolder().item);
- Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock);
- getItemHolder().expand();
- }
- });
-
- itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- }
-
- @Override
- protected void onBindItemView(AlarmItemHolder itemHolder) {
- super.onBindItemView(itemHolder);
- final Alarm alarm = itemHolder.item;
- final AlarmInstance alarmInstance = itemHolder.getAlarmInstance();
- final Context context = itemView.getContext();
- bindRepeatText(context, alarm);
- bindReadOnlyLabel(context, alarm);
- bindUpcomingInstance(context, alarm);
- bindPreemptiveDismissButton(context, alarm, alarmInstance);
- }
-
- private void bindReadOnlyLabel(Context context, Alarm alarm) {
- if (alarm.label != null && alarm.label.length() != 0) {
- alarmLabel.setText(alarm.label);
- alarmLabel.setVisibility(View.VISIBLE);
- alarmLabel.setContentDescription(context.getString(R.string.label_description)
- + " " + alarm.label);
- } else {
- alarmLabel.setVisibility(View.GONE);
- }
- }
-
- private void bindRepeatText(Context context, Alarm alarm) {
- if (alarm.daysOfWeek.isRepeating()) {
- final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
- final String daysOfWeekText = alarm.daysOfWeek.toString(context, weekdayOrder);
- daysOfWeek.setText(daysOfWeekText);
-
- final String string = alarm.daysOfWeek.toAccessibilityString(context, weekdayOrder);
- daysOfWeek.setContentDescription(string);
-
- daysOfWeek.setVisibility(View.VISIBLE);
- } else {
- daysOfWeek.setVisibility(View.GONE);
- }
- }
-
- private void bindUpcomingInstance(Context context, Alarm alarm) {
- if (alarm.daysOfWeek.isRepeating()) {
- upcomingInstanceLabel.setVisibility(View.GONE);
- } else {
- upcomingInstanceLabel.setVisibility(View.VISIBLE);
- final String labelText = Alarm.isTomorrow(alarm, Calendar.getInstance()) ?
- context.getString(R.string.alarm_tomorrow) :
- context.getString(R.string.alarm_today);
- upcomingInstanceLabel.setText(labelText);
- }
- }
-
- @Override
- public Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
- int fromBottom, long duration) {
- /* There are no possible partial animations for collapsed view holders. */
- return null;
- }
-
- @Override
- public Animator onAnimateChange(final ViewHolder oldHolder, ViewHolder newHolder,
- long duration) {
- if (!(oldHolder instanceof AlarmItemViewHolder)
- || !(newHolder instanceof AlarmItemViewHolder)) {
- return null;
- }
-
- final boolean isCollapsing = this == newHolder;
- setChangingViewsAlpha(isCollapsing ? 0f : 1f);
-
- final Animator changeAnimatorSet = isCollapsing
- ? createCollapsingAnimator((AlarmItemViewHolder) oldHolder, duration)
- : createExpandingAnimator((AlarmItemViewHolder) newHolder, duration);
- changeAnimatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- clock.setVisibility(View.VISIBLE);
- onOff.setVisibility(View.VISIBLE);
- arrow.setVisibility(View.VISIBLE);
- arrow.setTranslationY(0f);
- setChangingViewsAlpha(1f);
- arrow.jumpDrawablesToCurrentState();
- }
- });
- return changeAnimatorSet;
- }
-
- private Animator createExpandingAnimator(AlarmItemViewHolder newHolder, long duration) {
- clock.setVisibility(View.INVISIBLE);
- onOff.setVisibility(View.INVISIBLE);
- arrow.setVisibility(View.INVISIBLE);
-
- final AnimatorSet alphaAnimatorSet = new AnimatorSet();
- alphaAnimatorSet.playTogether(
- ObjectAnimator.ofFloat(alarmLabel, View.ALPHA, 0f),
- ObjectAnimator.ofFloat(daysOfWeek, View.ALPHA, 0f),
- ObjectAnimator.ofFloat(upcomingInstanceLabel, View.ALPHA, 0f),
- ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 0f),
- ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f));
- alphaAnimatorSet.setDuration((long) (duration * ANIM_SHORT_DURATION_MULTIPLIER));
-
- final View oldView = itemView;
- final View newView = newHolder.itemView;
- final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView)
- .setDuration(duration);
- boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(alphaAnimatorSet, boundsAnimator);
- return animatorSet;
- }
-
- private Animator createCollapsingAnimator(AlarmItemViewHolder oldHolder, long duration) {
- final AnimatorSet alphaAnimatorSet = new AnimatorSet();
- alphaAnimatorSet.playTogether(
- ObjectAnimator.ofFloat(alarmLabel, View.ALPHA, 1f),
- ObjectAnimator.ofFloat(daysOfWeek, View.ALPHA, 1f),
- ObjectAnimator.ofFloat(upcomingInstanceLabel, View.ALPHA, 1f),
- ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 1f),
- ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f));
- final long standardDelay = (long) (duration * ANIM_STANDARD_DELAY_MULTIPLIER);
- alphaAnimatorSet.setDuration(standardDelay);
- alphaAnimatorSet.setStartDelay(duration - standardDelay);
-
- final View oldView = oldHolder.itemView;
- final View newView = itemView;
- final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView)
- .setDuration(duration);
- boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- final View oldArrow = oldHolder.arrow;
- final Rect oldArrowRect = new Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight());
- final Rect newArrowRect = new Rect(0, 0, arrow.getWidth(), arrow.getHeight());
- ((ViewGroup) newView).offsetDescendantRectToMyCoords(arrow, newArrowRect);
- ((ViewGroup) oldView).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect);
- final float arrowTranslationY = oldArrowRect.bottom - newArrowRect.bottom;
- arrow.setTranslationY(arrowTranslationY);
- arrow.setVisibility(View.VISIBLE);
- clock.setVisibility(View.VISIBLE);
- onOff.setVisibility(View.VISIBLE);
-
- final Animator arrowAnimation = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
- .setDuration(duration);
- arrowAnimation.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(alphaAnimatorSet, boundsAnimator, arrowAnimation);
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- AnimatorUtils.startDrawableAnimation(arrow);
- }
- });
- return animatorSet;
- }
-
- private void setChangingViewsAlpha(float alpha) {
- alarmLabel.setAlpha(alpha);
- daysOfWeek.setAlpha(alpha);
- upcomingInstanceLabel.setAlpha(alpha);
- hairLine.setAlpha(alpha);
- preemptiveDismissButton.setAlpha(alpha);
- }
-
- public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
- private final LayoutInflater mLayoutInflater;
-
- public Factory(LayoutInflater layoutInflater) {
- mLayoutInflater = layoutInflater;
- }
-
- @Override
- public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
- return new CollapsedAlarmViewHolder(mLayoutInflater.inflate(
- viewType, parent, false /* attachToRoot */));
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.kt b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.kt
new file mode 100644
index 0000000..2b72c39
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/CollapsedAlarmViewHolder.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms.dataadapter
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+
+import java.util.Calendar
+
+/**
+ * A ViewHolder containing views for an alarm item in collapsed stated.
+ */
+class CollapsedAlarmViewHolder private constructor(itemView: View) : AlarmItemViewHolder(itemView) {
+ private val alarmLabel: TextView = itemView.findViewById(R.id.label) as TextView
+ val daysOfWeek: TextView = itemView.findViewById(R.id.days_of_week) as TextView
+ private val upcomingInstanceLabel: TextView =
+ itemView.findViewById(R.id.upcoming_instance_label) as TextView
+ private val hairLine: View = itemView.findViewById(R.id.hairline)
+
+ init {
+ // Expand handler
+ itemView.setOnClickListener { _ ->
+ Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock)
+ itemHolder?.expand()
+ }
+ alarmLabel.setOnClickListener { _ ->
+ Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock)
+ itemHolder?.expand()
+ }
+ arrow.setOnClickListener { _ ->
+ Events.sendAlarmEvent(R.string.action_expand, R.string.label_deskclock)
+ itemHolder?.expand()
+ }
+ // Edit time handler
+ clock.setOnClickListener { _ ->
+ itemHolder!!.alarmTimeClickHandler.onClockClicked(itemHolder!!.item)
+ Events.sendAlarmEvent(R.string.action_expand_implied, R.string.label_deskclock)
+ itemHolder?.expand()
+ }
+
+ itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO)
+ }
+
+ override fun onBindItemView(itemHolder: AlarmItemHolder) {
+ super.onBindItemView(itemHolder)
+ val alarm = itemHolder.item
+ val alarmInstance = itemHolder.alarmInstance
+ val context: Context = itemView.getContext()
+ bindRepeatText(context, alarm)
+ bindReadOnlyLabel(context, alarm)
+ bindUpcomingInstance(context, alarm)
+ bindPreemptiveDismissButton(context, alarm, alarmInstance)
+ }
+
+ private fun bindReadOnlyLabel(context: Context, alarm: Alarm) {
+ if (!alarm.label.isNullOrEmpty()) {
+ alarmLabel.text = alarm.label
+ alarmLabel.visibility = View.VISIBLE
+ alarmLabel.setContentDescription(context.getString(R.string.label_description)
+ .toString() + " " + alarm.label)
+ } else {
+ alarmLabel.visibility = View.GONE
+ }
+ }
+
+ private fun bindRepeatText(context: Context, alarm: Alarm) {
+ if (alarm.daysOfWeek.isRepeating) {
+ val weekdayOrder = DataModel.dataModel.weekdayOrder
+ val daysOfWeekText = alarm.daysOfWeek.toString(context, weekdayOrder)
+ daysOfWeek.text = daysOfWeekText
+
+ val string = alarm.daysOfWeek.toAccessibilityString(context, weekdayOrder)
+ daysOfWeek.setContentDescription(string)
+
+ daysOfWeek.visibility = View.VISIBLE
+ } else {
+ daysOfWeek.visibility = View.GONE
+ }
+ }
+
+ private fun bindUpcomingInstance(context: Context, alarm: Alarm) {
+ if (alarm.daysOfWeek.isRepeating) {
+ upcomingInstanceLabel.visibility = View.GONE
+ } else {
+ upcomingInstanceLabel.visibility = View.VISIBLE
+ val labelText: String = if (Alarm.isTomorrow(alarm, Calendar.getInstance())) {
+ context.getString(R.string.alarm_tomorrow)
+ } else {
+ context.getString(R.string.alarm_today)
+ }
+ upcomingInstanceLabel.text = labelText
+ }
+ }
+
+ override fun onAnimateChange(
+ payloads: List<Any>?,
+ fromLeft: Int,
+ fromTop: Int,
+ fromRight: Int,
+ fromBottom: Int,
+ duration: Long
+ ): Animator? {
+ /* There are no possible partial animations for collapsed view holders. */
+ return null
+ }
+
+ override fun onAnimateChange(
+ oldHolder: ViewHolder,
+ newHolder: ViewHolder,
+ duration: Long
+ ): Animator? {
+ if (oldHolder !is AlarmItemViewHolder ||
+ newHolder !is AlarmItemViewHolder) {
+ return null
+ }
+
+ val isCollapsing = this == newHolder
+ setChangingViewsAlpha(if (isCollapsing) 0f else 1f)
+
+ val changeAnimatorSet: Animator = if (isCollapsing) {
+ createCollapsingAnimator(oldHolder, duration)
+ } else {
+ createExpandingAnimator(newHolder, duration)
+ }
+ changeAnimatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator?) {
+ clock.visibility = View.VISIBLE
+ onOff.visibility = View.VISIBLE
+ arrow.visibility = View.VISIBLE
+ arrow.setTranslationY(0f)
+ setChangingViewsAlpha(1f)
+ arrow.jumpDrawablesToCurrentState()
+ }
+ })
+ return changeAnimatorSet
+ }
+
+ private fun createExpandingAnimator(newHolder: AlarmItemViewHolder, duration: Long): Animator {
+ clock.visibility = View.INVISIBLE
+ onOff.visibility = View.INVISIBLE
+ arrow.visibility = View.INVISIBLE
+
+ val alphaAnimatorSet = AnimatorSet()
+ alphaAnimatorSet.playTogether(
+ ObjectAnimator.ofFloat(alarmLabel, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(daysOfWeek, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(upcomingInstanceLabel, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 0f),
+ ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f))
+ alphaAnimatorSet.setDuration((duration * ANIM_SHORT_DURATION_MULTIPLIER).toLong())
+
+ val oldView: View = itemView
+ val newView: View = newHolder.itemView
+ val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView)
+ .setDuration(duration)
+ boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(alphaAnimatorSet, boundsAnimator)
+ return animatorSet
+ }
+
+ private fun createCollapsingAnimator(oldHolder: AlarmItemViewHolder, duration: Long): Animator {
+ val alphaAnimatorSet = AnimatorSet()
+ alphaAnimatorSet.playTogether(
+ ObjectAnimator.ofFloat(alarmLabel, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(daysOfWeek, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(upcomingInstanceLabel, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(preemptiveDismissButton, View.ALPHA, 1f),
+ ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f))
+ val standardDelay = (duration * ANIM_STANDARD_DELAY_MULTIPLIER).toLong()
+ alphaAnimatorSet.setDuration(standardDelay)
+ alphaAnimatorSet.setStartDelay(duration - standardDelay)
+
+ val oldView: View = oldHolder.itemView
+ val newView: View = itemView
+ val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView)
+ .setDuration(duration)
+ boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ val oldArrow: View = oldHolder.arrow
+ val oldArrowRect = Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight())
+ val newArrowRect = Rect(0, 0, arrow.getWidth(), arrow.getHeight())
+ (newView as ViewGroup).offsetDescendantRectToMyCoords(arrow, newArrowRect)
+ (oldView as ViewGroup).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect)
+ val arrowTranslationY: Float = (oldArrowRect.bottom - newArrowRect.bottom).toFloat()
+ arrow.setTranslationY(arrowTranslationY)
+ arrow.visibility = View.VISIBLE
+ clock.visibility = View.VISIBLE
+ onOff.visibility = View.VISIBLE
+
+ val arrowAnimation: Animator = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
+ .setDuration(duration)
+ arrowAnimation.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(alphaAnimatorSet, boundsAnimator, arrowAnimation)
+ animatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator?) {
+ AnimatorUtils.startDrawableAnimation(arrow)
+ }
+ })
+ return animatorSet
+ }
+
+ private fun setChangingViewsAlpha(alpha: Float) {
+ alarmLabel.alpha = alpha
+ daysOfWeek.alpha = alpha
+ upcomingInstanceLabel.alpha = alpha
+ hairLine.alpha = alpha
+ preemptiveDismissButton.alpha = alpha
+ }
+
+ class Factory(private val layoutInflater: LayoutInflater) : ItemViewHolder.Factory {
+ override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+ return CollapsedAlarmViewHolder(layoutInflater.inflate(
+ viewType, parent, false /* attachToRoot */))
+ }
+ }
+
+ companion object {
+ @JvmField
+ val VIEW_TYPE: Int = R.layout.alarm_time_collapsed
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java
deleted file mode 100644
index 59a26e0..0000000
--- a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.java
+++ /dev/null
@@ -1,531 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.alarms.dataadapter;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Vibrator;
-import androidx.core.content.ContextCompat;
-import androidx.recyclerview.widget.RecyclerView.ViewHolder;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.deskclock.AnimatorUtils;
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.R;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.alarms.AlarmTimeClickHandler;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.provider.AlarmInstance;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.List;
-
-import static android.content.Context.VIBRATOR_SERVICE;
-import static android.view.View.TRANSLATION_Y;
-
-/**
- * A ViewHolder containing views for an alarm item in expanded state.
- */
-public final class ExpandedAlarmViewHolder extends AlarmItemViewHolder {
- public static final int VIEW_TYPE = R.layout.alarm_time_expanded;
-
- public final CheckBox repeat;
- private final TextView editLabel;
- public final LinearLayout repeatDays;
- private final CompoundButton[] dayButtons = new CompoundButton[7];
- public final CheckBox vibrate;
- public final TextView ringtone;
- public final TextView delete;
- private final View hairLine;
-
- private final boolean mHasVibrator;
-
- private ExpandedAlarmViewHolder(View itemView, boolean hasVibrator) {
- super(itemView);
-
- mHasVibrator = hasVibrator;
-
- delete = (TextView) itemView.findViewById(R.id.delete);
- repeat = (CheckBox) itemView.findViewById(R.id.repeat_onoff);
- vibrate = (CheckBox) itemView.findViewById(R.id.vibrate_onoff);
- ringtone = (TextView) itemView.findViewById(R.id.choose_ringtone);
- editLabel = (TextView) itemView.findViewById(R.id.edit_label);
- repeatDays = (LinearLayout) itemView.findViewById(R.id.repeat_days);
- hairLine = itemView.findViewById(R.id.hairline);
-
- final Context context = itemView.getContext();
- itemView.setBackground(new LayerDrawable(new Drawable[] {
- ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded),
- ThemeUtils.resolveDrawable(context, R.attr.selectableItemBackground)
- }));
-
- // Build button for each day.
- final LayoutInflater inflater = LayoutInflater.from(context);
- final List<Integer> weekdays = DataModel.getDataModel().getWeekdayOrder().getCalendarDays();
- for (int i = 0; i < 7; i++) {
- final View dayButtonFrame = inflater.inflate(R.layout.day_button, repeatDays,
- false /* attachToRoot */);
- final CompoundButton dayButton =
- (CompoundButton) dayButtonFrame.findViewById(R.id.day_button_box);
- final int weekday = weekdays.get(i);
- dayButton.setText(UiDataModel.getUiDataModel().getShortWeekday(weekday));
- dayButton.setContentDescription(UiDataModel.getUiDataModel().getLongWeekday(weekday));
- repeatDays.addView(dayButtonFrame);
- dayButtons[i] = dayButton;
- }
-
- // Cannot set in xml since we need compat functionality for API < 21
- final Drawable labelIcon = Utils.getVectorDrawable(context, R.drawable.ic_label);
- editLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(labelIcon, null, null, null);
- final Drawable deleteIcon = Utils.getVectorDrawable(context, R.drawable.ic_delete_small);
- delete.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null);
-
- // Collapse handler
- itemView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Events.sendAlarmEvent(R.string.action_collapse_implied, R.string.label_deskclock);
- getItemHolder().collapse();
- }
- });
- arrow.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Events.sendAlarmEvent(R.string.action_collapse, R.string.label_deskclock);
- getItemHolder().collapse();
- }
- });
- // Edit time handler
- clock.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- getAlarmTimeClickHandler().onClockClicked(getItemHolder().item);
- }
- });
- // Edit label handler
- editLabel.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- getAlarmTimeClickHandler().onEditLabelClicked(getItemHolder().item);
- }
- });
- // Vibrator checkbox handler
- vibrate.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- getAlarmTimeClickHandler().setAlarmVibrationEnabled(getItemHolder().item,
- ((CheckBox) v).isChecked());
- }
- });
- // Ringtone editor handler
- ringtone.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- getAlarmTimeClickHandler().onRingtoneClicked(context, getItemHolder().item);
- }
- });
- // Delete alarm handler
- delete.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- getAlarmTimeClickHandler().onDeleteClicked(getItemHolder());
- v.announceForAccessibility(context.getString(R.string.alarm_deleted));
- }
- });
- // Repeat checkbox handler
- repeat.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- final boolean checked = ((CheckBox) view).isChecked();
- getAlarmTimeClickHandler().setAlarmRepeatEnabled(getItemHolder().item, checked);
- getItemHolder().notifyItemChanged(ANIMATE_REPEAT_DAYS);
- }
- });
- // Day buttons handler
- for (int i = 0; i < dayButtons.length; i++) {
- final int buttonIndex = i;
- dayButtons[i].setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- final boolean isChecked = ((CompoundButton) view).isChecked();
- getAlarmTimeClickHandler().setDayOfWeekEnabled(getItemHolder().item,
- isChecked, buttonIndex);
- }
- });
- }
-
- itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- }
-
- @Override
- protected void onBindItemView(final AlarmItemHolder itemHolder) {
- super.onBindItemView(itemHolder);
-
- final Alarm alarm = itemHolder.item;
- final AlarmInstance alarmInstance = itemHolder.getAlarmInstance();
- final Context context = itemView.getContext();
- bindEditLabel(context, alarm);
- bindDaysOfWeekButtons(alarm, context);
- bindVibrator(alarm);
- bindRingtone(context, alarm);
- bindPreemptiveDismissButton(context, alarm, alarmInstance);
- }
-
- private void bindRingtone(Context context, Alarm alarm) {
- final String title = DataModel.getDataModel().getRingtoneTitle(alarm.alert);
- ringtone.setText(title);
-
- final String description = context.getString(R.string.ringtone_description);
- ringtone.setContentDescription(description + " " + title);
-
- final boolean silent = Utils.RINGTONE_SILENT.equals(alarm.alert);
- final Drawable icon = Utils.getVectorDrawable(context,
- silent ? R.drawable.ic_ringtone_silent : R.drawable.ic_ringtone);
- ringtone.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
- }
-
- private void bindDaysOfWeekButtons(Alarm alarm, Context context) {
- final List<Integer> weekdays = DataModel.getDataModel().getWeekdayOrder().getCalendarDays();
- for (int i = 0; i < weekdays.size(); i++) {
- final CompoundButton dayButton = dayButtons[i];
- if (alarm.daysOfWeek.isBitOn(weekdays.get(i))) {
- dayButton.setChecked(true);
- dayButton.setTextColor(ThemeUtils.resolveColor(context,
- android.R.attr.windowBackground));
- } else {
- dayButton.setChecked(false);
- dayButton.setTextColor(Color.WHITE);
- }
- }
- if (alarm.daysOfWeek.isRepeating()) {
- repeat.setChecked(true);
- repeatDays.setVisibility(View.VISIBLE);
- } else {
- repeat.setChecked(false);
- repeatDays.setVisibility(View.GONE);
- }
- }
-
- private void bindEditLabel(Context context, Alarm alarm) {
- editLabel.setText(alarm.label);
- editLabel.setContentDescription(alarm.label != null && alarm.label.length() > 0
- ? context.getString(R.string.label_description) + " " + alarm.label
- : context.getString(R.string.no_label_specified));
- }
-
- private void bindVibrator(Alarm alarm) {
- if (!mHasVibrator) {
- vibrate.setVisibility(View.INVISIBLE);
- } else {
- vibrate.setVisibility(View.VISIBLE);
- vibrate.setChecked(alarm.vibrate);
- }
- }
-
- private AlarmTimeClickHandler getAlarmTimeClickHandler() {
- return getItemHolder().getAlarmTimeClickHandler();
- }
-
- @Override
- public Animator onAnimateChange(List<Object> payloads, int fromLeft, int fromTop, int fromRight,
- int fromBottom, long duration) {
- if (payloads == null || payloads.isEmpty() || !payloads.contains(ANIMATE_REPEAT_DAYS)) {
- return null;
- }
-
- final boolean isExpansion = repeatDays.getVisibility() == View.VISIBLE;
- final int height = repeatDays.getHeight();
- setTranslationY(isExpansion ? -height : 0f, isExpansion ? -height : height);
- repeatDays.setVisibility(View.VISIBLE);
- repeatDays.setAlpha(isExpansion ? 0f : 1f);
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(AnimatorUtils.getBoundsAnimator(itemView,
- fromLeft, fromTop, fromRight, fromBottom,
- itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()),
- ObjectAnimator.ofFloat(repeatDays, View.ALPHA, isExpansion ? 1f : 0f),
- ObjectAnimator.ofFloat(repeatDays, TRANSLATION_Y, isExpansion ? 0f : -height),
- ObjectAnimator.ofFloat(ringtone, TRANSLATION_Y, 0f),
- ObjectAnimator.ofFloat(vibrate, TRANSLATION_Y, 0f),
- ObjectAnimator.ofFloat(editLabel, TRANSLATION_Y, 0f),
- ObjectAnimator.ofFloat(preemptiveDismissButton, TRANSLATION_Y, 0f),
- ObjectAnimator.ofFloat(hairLine, TRANSLATION_Y, 0f),
- ObjectAnimator.ofFloat(delete, TRANSLATION_Y, 0f),
- ObjectAnimator.ofFloat(arrow, TRANSLATION_Y, 0f));
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- setTranslationY(0f, 0f);
- repeatDays.setAlpha(1f);
- repeatDays.setVisibility(isExpansion ? View.VISIBLE : View.GONE);
- itemView.requestLayout();
- }
- });
- animatorSet.setDuration(duration);
- animatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- return animatorSet;
- }
-
- private void setTranslationY(float repeatDaysTranslationY, float translationY) {
- repeatDays.setTranslationY(repeatDaysTranslationY);
- ringtone.setTranslationY(translationY);
- vibrate.setTranslationY(translationY);
- editLabel.setTranslationY(translationY);
- preemptiveDismissButton.setTranslationY(translationY);
- hairLine.setTranslationY(translationY);
- delete.setTranslationY(translationY);
- arrow.setTranslationY(translationY);
- }
-
- @Override
- public Animator onAnimateChange(final ViewHolder oldHolder, ViewHolder newHolder,
- long duration) {
- if (!(oldHolder instanceof AlarmItemViewHolder)
- || !(newHolder instanceof AlarmItemViewHolder)) {
- return null;
- }
-
- final boolean isExpanding = this == newHolder;
- AnimatorUtils.setBackgroundAlpha(itemView, isExpanding ? 0 : 255);
- setChangingViewsAlpha(isExpanding ? 0f : 1f);
-
- final Animator changeAnimatorSet = isExpanding
- ? createExpandingAnimator((AlarmItemViewHolder) oldHolder, duration)
- : createCollapsingAnimator((AlarmItemViewHolder) newHolder, duration);
- changeAnimatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- AnimatorUtils.setBackgroundAlpha(itemView, 255);
- clock.setVisibility(View.VISIBLE);
- onOff.setVisibility(View.VISIBLE);
- arrow.setVisibility(View.VISIBLE);
- arrow.setTranslationY(0f);
- setChangingViewsAlpha(1f);
- arrow.jumpDrawablesToCurrentState();
- }
- });
- return changeAnimatorSet;
- }
-
- private Animator createCollapsingAnimator(AlarmItemViewHolder newHolder, long duration) {
- arrow.setVisibility(View.INVISIBLE);
- clock.setVisibility(View.INVISIBLE);
- onOff.setVisibility(View.INVISIBLE);
-
- final boolean daysVisible = repeatDays.getVisibility() == View.VISIBLE;
- final int numberOfItems = countNumberOfItems();
-
- final View oldView = itemView;
- final View newView = newHolder.itemView;
-
- final Animator backgroundAnimator = ObjectAnimator.ofPropertyValuesHolder(oldView,
- PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 255, 0));
- backgroundAnimator.setDuration(duration);
-
- final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView);
- boundsAnimator.setDuration(duration);
- boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- final long shortDuration = (long) (duration * ANIM_SHORT_DURATION_MULTIPLIER);
- final Animator repeatAnimation = ObjectAnimator.ofFloat(repeat, View.ALPHA, 0f)
- .setDuration(shortDuration);
- final Animator editLabelAnimation = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 0f)
- .setDuration(shortDuration);
- final Animator repeatDaysAnimation = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 0f)
- .setDuration(shortDuration);
- final Animator vibrateAnimation = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 0f)
- .setDuration(shortDuration);
- final Animator ringtoneAnimation = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 0f)
- .setDuration(shortDuration);
- final Animator dismissAnimation = ObjectAnimator.ofFloat(preemptiveDismissButton,
- View.ALPHA, 0f).setDuration(shortDuration);
- final Animator deleteAnimation = ObjectAnimator.ofFloat(delete, View.ALPHA, 0f)
- .setDuration(shortDuration);
- final Animator hairLineAnimation = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f)
- .setDuration(shortDuration);
-
- // Set the staggered delays; use the first portion (duration * (1 - 1/4 - 1/6)) of the time,
- // so that the final animation, with a duration of 1/4 the total duration, finishes exactly
- // before the collapsed holder begins expanding.
- long startDelay = 0L;
- final long delayIncrement = (long) (duration * ANIM_LONG_DELAY_INCREMENT_MULTIPLIER)
- / (numberOfItems - 1);
- deleteAnimation.setStartDelay(startDelay);
- if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
- startDelay += delayIncrement;
- dismissAnimation.setStartDelay(startDelay);
- }
- hairLineAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- editLabelAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- vibrateAnimation.setStartDelay(startDelay);
- ringtoneAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- if (daysVisible) {
- repeatDaysAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- }
- repeatAnimation.setStartDelay(startDelay);
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(backgroundAnimator, boundsAnimator, repeatAnimation,
- repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
- deleteAnimation, hairLineAnimation, dismissAnimation);
- return animatorSet;
- }
-
- private Animator createExpandingAnimator(AlarmItemViewHolder oldHolder, long duration) {
- final View oldView = oldHolder.itemView;
- final View newView = itemView;
- final Animator boundsAnimator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView);
- boundsAnimator.setDuration(duration);
- boundsAnimator.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- final Animator backgroundAnimator = ObjectAnimator.ofPropertyValuesHolder(newView,
- PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255));
- backgroundAnimator.setDuration(duration);
-
- final View oldArrow = oldHolder.arrow;
- final Rect oldArrowRect = new Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight());
- final Rect newArrowRect = new Rect(0, 0, arrow.getWidth(), arrow.getHeight());
- ((ViewGroup) newView).offsetDescendantRectToMyCoords(arrow, newArrowRect);
- ((ViewGroup) oldView).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect);
- final float arrowTranslationY = oldArrowRect.bottom - newArrowRect.bottom;
-
- arrow.setTranslationY(arrowTranslationY);
- arrow.setVisibility(View.VISIBLE);
- clock.setVisibility(View.VISIBLE);
- onOff.setVisibility(View.VISIBLE);
-
- final long longDuration = (long) (duration * ANIM_LONG_DURATION_MULTIPLIER);
- final Animator repeatAnimation = ObjectAnimator.ofFloat(repeat, View.ALPHA, 1f)
- .setDuration(longDuration);
- final Animator repeatDaysAnimation = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 1f)
- .setDuration(longDuration);
- final Animator ringtoneAnimation = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 1f)
- .setDuration(longDuration);
- final Animator dismissAnimation = ObjectAnimator.ofFloat(preemptiveDismissButton,
- View.ALPHA, 1f).setDuration(longDuration);
- final Animator vibrateAnimation = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 1f)
- .setDuration(longDuration);
- final Animator editLabelAnimation = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 1f)
- .setDuration(longDuration);
- final Animator hairLineAnimation = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f)
- .setDuration(longDuration);
- final Animator deleteAnimation = ObjectAnimator.ofFloat(delete, View.ALPHA, 1f)
- .setDuration(longDuration);
- final Animator arrowAnimation = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
- .setDuration(duration);
- arrowAnimation.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- // Set the stagger delays; delay the first by the amount of time it takes for the collapse
- // to complete, then stagger the expansion with the remaining time.
- long startDelay = (long) (duration * ANIM_STANDARD_DELAY_MULTIPLIER);
- final int numberOfItems = countNumberOfItems();
- final long delayIncrement = (long) (duration * ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER)
- / (numberOfItems - 1);
- repeatAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- final boolean daysVisible = repeatDays.getVisibility() == View.VISIBLE;
- if (daysVisible) {
- repeatDaysAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- }
- ringtoneAnimation.setStartDelay(startDelay);
- vibrateAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- editLabelAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- hairLineAnimation.setStartDelay(startDelay);
- if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
- dismissAnimation.setStartDelay(startDelay);
- startDelay += delayIncrement;
- }
- deleteAnimation.setStartDelay(startDelay);
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(backgroundAnimator, repeatAnimation, boundsAnimator,
- repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
- deleteAnimation, hairLineAnimation, dismissAnimation, arrowAnimation);
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- AnimatorUtils.startDrawableAnimation(arrow);
- }
- });
- return animatorSet;
- }
-
- private int countNumberOfItems() {
- // Always between 4 and 6 items.
- int numberOfItems = 4;
- if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
- numberOfItems++;
- }
- if (repeatDays.getVisibility() == View.VISIBLE) {
- numberOfItems++;
- }
- return numberOfItems;
- }
-
- private void setChangingViewsAlpha(float alpha) {
- repeat.setAlpha(alpha);
- editLabel.setAlpha(alpha);
- repeatDays.setAlpha(alpha);
- vibrate.setAlpha(alpha);
- ringtone.setAlpha(alpha);
- hairLine.setAlpha(alpha);
- delete.setAlpha(alpha);
- preemptiveDismissButton.setAlpha(alpha);
- }
-
- public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
-
- private final LayoutInflater mLayoutInflater;
- private final boolean mHasVibrator;
-
- public Factory(Context context) {
- mLayoutInflater = LayoutInflater.from(context);
- mHasVibrator = ((Vibrator) context.getSystemService(VIBRATOR_SERVICE)).hasVibrator();
- }
-
- @Override
- public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
- final View itemView = mLayoutInflater.inflate(viewType, parent, false);
- return new ExpandedAlarmViewHolder(itemView, mHasVibrator);
- }
- }
-}
diff --git a/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.kt b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.kt
new file mode 100644
index 0000000..5215b2b
--- /dev/null
+++ b/src/com/android/deskclock/alarms/dataadapter/ExpandedAlarmViewHolder.kt
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.alarms.dataadapter
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.content.Context
+import android.content.Context.VIBRATOR_SERVICE
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
+import android.os.Vibrator
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.TRANSLATION_Y
+import android.view.ViewGroup
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.alarms.AlarmTimeClickHandler
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * A ViewHolder containing views for an alarm item in expanded state.
+ */
+class ExpandedAlarmViewHolder private constructor(itemView: View, private val mHasVibrator: Boolean)
+ : AlarmItemViewHolder(itemView) {
+ val repeat: CheckBox = itemView.findViewById(R.id.repeat_onoff) as CheckBox
+ private val editLabel: TextView = itemView.findViewById(R.id.edit_label) as TextView
+ val repeatDays: LinearLayout = itemView.findViewById(R.id.repeat_days) as LinearLayout
+ private val dayButtons: Array<CompoundButton?> = arrayOfNulls<CompoundButton>(7)
+ val vibrate: CheckBox = itemView.findViewById(R.id.vibrate_onoff) as CheckBox
+ val ringtone: TextView = itemView.findViewById(R.id.choose_ringtone) as TextView
+ val delete: TextView = itemView.findViewById(R.id.delete) as TextView
+ private val hairLine: View = itemView.findViewById(R.id.hairline)
+
+ init {
+ val context: Context = itemView.getContext()
+ itemView.setBackground(LayerDrawable(arrayOf(
+ ContextCompat.getDrawable(context, R.drawable.alarm_background_expanded),
+ ThemeUtils.resolveDrawable(context, R.attr.selectableItemBackground)
+ )))
+
+ // Build button for each day.
+ val inflater: LayoutInflater = LayoutInflater.from(context)
+ val weekdays = DataModel.dataModel.weekdayOrder.calendarDays
+ for (i in 0..6) {
+ val dayButtonFrame: View = inflater.inflate(R.layout.day_button, repeatDays,
+ false /* attachToRoot */)
+ val dayButton: CompoundButton =
+ dayButtonFrame.findViewById(R.id.day_button_box) as CompoundButton
+ val weekday = weekdays[i]
+ dayButton.text = UiDataModel.uiDataModel.getShortWeekday(weekday)
+ dayButton.setContentDescription(UiDataModel.uiDataModel.getLongWeekday(weekday))
+ repeatDays.addView(dayButtonFrame)
+ dayButtons[i] = dayButton
+ }
+
+ // Cannot set in xml since we need compat functionality for API < 21
+ val labelIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_label)
+ editLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(labelIcon, null, null, null)
+ val deleteIcon: Drawable? = Utils.getVectorDrawable(context, R.drawable.ic_delete_small)
+ delete.setCompoundDrawablesRelativeWithIntrinsicBounds(deleteIcon, null, null, null)
+
+ // Collapse handler
+ itemView.setOnClickListener { _ ->
+ Events.sendAlarmEvent(R.string.action_collapse_implied, R.string.label_deskclock)
+ itemHolder?.collapse()
+ }
+ arrow.setOnClickListener { _ ->
+ Events.sendAlarmEvent(R.string.action_collapse, R.string.label_deskclock)
+ itemHolder?.collapse()
+ }
+ // Edit time handler
+ clock.setOnClickListener { _ ->
+ alarmTimeClickHandler.onClockClicked(itemHolder!!.item)
+ }
+ // Edit label handler
+ editLabel.setOnClickListener { _ ->
+ alarmTimeClickHandler.onEditLabelClicked(itemHolder!!.item)
+ }
+ // Vibrator checkbox handler
+ vibrate.setOnClickListener { view ->
+ alarmTimeClickHandler.setAlarmVibrationEnabled(itemHolder!!.item,
+ (view as CheckBox).isChecked)
+ }
+ // Ringtone editor handler
+ ringtone.setOnClickListener { _ ->
+ alarmTimeClickHandler.onRingtoneClicked(context, itemHolder!!.item)
+ }
+ // Delete alarm handler
+ delete.setOnClickListener { view ->
+ alarmTimeClickHandler.onDeleteClicked(itemHolder!!)
+ view.announceForAccessibility(context.getString(R.string.alarm_deleted))
+ }
+ // Repeat checkbox handler
+ repeat.setOnClickListener { view ->
+ val checked: Boolean = (view as CheckBox).isChecked
+ alarmTimeClickHandler.setAlarmRepeatEnabled(itemHolder!!.item, checked)
+ itemHolder?.notifyItemChanged(ANIMATE_REPEAT_DAYS)
+ }
+ // Day buttons handler
+ for (i in dayButtons.indices) {
+ dayButtons[i]?.setOnClickListener { view ->
+ val isChecked: Boolean = (view as CompoundButton).isChecked
+ alarmTimeClickHandler.setDayOfWeekEnabled(itemHolder!!.item, isChecked, i)
+ }
+ }
+ itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO)
+ }
+
+ override fun onBindItemView(itemHolder: AlarmItemHolder) {
+ super.onBindItemView(itemHolder)
+
+ val alarm = itemHolder.item
+ val alarmInstance = itemHolder.alarmInstance
+ val context: Context = itemView.getContext()
+ bindEditLabel(context, alarm)
+ bindDaysOfWeekButtons(alarm, context)
+ bindVibrator(alarm)
+ bindRingtone(context, alarm)
+ bindPreemptiveDismissButton(context, alarm, alarmInstance)
+ }
+
+ private fun bindRingtone(context: Context, alarm: Alarm) {
+ val title = DataModel.dataModel.getRingtoneTitle(alarm.alert!!)
+ ringtone.text = title
+
+ val description: String = context.getString(R.string.ringtone_description)
+ ringtone.setContentDescription("$description $title")
+
+ val silent: Boolean = Utils.RINGTONE_SILENT == alarm.alert
+ val icon: Drawable? = Utils.getVectorDrawable(context,
+ if (silent) R.drawable.ic_ringtone_silent else R.drawable.ic_ringtone)
+ ringtone.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
+ }
+
+ private fun bindDaysOfWeekButtons(alarm: Alarm, context: Context) {
+ val weekdays = DataModel.dataModel.weekdayOrder.calendarDays
+ for (i in weekdays.indices) {
+ val dayButton: CompoundButton? = dayButtons[i]
+ dayButton?.let {
+ if (alarm.daysOfWeek.isBitOn(weekdays[i])) {
+ dayButton.isChecked = true
+ dayButton.setTextColor(ThemeUtils.resolveColor(context,
+ android.R.attr.windowBackground))
+ } else {
+ dayButton.isChecked = false
+ dayButton.setTextColor(Color.WHITE)
+ }
+ }
+ }
+ if (alarm.daysOfWeek.isRepeating) {
+ repeat.isChecked = true
+ repeatDays.visibility = View.VISIBLE
+ } else {
+ repeat.isChecked = false
+ repeatDays.visibility = View.GONE
+ }
+ }
+
+ private fun bindEditLabel(context: Context, alarm: Alarm) {
+ editLabel.text = alarm.label
+ editLabel.contentDescription = if (!alarm.label.isNullOrEmpty()) {
+ context.getString(R.string.label_description).toString() + " " + alarm.label
+ } else {
+ context.getString(R.string.no_label_specified)
+ }
+ }
+
+ private fun bindVibrator(alarm: Alarm) {
+ if (!mHasVibrator) {
+ vibrate.visibility = View.INVISIBLE
+ } else {
+ vibrate.visibility = View.VISIBLE
+ vibrate.isChecked = alarm.vibrate
+ }
+ }
+
+ private val alarmTimeClickHandler: AlarmTimeClickHandler
+ get() = itemHolder!!.alarmTimeClickHandler
+
+ override fun onAnimateChange(
+ payloads: List<Any>?,
+ fromLeft: Int,
+ fromTop: Int,
+ fromRight: Int,
+ fromBottom: Int,
+ duration: Long
+ ): Animator? {
+ if (payloads == null || payloads.isEmpty() || !payloads.contains(ANIMATE_REPEAT_DAYS)) {
+ return null
+ }
+
+ val isExpansion = repeatDays.getVisibility() == View.VISIBLE
+ val height: Int = repeatDays.getHeight()
+ setTranslationY(if (isExpansion) {
+ -height.toFloat()
+ } else {
+ 0f
+ }, if (isExpansion) {
+ -height.toFloat()
+ } else {
+ height.toFloat()
+ })
+ repeatDays.visibility = View.VISIBLE
+ repeatDays.alpha = if (isExpansion) 0f else 1f
+
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(AnimatorUtils.getBoundsAnimator(itemView,
+ fromLeft, fromTop, fromRight, fromBottom,
+ itemView.getLeft(), itemView.getTop(), itemView.getRight(), itemView.getBottom()),
+ ObjectAnimator.ofFloat(repeatDays, View.ALPHA, if (isExpansion) 1f else 0f),
+ ObjectAnimator.ofFloat(repeatDays, TRANSLATION_Y, if (isExpansion) {
+ 0f
+ } else {
+ -height.toFloat()
+ }),
+ ObjectAnimator.ofFloat(ringtone, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(vibrate, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(editLabel, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(preemptiveDismissButton, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(hairLine, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(delete, TRANSLATION_Y, 0f),
+ ObjectAnimator.ofFloat(arrow, TRANSLATION_Y, 0f))
+ animatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator?) {
+ setTranslationY(0f, 0f)
+ repeatDays.alpha = 1f
+ repeatDays.visibility = if (isExpansion) View.VISIBLE else View.GONE
+ itemView.requestLayout()
+ }
+ })
+ animatorSet.duration = duration
+ animatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ return animatorSet
+ }
+
+ private fun setTranslationY(repeatDaysTranslationY: Float, translationY: Float) {
+ repeatDays.setTranslationY(repeatDaysTranslationY)
+ ringtone.setTranslationY(translationY)
+ vibrate.setTranslationY(translationY)
+ editLabel.setTranslationY(translationY)
+ preemptiveDismissButton.setTranslationY(translationY)
+ hairLine.setTranslationY(translationY)
+ delete.setTranslationY(translationY)
+ arrow.setTranslationY(translationY)
+ }
+
+ override fun onAnimateChange(
+ oldHolder: ViewHolder,
+ newHolder: ViewHolder,
+ duration: Long
+ ): Animator? {
+ if (oldHolder !is AlarmItemViewHolder ||
+ newHolder !is AlarmItemViewHolder) {
+ return null
+ }
+
+ val isExpanding = this == newHolder
+ AnimatorUtils.setBackgroundAlpha(itemView, if (isExpanding) 0 else 255)
+ setChangingViewsAlpha(if (isExpanding) 0f else 1f)
+
+ val changeAnimatorSet: Animator = if (isExpanding) {
+ createExpandingAnimator(oldHolder, duration)
+ } else {
+ createCollapsingAnimator(newHolder, duration)
+ }
+ changeAnimatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animator: Animator?) {
+ AnimatorUtils.setBackgroundAlpha(itemView, 255)
+ clock.visibility = View.VISIBLE
+ onOff.visibility = View.VISIBLE
+ arrow.visibility = View.VISIBLE
+ arrow.setTranslationY(0f)
+ setChangingViewsAlpha(1f)
+ arrow.jumpDrawablesToCurrentState()
+ }
+ })
+ return changeAnimatorSet
+ }
+
+ private fun createCollapsingAnimator(newHolder: AlarmItemViewHolder, duration: Long): Animator {
+ arrow.visibility = View.INVISIBLE
+ clock.visibility = View.INVISIBLE
+ onOff.visibility = View.INVISIBLE
+
+ val daysVisible = repeatDays.getVisibility() == View.VISIBLE
+ val numberOfItems = countNumberOfItems()
+
+ val oldView: View = itemView
+ val newView: View = newHolder.itemView
+
+ val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(oldView,
+ PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 255, 0))
+ backgroundAnimator.duration = duration
+
+ val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(oldView, oldView, newView)
+ boundsAnimator.duration = duration
+ boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ val shortDuration = (duration * ANIM_SHORT_DURATION_MULTIPLIER).toLong()
+ val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 0f)
+ .setDuration(shortDuration)
+ val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 0f)
+ .setDuration(shortDuration)
+ val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 0f)
+ .setDuration(shortDuration)
+ val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 0f)
+ .setDuration(shortDuration)
+ val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 0f)
+ .setDuration(shortDuration)
+ val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton,
+ View.ALPHA, 0f).setDuration(shortDuration)
+ val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 0f)
+ .setDuration(shortDuration)
+ val hairLineAnimation: Animator = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 0f)
+ .setDuration(shortDuration)
+
+ // Set the staggered delays; use the first portion (duration * (1 - 1/4 - 1/6)) of the time,
+ // so that the final animation, with a duration of 1/4 the total duration, finishes exactly
+ // before the collapsed holder begins expanding.
+ var startDelay = 0L
+ val delayIncrement = (duration * ANIM_LONG_DELAY_INCREMENT_MULTIPLIER).toLong() /
+ (numberOfItems - 1)
+ deleteAnimation.setStartDelay(startDelay)
+ if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
+ startDelay += delayIncrement
+ dismissAnimation.setStartDelay(startDelay)
+ }
+ hairLineAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ editLabelAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ vibrateAnimation.setStartDelay(startDelay)
+ ringtoneAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ if (daysVisible) {
+ repeatDaysAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ }
+ repeatAnimation.setStartDelay(startDelay)
+
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(backgroundAnimator, boundsAnimator, repeatAnimation,
+ repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
+ deleteAnimation, hairLineAnimation, dismissAnimation)
+ return animatorSet
+ }
+
+ private fun createExpandingAnimator(oldHolder: AlarmItemViewHolder, duration: Long): Animator {
+ val oldView: View = oldHolder.itemView
+ val newView: View = itemView
+ val boundsAnimator: Animator = AnimatorUtils.getBoundsAnimator(newView, oldView, newView)
+ boundsAnimator.duration = duration
+ boundsAnimator.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ val backgroundAnimator: Animator = ObjectAnimator.ofPropertyValuesHolder(newView,
+ PropertyValuesHolder.ofInt(AnimatorUtils.BACKGROUND_ALPHA, 0, 255))
+ backgroundAnimator.duration = duration
+
+ val oldArrow: View = oldHolder.arrow
+ val oldArrowRect = Rect(0, 0, oldArrow.getWidth(), oldArrow.getHeight())
+ val newArrowRect = Rect(0, 0, arrow.getWidth(), arrow.getHeight())
+ (newView as ViewGroup).offsetDescendantRectToMyCoords(arrow, newArrowRect)
+ (oldView as ViewGroup).offsetDescendantRectToMyCoords(oldArrow, oldArrowRect)
+ val arrowTranslationY: Float = (oldArrowRect.bottom - newArrowRect.bottom).toFloat()
+
+ arrow.setTranslationY(arrowTranslationY)
+ arrow.visibility = View.VISIBLE
+ clock.visibility = View.VISIBLE
+ onOff.visibility = View.VISIBLE
+
+ val longDuration = (duration * ANIM_LONG_DURATION_MULTIPLIER).toLong()
+ val repeatAnimation: Animator = ObjectAnimator.ofFloat(repeat, View.ALPHA, 1f)
+ .setDuration(longDuration)
+ val repeatDaysAnimation: Animator = ObjectAnimator.ofFloat(repeatDays, View.ALPHA, 1f)
+ .setDuration(longDuration)
+ val ringtoneAnimation: Animator = ObjectAnimator.ofFloat(ringtone, View.ALPHA, 1f)
+ .setDuration(longDuration)
+ val dismissAnimation: Animator = ObjectAnimator.ofFloat(preemptiveDismissButton,
+ View.ALPHA, 1f).setDuration(longDuration)
+ val vibrateAnimation: Animator = ObjectAnimator.ofFloat(vibrate, View.ALPHA, 1f)
+ .setDuration(longDuration)
+ val editLabelAnimation: Animator = ObjectAnimator.ofFloat(editLabel, View.ALPHA, 1f)
+ .setDuration(longDuration)
+ val hairLineAnimation: Animator = ObjectAnimator.ofFloat(hairLine, View.ALPHA, 1f)
+ .setDuration(longDuration)
+ val deleteAnimation: Animator = ObjectAnimator.ofFloat(delete, View.ALPHA, 1f)
+ .setDuration(longDuration)
+ val arrowAnimation: Animator = ObjectAnimator.ofFloat(arrow, View.TRANSLATION_Y, 0f)
+ .setDuration(duration)
+ arrowAnimation.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ // Set the stagger delays; delay the first by the amount of time it takes for the collapse
+ // to complete, then stagger the expansion with the remaining time.
+ var startDelay = (duration * ANIM_STANDARD_DELAY_MULTIPLIER).toLong()
+ val numberOfItems = countNumberOfItems()
+ val delayIncrement = (duration * ANIM_SHORT_DELAY_INCREMENT_MULTIPLIER).toLong() /
+ (numberOfItems - 1)
+ repeatAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ val daysVisible = repeatDays.getVisibility() == View.VISIBLE
+ if (daysVisible) {
+ repeatDaysAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ }
+ ringtoneAnimation.setStartDelay(startDelay)
+ vibrateAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ editLabelAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ hairLineAnimation.setStartDelay(startDelay)
+ if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
+ dismissAnimation.setStartDelay(startDelay)
+ startDelay += delayIncrement
+ }
+ deleteAnimation.setStartDelay(startDelay)
+
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(backgroundAnimator, repeatAnimation, boundsAnimator,
+ repeatDaysAnimation, vibrateAnimation, ringtoneAnimation, editLabelAnimation,
+ deleteAnimation, hairLineAnimation, dismissAnimation, arrowAnimation)
+ animatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator?) {
+ AnimatorUtils.startDrawableAnimation(arrow)
+ }
+ })
+ return animatorSet
+ }
+
+ private fun countNumberOfItems(): Int {
+ // Always between 4 and 6 items.
+ var numberOfItems = 4
+ if (preemptiveDismissButton.getVisibility() == View.VISIBLE) {
+ numberOfItems++
+ }
+ if (repeatDays.getVisibility() == View.VISIBLE) {
+ numberOfItems++
+ }
+ return numberOfItems
+ }
+
+ private fun setChangingViewsAlpha(alpha: Float) {
+ repeat.alpha = alpha
+ editLabel.alpha = alpha
+ repeatDays.alpha = alpha
+ vibrate.alpha = alpha
+ ringtone.alpha = alpha
+ hairLine.alpha = alpha
+ delete.alpha = alpha
+ preemptiveDismissButton.alpha = alpha
+ }
+
+ class Factory(context: Context) : ItemViewHolder.Factory {
+ private val mLayoutInflater: LayoutInflater = LayoutInflater.from(context)
+ private val mHasVibrator: Boolean =
+ (context.getSystemService(VIBRATOR_SERVICE) as Vibrator).hasVibrator()
+
+ override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+ val itemView: View = mLayoutInflater.inflate(viewType, parent, false)
+ return ExpandedAlarmViewHolder(itemView, mHasVibrator)
+ }
+ }
+
+ companion object {
+ @JvmField
+ val VIEW_TYPE: Int = R.layout.alarm_time_expanded
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/Controller.java b/src/com/android/deskclock/controller/Controller.java
deleted file mode 100644
index 76f839a..0000000
--- a/src/com/android/deskclock/controller/Controller.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.controller;
-
-import android.app.Activity;
-import android.content.Context;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.Utils;
-import com.android.deskclock.events.EventTracker;
-
-import static com.android.deskclock.Utils.enforceMainLooper;
-
-/**
- * Interactions with Android framework components responsible for part of the user experience are
- * handled via this singleton.
- */
-public final class Controller {
-
- private static final Controller sController = new Controller();
-
- private Context mContext;
-
- /** The controller that dispatches app events to event trackers. */
- private EventController mEventController;
-
- /** The controller that interacts with voice interaction sessions on M+. */
- private VoiceController mVoiceController;
-
- /** The controller that creates and updates launcher shortcuts on N MR1+ */
- private ShortcutController mShortcutController;
-
- private Controller() {}
-
- public static Controller getController() {
- return sController;
- }
-
- public void setContext(Context context) {
- if (mContext != context) {
- mContext = context.getApplicationContext();
- mEventController = new EventController();
- mVoiceController = new VoiceController();
- if (Utils.isNMR1OrLater()) {
- mShortcutController = new ShortcutController(mContext);
- }
- }
- }
-
- //
- // Event Tracking
- //
-
- /**
- * @param eventTracker to be registered for tracking application events
- */
- public void addEventTracker(EventTracker eventTracker) {
- enforceMainLooper();
- mEventController.addEventTracker(eventTracker);
- }
-
- /**
- * @param eventTracker to be unregistered from tracking application events
- */
- public void removeEventTracker(EventTracker eventTracker) {
- enforceMainLooper();
- mEventController.removeEventTracker(eventTracker);
- }
-
- /**
- * Tracks an event. Events have a category, action and label. This method can be used to track
- * events such as button presses or other user interactions with your application.
- *
- * @param category resource id of event category
- * @param action resource id of event action
- * @param label resource id of event label
- */
- public void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
- mEventController.sendEvent(category, action, label);
- }
-
- //
- // Voice Interaction
- //
-
- public void notifyVoiceSuccess(Activity activity, String message) {
- mVoiceController.notifyVoiceSuccess(activity, message);
- }
-
- public void notifyVoiceFailure(Activity activity, String message) {
- mVoiceController.notifyVoiceFailure(activity, message);
- }
-
- //
- // Shortcuts
- //
-
- public void updateShortcuts() {
- enforceMainLooper();
- if (mShortcutController != null) {
- mShortcutController.updateShortcuts();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/Controller.kt b/src/com/android/deskclock/controller/Controller.kt
new file mode 100644
index 0000000..0b7c82c
--- /dev/null
+++ b/src/com/android/deskclock/controller/Controller.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.controller
+
+import android.app.Activity
+import android.content.Context
+import androidx.annotation.StringRes
+
+import com.android.deskclock.Utils
+import com.android.deskclock.events.EventTracker
+
+/**
+ * Interactions with Android framework components responsible for part of the user experience are
+ * handled via this singleton.
+ */
+class Controller private constructor() {
+ private var mContext: Context? = null
+
+ /** The controller that dispatches app events to event trackers. */
+ private lateinit var mEventController: EventController
+
+ /** The controller that interacts with voice interaction sessions on M+. */
+ private lateinit var mVoiceController: VoiceController
+
+ /** The controller that creates and updates launcher shortcuts on N MR1+ */
+ private var mShortcutController: ShortcutController? = null
+
+ fun setContext(context: Context) {
+ if (mContext != context) {
+ mContext = context.getApplicationContext()
+ mEventController = EventController()
+ mVoiceController = VoiceController()
+ if (Utils.isNMR1OrLater) {
+ mShortcutController = ShortcutController(mContext!!)
+ }
+ }
+ }
+
+ //
+ // Event Tracking
+ //
+
+ /**
+ * @param eventTracker to be registered for tracking application events
+ */
+ fun addEventTracker(eventTracker: EventTracker) {
+ Utils.enforceMainLooper()
+ mEventController.addEventTracker(eventTracker)
+ }
+
+ /**
+ * @param eventTracker to be unregistered from tracking application events
+ */
+ fun removeEventTracker(eventTracker: EventTracker) {
+ Utils.enforceMainLooper()
+ mEventController.removeEventTracker(eventTracker)
+ }
+
+ /**
+ * Tracks an event. Events have a category, action and label. This method can be used to track
+ * events such as button presses or other user interactions with your application.
+ *
+ * @param category resource id of event category
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int) {
+ mEventController.sendEvent(category, action, label)
+ }
+
+ //
+ // Voice Interaction
+ //
+
+ fun notifyVoiceSuccess(activity: Activity, message: String) {
+ mVoiceController.notifyVoiceSuccess(activity, message)
+ }
+
+ fun notifyVoiceFailure(activity: Activity, message: String) {
+ mVoiceController.notifyVoiceFailure(activity, message)
+ }
+
+ //
+ // Shortcuts
+ //
+
+ fun updateShortcuts() {
+ Utils.enforceMainLooper()
+ mShortcutController?.updateShortcuts()
+ }
+
+ companion object {
+ private val sController = Controller()
+
+ @JvmStatic
+ fun getController() = sController
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/EventController.java b/src/com/android/deskclock/controller/EventController.java
deleted file mode 100644
index 457756a..0000000
--- a/src/com/android/deskclock/controller/EventController.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.controller;
-
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.events.EventTracker;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-class EventController {
-
- private final Collection<EventTracker> mEventTrackers = new ArrayList<>();
-
- void addEventTracker(EventTracker eventTracker) {
- mEventTrackers.add(eventTracker);
- }
-
- void removeEventTracker(EventTracker eventTracker) {
- mEventTrackers.remove(eventTracker);
- }
-
- void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
- for (EventTracker eventTracker : mEventTrackers) {
- eventTracker.sendEvent(category, action, label);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/EventController.kt b/src/com/android/deskclock/controller/EventController.kt
new file mode 100644
index 0000000..d2f648f
--- /dev/null
+++ b/src/com/android/deskclock/controller/EventController.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.controller
+
+import androidx.annotation.StringRes
+
+import com.android.deskclock.events.EventTracker
+
+import java.util.ArrayList
+
+internal class EventController {
+ private val mEventTrackers: MutableCollection<EventTracker> = ArrayList()
+
+ fun addEventTracker(eventTracker: EventTracker) {
+ mEventTrackers.add(eventTracker)
+ }
+
+ fun removeEventTracker(eventTracker: EventTracker) {
+ mEventTrackers.remove(eventTracker)
+ }
+
+ fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int) {
+ mEventTrackers.forEach { eventTracker ->
+ eventTracker.sendEvent(category, action, label)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/ShortcutController.java b/src/com/android/deskclock/controller/ShortcutController.java
deleted file mode 100644
index 8eba32f..0000000
--- a/src/com/android/deskclock/controller/ShortcutController.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.controller;
-
-import android.annotation.TargetApi;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ShortcutInfo;
-import android.content.pm.ShortcutManager;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.UserManager;
-import android.provider.AlarmClock;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.HandleApiCalls;
-import com.android.deskclock.HandleShortcuts;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.ScreensaverActivity;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Lap;
-import com.android.deskclock.data.Stopwatch;
-import com.android.deskclock.data.StopwatchListener;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.events.ShortcutEventTracker;
-import com.android.deskclock.stopwatch.StopwatchService;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-@TargetApi(Build.VERSION_CODES.N_MR1)
-class ShortcutController {
-
- private final Context mContext;
- private final ComponentName mComponentName;
- private final ShortcutManager mShortcutManager;
- private final UserManager mUserManager;
-
- ShortcutController(Context context) {
- mContext = context;
- mComponentName = new ComponentName(mContext, DeskClock.class);
- mShortcutManager = mContext.getSystemService(ShortcutManager.class);
- mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- Controller.getController().addEventTracker(new ShortcutEventTracker(mContext));
- DataModel.getDataModel().addStopwatchListener(new StopwatchWatcher());
- }
-
- void updateShortcuts() {
- if (!mUserManager.isUserUnlocked()) {
- LogUtils.i("Skipping shortcut update because user is locked.");
- return;
- }
- try {
- final ShortcutInfo alarm = createNewAlarmShortcut();
- final ShortcutInfo timer = createNewTimerShortcut();
- final ShortcutInfo stopwatch = createStopwatchShortcut();
- final ShortcutInfo screensaver = createScreensaverShortcut();
- mShortcutManager.setDynamicShortcuts(
- Arrays.asList(alarm, timer, stopwatch, screensaver));
- } catch (IllegalStateException e) {
- LogUtils.wtf(e);
- }
- }
-
- private ShortcutInfo createNewAlarmShortcut() {
- final Intent intent = new Intent(AlarmClock.ACTION_SET_ALARM)
- .setClass(mContext, HandleApiCalls.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
- final String setAlarmShortcut = UiDataModel.getUiDataModel()
- .getShortcutId(R.string.category_alarm, R.string.action_create);
- return new ShortcutInfo.Builder(mContext, setAlarmShortcut)
- .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_new_alarm))
- .setActivity(mComponentName)
- .setShortLabel(mContext.getString(R.string.shortcut_new_alarm_short))
- .setLongLabel(mContext.getString(R.string.shortcut_new_alarm_long))
- .setIntent(intent)
- .setRank(0)
- .build();
- }
-
- private ShortcutInfo createNewTimerShortcut() {
- final Intent intent = new Intent(AlarmClock.ACTION_SET_TIMER)
- .setClass(mContext, HandleApiCalls.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
- final String setTimerShortcut = UiDataModel.getUiDataModel()
- .getShortcutId(R.string.category_timer, R.string.action_create);
- return new ShortcutInfo.Builder(mContext, setTimerShortcut)
- .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_new_timer))
- .setActivity(mComponentName)
- .setShortLabel(mContext.getString(R.string.shortcut_new_timer_short))
- .setLongLabel(mContext.getString(R.string.shortcut_new_timer_long))
- .setIntent(intent)
- .setRank(1)
- .build();
- }
-
- private ShortcutInfo createStopwatchShortcut() {
- final @StringRes int action = DataModel.getDataModel().getStopwatch().isRunning()
- ? R.string.action_pause : R.string.action_start;
- final String shortcutId = UiDataModel.getUiDataModel()
- .getShortcutId(R.string.category_stopwatch, action);
- final ShortcutInfo.Builder shortcut = new ShortcutInfo.Builder(mContext, shortcutId)
- .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_stopwatch))
- .setActivity(mComponentName)
- .setRank(2);
- final Intent intent;
- if (DataModel.getDataModel().getStopwatch().isRunning()) {
- intent = new Intent(StopwatchService.ACTION_PAUSE_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
- shortcut.setShortLabel(mContext.getString(R.string.shortcut_pause_stopwatch_short))
- .setLongLabel(mContext.getString(R.string.shortcut_pause_stopwatch_long));
- } else {
- intent = new Intent(StopwatchService.ACTION_START_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
- shortcut.setShortLabel(mContext.getString(R.string.shortcut_start_stopwatch_short))
- .setLongLabel(mContext.getString(R.string.shortcut_start_stopwatch_long));
- }
- intent.setClass(mContext, HandleShortcuts.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- return shortcut
- .setIntent(intent)
- .build();
- }
-
- private ShortcutInfo createScreensaverShortcut() {
- final Intent intent = new Intent(Intent.ACTION_MAIN)
- .setClass(mContext, ScreensaverActivity.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut);
- final String screensaverShortcut = UiDataModel.getUiDataModel()
- .getShortcutId(R.string.category_screensaver, R.string.action_show);
- return new ShortcutInfo.Builder(mContext, screensaverShortcut)
- .setIcon(Icon.createWithResource(mContext, R.drawable.shortcut_screensaver))
- .setActivity(mComponentName)
- .setShortLabel((mContext.getString(R.string.shortcut_start_screensaver_short)))
- .setLongLabel((mContext.getString(R.string.shortcut_start_screensaver_long)))
- .setIntent(intent)
- .setRank(3)
- .build();
- }
-
- private class StopwatchWatcher implements StopwatchListener {
-
- @Override
- public void stopwatchUpdated(Stopwatch before, Stopwatch after) {
- if (!mUserManager.isUserUnlocked()) {
- LogUtils.i("Skipping stopwatch shortcut update because user is locked.");
- return;
- }
- try {
- mShortcutManager.updateShortcuts(
- Collections.singletonList(createStopwatchShortcut()));
- } catch (IllegalStateException e) {
- LogUtils.wtf(e);
- }
- }
-
- @Override
- public void lapAdded(Lap lap) {
- }
- }
-}
diff --git a/src/com/android/deskclock/controller/ShortcutController.kt b/src/com/android/deskclock/controller/ShortcutController.kt
new file mode 100644
index 0000000..fd2cf5d
--- /dev/null
+++ b/src/com/android/deskclock/controller/ShortcutController.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.controller
+
+import android.annotation.TargetApi
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.UserManager
+import android.provider.AlarmClock
+import androidx.annotation.StringRes
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.HandleApiCalls
+import com.android.deskclock.HandleShortcuts
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.ScreensaverActivity
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Lap
+import com.android.deskclock.data.Stopwatch
+import com.android.deskclock.data.StopwatchListener
+import com.android.deskclock.events.Events
+import com.android.deskclock.events.ShortcutEventTracker
+import com.android.deskclock.stopwatch.StopwatchService
+import com.android.deskclock.uidata.UiDataModel
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+internal class ShortcutController(val context: Context) {
+ private val mComponentName = ComponentName(context, DeskClock::class.java)
+ private val mShortcutManager = context.getSystemService(ShortcutManager::class.java)
+ private val mUserManager = context.getSystemService(Context.USER_SERVICE) as UserManager
+
+ init {
+ Controller.getController().addEventTracker(ShortcutEventTracker(context))
+ DataModel.dataModel.addStopwatchListener(StopwatchWatcher())
+ }
+
+ fun updateShortcuts() {
+ if (!mUserManager.isUserUnlocked()) {
+ LogUtils.i("Skipping shortcut update because user is locked.")
+ return
+ }
+ try {
+ val alarm: ShortcutInfo = createNewAlarmShortcut()
+ val timer: ShortcutInfo = createNewTimerShortcut()
+ val stopwatch: ShortcutInfo = createStopwatchShortcut()
+ val screensaver: ShortcutInfo = createScreensaverShortcut()
+ mShortcutManager.setDynamicShortcuts(listOf(alarm, timer, stopwatch, screensaver))
+ } catch (e: IllegalStateException) {
+ LogUtils.wtf(e)
+ }
+ }
+
+ private fun createNewAlarmShortcut(): ShortcutInfo {
+ val intent: Intent = Intent(AlarmClock.ACTION_SET_ALARM)
+ .setClass(context, HandleApiCalls::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+ val setAlarmShortcut = UiDataModel.uiDataModel
+ .getShortcutId(R.string.category_alarm, R.string.action_create)
+ return ShortcutInfo.Builder(context, setAlarmShortcut)
+ .setIcon(Icon.createWithResource(context, R.drawable.shortcut_new_alarm))
+ .setActivity(mComponentName)
+ .setShortLabel(context.getString(R.string.shortcut_new_alarm_short))
+ .setLongLabel(context.getString(R.string.shortcut_new_alarm_long))
+ .setIntent(intent)
+ .setRank(0)
+ .build()
+ }
+
+ private fun createNewTimerShortcut(): ShortcutInfo {
+ val intent: Intent = Intent(AlarmClock.ACTION_SET_TIMER)
+ .setClass(context, HandleApiCalls::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+ val setTimerShortcut = UiDataModel.uiDataModel
+ .getShortcutId(R.string.category_timer, R.string.action_create)
+ return ShortcutInfo.Builder(context, setTimerShortcut)
+ .setIcon(Icon.createWithResource(context, R.drawable.shortcut_new_timer))
+ .setActivity(mComponentName)
+ .setShortLabel(context.getString(R.string.shortcut_new_timer_short))
+ .setLongLabel(context.getString(R.string.shortcut_new_timer_long))
+ .setIntent(intent)
+ .setRank(1)
+ .build()
+ }
+
+ private fun createStopwatchShortcut(): ShortcutInfo {
+ @StringRes val action: Int = if (DataModel.dataModel.stopwatch.isRunning) {
+ R.string.action_pause
+ } else {
+ R.string.action_start
+ }
+ val shortcutId = UiDataModel.uiDataModel
+ .getShortcutId(R.string.category_stopwatch, action)
+ val shortcut: ShortcutInfo.Builder = ShortcutInfo.Builder(context, shortcutId)
+ .setIcon(Icon.createWithResource(context, R.drawable.shortcut_stopwatch))
+ .setActivity(mComponentName)
+ .setRank(2)
+ val intent: Intent
+ if (DataModel.dataModel.stopwatch.isRunning) {
+ intent = Intent(StopwatchService.ACTION_PAUSE_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+ shortcut.setShortLabel(context.getString(R.string.shortcut_pause_stopwatch_short))
+ .setLongLabel(context.getString(R.string.shortcut_pause_stopwatch_long))
+ } else {
+ intent = Intent(StopwatchService.ACTION_START_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+ shortcut.setShortLabel(context.getString(R.string.shortcut_start_stopwatch_short))
+ .setLongLabel(context.getString(R.string.shortcut_start_stopwatch_long))
+ }
+ intent.setClass(context, HandleShortcuts::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ return shortcut
+ .setIntent(intent)
+ .build()
+ }
+
+ private fun createScreensaverShortcut(): ShortcutInfo {
+ val intent: Intent = Intent(Intent.ACTION_MAIN)
+ .setClass(context, ScreensaverActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_shortcut)
+ val screensaverShortcut = UiDataModel.uiDataModel
+ .getShortcutId(R.string.category_screensaver, R.string.action_show)
+ return ShortcutInfo.Builder(context, screensaverShortcut)
+ .setIcon(Icon.createWithResource(context, R.drawable.shortcut_screensaver))
+ .setActivity(mComponentName)
+ .setShortLabel(context.getString(R.string.shortcut_start_screensaver_short))
+ .setLongLabel(context.getString(R.string.shortcut_start_screensaver_long))
+ .setIntent(intent)
+ .setRank(3)
+ .build()
+ }
+
+ private inner class StopwatchWatcher : StopwatchListener {
+
+ override fun stopwatchUpdated(before: Stopwatch, after: Stopwatch) {
+ if (!mUserManager.isUserUnlocked()) {
+ LogUtils.i("Skipping stopwatch shortcut update because user is locked.")
+ return
+ }
+ try {
+ mShortcutManager.updateShortcuts(listOf(createStopwatchShortcut()))
+ } catch (e: IllegalStateException) {
+ LogUtils.wtf(e)
+ }
+ }
+
+ override fun lapAdded(lap: Lap) {}
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/VoiceController.java b/src/com/android/deskclock/controller/VoiceController.java
deleted file mode 100644
index 1eae560..0000000
--- a/src/com/android/deskclock/controller/VoiceController.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.controller;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.VoiceInteractor;
-import android.app.VoiceInteractor.AbortVoiceRequest;
-import android.app.VoiceInteractor.CompleteVoiceRequest;
-import android.app.VoiceInteractor.Prompt;
-import android.os.Build;
-
-import com.android.deskclock.Utils;
-
-@TargetApi(Build.VERSION_CODES.M)
-class VoiceController {
- /**
- * If the {@code activity} is currently hosting a voice interaction session, indicate the voice
- * command was processed successfully.
- *
- * @param activity an Activity that may be hosting a voice interaction session
- * @param message to be spoken to the user to indicate success
- */
- void notifyVoiceSuccess(Activity activity, String message) {
- if (!Utils.isMOrLater()) {
- return;
- }
-
- final VoiceInteractor voiceInteractor = activity.getVoiceInteractor();
- if (voiceInteractor != null) {
- final Prompt prompt = new Prompt(message);
- voiceInteractor.submitRequest(new CompleteVoiceRequest(prompt, null));
- }
- }
-
- /**
- * If the {@code activity} is currently hosting a voice interaction session, indicate the voice
- * command failed and must be aborted.
- *
- * @param activity an Activity that may be hosting a voice interaction session
- * @param message to be spoken to the user to indicate failure
- */
- void notifyVoiceFailure(Activity activity, String message) {
- if (!Utils.isMOrLater()) {
- return;
- }
-
- final VoiceInteractor voiceInteractor = activity.getVoiceInteractor();
- if (voiceInteractor != null) {
- final Prompt prompt = new Prompt(message);
- voiceInteractor.submitRequest(new AbortVoiceRequest(prompt, null));
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/controller/VoiceController.kt b/src/com/android/deskclock/controller/VoiceController.kt
new file mode 100644
index 0000000..b5fb146
--- /dev/null
+++ b/src/com/android/deskclock/controller/VoiceController.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.controller
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.app.VoiceInteractor
+import android.app.VoiceInteractor.AbortVoiceRequest
+import android.app.VoiceInteractor.CompleteVoiceRequest
+import android.app.VoiceInteractor.Prompt
+import android.os.Build
+
+import com.android.deskclock.Utils
+
+@TargetApi(Build.VERSION_CODES.M)
+internal class VoiceController {
+ /**
+ * If the `activity` is currently hosting a voice interaction session, indicate the voice
+ * command was processed successfully.
+ *
+ * @param activity an Activity that may be hosting a voice interaction session
+ * @param message to be spoken to the user to indicate success
+ */
+ fun notifyVoiceSuccess(activity: Activity, message: String) {
+ if (!Utils.isMOrLater) {
+ return
+ }
+
+ val voiceInteractor: VoiceInteractor? = activity.getVoiceInteractor()
+ voiceInteractor?.let {
+ val prompt = Prompt(message)
+ it.submitRequest(CompleteVoiceRequest(prompt, null))
+ }
+ }
+
+ /**
+ * If the `activity` is currently hosting a voice interaction session, indicate the voice
+ * command failed and must be aborted.
+ *
+ * @param activity an Activity that may be hosting a voice interaction session
+ * @param message to be spoken to the user to indicate failure
+ */
+ fun notifyVoiceFailure(activity: Activity, message: String) {
+ if (!Utils.isMOrLater) {
+ return
+ }
+
+ val voiceInteractor: VoiceInteractor? = activity.getVoiceInteractor()
+ voiceInteractor?.let {
+ val prompt = Prompt(message)
+ it.submitRequest(AbortVoiceRequest(prompt, null))
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/AlarmModel.java b/src/com/android/deskclock/data/AlarmModel.java
deleted file mode 100644
index da50fe5..0000000
--- a/src/com/android/deskclock/data/AlarmModel.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-
-import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
-import com.android.deskclock.provider.Alarm;
-
-/**
- * All alarm data will eventually be accessed via this model.
- */
-final class AlarmModel {
-
- /** The model from which settings are fetched. */
- private final SettingsModel mSettingsModel;
-
- /** The uri of the default ringtone to play for new alarms; mirrors last selection. */
- private Uri mDefaultAlarmRingtoneUri;
-
- AlarmModel(Context context, SettingsModel settingsModel) {
- mSettingsModel = settingsModel;
-
- // Clear caches affected by system settings when system settings change.
- final ContentResolver cr = context.getContentResolver();
- final ContentObserver observer = new SystemAlarmAlertChangeObserver();
- cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer);
- }
-
- Uri getDefaultAlarmRingtoneUri() {
- if (mDefaultAlarmRingtoneUri == null) {
- mDefaultAlarmRingtoneUri = mSettingsModel.getDefaultAlarmRingtoneUri();
- }
-
- return mDefaultAlarmRingtoneUri;
- }
-
- void setDefaultAlarmRingtoneUri(Uri uri) {
- // Never set the silent ringtone as default; new alarms should always make sound by default.
- if (!Alarm.NO_RINGTONE_URI.equals(uri)) {
- mSettingsModel.setDefaultAlarmRingtoneUri(uri);
- mDefaultAlarmRingtoneUri = uri;
- }
- }
-
- long getAlarmCrescendoDuration() {
- return mSettingsModel.getAlarmCrescendoDuration();
- }
-
- AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
- return mSettingsModel.getAlarmVolumeButtonBehavior();
- }
-
- int getAlarmTimeout() {
- return mSettingsModel.getAlarmTimeout();
- }
-
- int getSnoozeLength() {
- return mSettingsModel.getSnoozeLength();
- }
-
- /**
- * This receiver is notified when system settings change. Cached information built on
- * those system settings must be cleared.
- */
- private final class SystemAlarmAlertChangeObserver extends ContentObserver {
-
- private SystemAlarmAlertChangeObserver() {
- super(new Handler());
- }
-
- @Override
- public void onChange(boolean selfChange) {
- super.onChange(selfChange);
- mDefaultAlarmRingtoneUri = null;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/AlarmModel.kt b/src/com/android/deskclock/data/AlarmModel.kt
new file mode 100644
index 0000000..df9fe20
--- /dev/null
+++ b/src/com/android/deskclock/data/AlarmModel.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.ContentResolver
+import android.content.Context
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+
+/**
+ * All alarm data will eventually be accessed via this model.
+ */
+internal class AlarmModel(
+ context: Context,
+ /** The model from which settings are fetched. */
+ private val mSettingsModel: SettingsModel
+) {
+
+ /** The uri of the default ringtone to play for new alarms; mirrors last selection. */
+ private var mDefaultAlarmRingtoneUri: Uri? = null
+
+ init {
+ // Clear caches affected by system settings when system settings change.
+ val cr: ContentResolver = context.getContentResolver()
+ val observer: ContentObserver = SystemAlarmAlertChangeObserver()
+ cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer)
+ }
+
+ // Never set the silent ringtone as default; new alarms should always make sound by default.
+ var defaultAlarmRingtoneUri: Uri
+ get() {
+ if (mDefaultAlarmRingtoneUri == null) {
+ mDefaultAlarmRingtoneUri = mSettingsModel.defaultAlarmRingtoneUri
+ }
+
+ return mDefaultAlarmRingtoneUri!!
+ }
+ set(uri) {
+ // Never set the silent ringtone as default; new alarms should always make sound by default.
+ if (!AlarmSettingColumns.NO_RINGTONE_URI.equals(uri)) {
+ mSettingsModel.defaultAlarmRingtoneUri = uri
+ mDefaultAlarmRingtoneUri = uri
+ }
+ }
+
+ val alarmCrescendoDuration: Long
+ get() = mSettingsModel.alarmCrescendoDuration
+
+ val alarmVolumeButtonBehavior: DataModel.AlarmVolumeButtonBehavior
+ get() = mSettingsModel.alarmVolumeButtonBehavior
+
+ val alarmTimeout: Int
+ get() = mSettingsModel.alarmTimeout
+
+ val snoozeLength: Int
+ get() = mSettingsModel.snoozeLength
+
+ /**
+ * This receiver is notified when system settings change. Cached information built on
+ * those system settings must be cleared.
+ */
+ private inner class SystemAlarmAlertChangeObserver
+ : ContentObserver(Handler(Looper.myLooper()!!)) {
+ override fun onChange(selfChange: Boolean) {
+ super.onChange(selfChange)
+ mDefaultAlarmRingtoneUri = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/City.java b/src/com/android/deskclock/data/City.java
deleted file mode 100644
index 7a41d08..0000000
--- a/src/com/android/deskclock/data/City.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import java.text.Collator;
-import java.util.Comparator;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/**
- * A read-only domain object representing a city of the world and associated time information. It
- * also contains static comparators that can be instantiated to order cities in common sort orders.
- */
-public final class City {
-
- /** A unique identifier for the city. */
- private final String mId;
-
- /** An optional numeric index used to order cities for display; -1 if no such index exists. */
- private final int mIndex;
-
- /** An index string used to order cities for display. */
- private final String mIndexString;
-
- /** The display name of the city. */
- private final String mName;
-
- /** The phonetic name of the city used to order cities for display. */
- private final String mPhoneticName;
-
- /** The TimeZone corresponding to the city. */
- private final TimeZone mTimeZone;
-
- /** A cached upper case form of the {@link #mName} used in case-insensitive name comparisons. */
- private String mNameUpperCase;
-
- /**
- * A cached upper case form of the {@link #mName} used in case-insensitive name comparisons
- * which ignore {@link #removeSpecialCharacters(String)} special characters.
- */
- private String mNameUpperCaseNoSpecialCharacters;
-
- City(String id, int index, String indexString, String name, String phoneticName, TimeZone tz) {
- mId = id;
- mIndex = index;
- mIndexString = indexString;
- mName = name;
- mPhoneticName = phoneticName;
- mTimeZone = tz;
- }
-
- public String getId() { return mId; }
- public int getIndex() { return mIndex; }
- public String getName() { return mName; }
- public TimeZone getTimeZone() { return mTimeZone; }
- public String getIndexString() { return mIndexString; }
- public String getPhoneticName() { return mPhoneticName; }
-
- /**
- * @return the city name converted to upper case
- */
- public String getNameUpperCase() {
- if (mNameUpperCase == null) {
- mNameUpperCase = mName.toUpperCase();
- }
- return mNameUpperCase;
- }
-
- /**
- * @return the city name converted to upper case with all special characters removed
- */
- private String getNameUpperCaseNoSpecialCharacters() {
- if (mNameUpperCaseNoSpecialCharacters == null) {
- mNameUpperCaseNoSpecialCharacters = removeSpecialCharacters(getNameUpperCase());
- }
- return mNameUpperCaseNoSpecialCharacters;
- }
-
- /**
- * @param upperCaseQueryNoSpecialCharacters search term with all special characters removed
- * to match against the upper case city name
- * @return {@code true} iff the name of this city starts with the given query
- */
- public boolean matches(String upperCaseQueryNoSpecialCharacters) {
- // By removing all special characters, prefix matching becomes more liberal and it is easier
- // to locate the desired city. e.g. "St. Lucia" is matched by "StL", "St.L", "St L", "St. L"
- return getNameUpperCaseNoSpecialCharacters().startsWith(upperCaseQueryNoSpecialCharacters);
- }
-
- @Override
- public String toString() {
- return String.format(Locale.US,
- "City {id=%s, index=%d, indexString=%s, name=%s, phonetic=%s, tz=%s}",
- mId, mIndex, mIndexString, mName, mPhoneticName, mTimeZone.getID());
- }
-
- /**
- * Strips out any characters considered optional for matching purposes. These include spaces,
- * dashes, periods and apostrophes.
- *
- * @param token a city name or search term
- * @return the given {@code token} without any characters considered optional when matching
- */
- public static String removeSpecialCharacters(String token) {
- return token.replaceAll("[ -.']", "");
- }
-
- /**
- * Orders by:
- *
- * <ol>
- * <li>UTC offset of {@link #getTimeZone() timezone}</li>
- * <li>{@link #getIndex() numeric index}</li>
- * <li>{@link #getIndexString()} alphabetic index}</li>
- * <li>{@link #getPhoneticName() phonetic name}</li>
- * </ol>
- */
- public static final class UtcOffsetComparator implements Comparator<City> {
-
- private final Comparator<City> mDelegate1 = new UtcOffsetIndexComparator();
-
- private final Comparator<City> mDelegate2 = new NameComparator();
-
- public int compare(City c1, City c2) {
- int result = mDelegate1.compare(c1, c2);
-
- if (result == 0) {
- result = mDelegate2.compare(c1, c2);
- }
-
- return result;
- }
- }
-
- /**
- * Orders by:
- *
- * <ol>
- * <li>UTC offset of {@link #getTimeZone() timezone}</li>
- * </ol>
- */
- public static final class UtcOffsetIndexComparator implements Comparator<City> {
-
- // Snapshot the current time when the Comparator is created to obtain consistent offsets.
- private final long now = System.currentTimeMillis();
-
- public int compare(City c1, City c2) {
- final int utcOffset1 = c1.getTimeZone().getOffset(now);
- final int utcOffset2 = c2.getTimeZone().getOffset(now);
- return Integer.compare(utcOffset1, utcOffset2);
- }
- }
-
- /**
- * This comparator sorts using the city fields that influence natural name sort order:
- *
- * <ol>
- * <li>{@link #getIndex() numeric index}</li>
- * <li>{@link #getIndexString()} alphabetic index}</li>
- * <li>{@link #getPhoneticName() phonetic name}</li>
- * </ol>
- */
- public static final class NameComparator implements Comparator<City> {
-
- private final Comparator<City> mDelegate = new NameIndexComparator();
-
- // Locale-sensitive comparator for phonetic names.
- private final Collator mNameCollator = Collator.getInstance();
-
- @Override
- public int compare(City c1, City c2) {
- int result = mDelegate.compare(c1, c2);
-
- if (result == 0) {
- result = mNameCollator.compare(c1.getPhoneticName(), c2.getPhoneticName());
- }
-
- return result;
- }
- }
-
- /**
- * Orders by:
- *
- * <ol>
- * <li>{@link #getIndex() numeric index}</li>
- * <li>{@link #getIndexString()} alphabetic index}</li>
- * </ol>
- */
- public static final class NameIndexComparator implements Comparator<City> {
-
- // Locale-sensitive comparator for index strings.
- private final Collator mNameCollator = Collator.getInstance();
-
- @Override
- public int compare(City c1, City c2) {
- int result = Integer.compare(c1.getIndex(), c2.getIndex());
-
- if (result == 0) {
- result = mNameCollator.compare(c1.getIndexString(), c2.getIndexString());
- }
-
- return result;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/City.kt b/src/com/android/deskclock/data/City.kt
new file mode 100644
index 0000000..4a8abf3
--- /dev/null
+++ b/src/com/android/deskclock/data/City.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import java.text.Collator
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * A read-only domain object representing a city of the world and associated time information. It
+ * also contains static comparators that can be instantiated to order cities in common sort orders.
+ */
+class City internal constructor(
+ /** A unique identifier for the city. */
+ val id: String?,
+ /** An optional numeric index used to order cities for display; -1 if no such index exists. */
+ val index: Int,
+ /** An index string used to order cities for display. */
+ val indexString: String?,
+ /** The display name of the city. */
+ val name: String,
+ /** The phonetic name of the city used to order cities for display. */
+ val phoneticName: String,
+ /** The TimeZone corresponding to the city. */
+ val timeZone: TimeZone
+) {
+
+ /** A cached upper case form of the [.mName] used in case-insensitive name comparisons. */
+ private var mNameUpperCase: String? = null
+
+ /**
+ * A cached upper case form of the [.mName] used in case-insensitive name comparisons
+ * which ignore [.removeSpecialCharacters] special characters.
+ */
+ private var mNameUpperCaseNoSpecialCharacters: String? = null
+
+ /**
+ * @return the city name converted to upper case
+ */
+ val nameUpperCase: String
+ get() {
+ if (mNameUpperCase == null) {
+ mNameUpperCase = name.toUpperCase()
+ }
+ return mNameUpperCase!!
+ }
+
+ /**
+ * @return the city name converted to upper case with all special characters removed
+ */
+ private val nameUpperCaseNoSpecialCharacters: String
+ get() {
+ if (mNameUpperCaseNoSpecialCharacters == null) {
+ mNameUpperCaseNoSpecialCharacters = removeSpecialCharacters(nameUpperCase)
+ }
+ return mNameUpperCaseNoSpecialCharacters!!
+ }
+
+ /**
+ * @param upperCaseQueryNoSpecialCharacters search term with all special characters removed
+ * to match against the upper case city name
+ * @return `true` iff the name of this city starts with the given query
+ */
+ fun matches(upperCaseQueryNoSpecialCharacters: String): Boolean {
+ // By removing all special characters, prefix matching becomes more liberal and it is easier
+ // to locate the desired city. e.g. "St. Lucia" is matched by "StL", "St.L", "St L", "St. L"
+ return nameUpperCaseNoSpecialCharacters.startsWith(upperCaseQueryNoSpecialCharacters)
+ }
+
+ override fun toString(): String {
+ return String.format(Locale.US,
+ "City {id=%s, index=%d, indexString=%s, name=%s, phonetic=%s, tz=%s}",
+ id, index, indexString, name, phoneticName, timeZone.id)
+ }
+
+ /**
+ * Orders by:
+ *
+ * 1. UTC offset of [timezone][.getTimeZone]
+ * 1. [numeric index][.getIndex]
+ * 1. [.getIndexString] alphabetic index}
+ * 1. [phonetic name][.getPhoneticName]
+ */
+ class UtcOffsetComparator : Comparator<City> {
+ private val mDelegate1: Comparator<City> = UtcOffsetIndexComparator()
+ private val mDelegate2: Comparator<City> = NameComparator()
+
+ override fun compare(c1: City, c2: City): Int {
+ var result = mDelegate1.compare(c1, c2)
+
+ if (result == 0) {
+ result = mDelegate2.compare(c1, c2)
+ }
+
+ return result
+ }
+ }
+
+ /**
+ * Orders by:
+ *
+ * 1. UTC offset of [timezone][.getTimeZone]
+ */
+ class UtcOffsetIndexComparator : Comparator<City> {
+ // Snapshot the current time when the Comparator is created to obtain consistent offsets.
+ private val now = System.currentTimeMillis()
+
+ override fun compare(c1: City, c2: City): Int {
+ val utcOffset1 = c1.timeZone.getOffset(now)
+ val utcOffset2 = c2.timeZone.getOffset(now)
+ return utcOffset1.compareTo(utcOffset2)
+ }
+ }
+
+ /**
+ * This comparator sorts using the city fields that influence natural name sort order:
+ *
+ * 1. [numeric index][.getIndex]
+ * 1. [.getIndexString] alphabetic index}
+ * 1. [phonetic name][.getPhoneticName]
+ */
+ class NameComparator : Comparator<City> {
+ private val mDelegate: Comparator<City> = NameIndexComparator()
+
+ // Locale-sensitive comparator for phonetic names.
+ private val mNameCollator = Collator.getInstance()
+
+ override fun compare(c1: City, c2: City): Int {
+ var result = mDelegate.compare(c1, c2)
+
+ if (result == 0) {
+ result = mNameCollator.compare(c1.phoneticName, c2.phoneticName)
+ }
+
+ return result
+ }
+ }
+
+ /**
+ * Orders by:
+ *
+ * 1. [numeric index][.getIndex]
+ * 1. [.getIndexString] alphabetic index}
+ */
+ class NameIndexComparator : Comparator<City> {
+ // Locale-sensitive comparator for index strings.
+ private val mNameCollator = Collator.getInstance()
+
+ override fun compare(c1: City, c2: City): Int {
+ var result = c1.index.compareTo(c2.index)
+
+ if (result == 0) {
+ result = mNameCollator.compare(c1.indexString, c2.indexString)
+ }
+
+ return result
+ }
+ }
+
+ companion object {
+ /**
+ * Strips out any characters considered optional for matching purposes. These include spaces,
+ * dashes, periods and apostrophes.
+ *
+ * @param token a city name or search term
+ * @return the given `token` without any characters considered optional when matching
+ */
+ @JvmStatic
+ fun removeSpecialCharacters(token: String): String {
+ return token.replace("[ -.']".toRegex(), "")
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityDAO.java b/src/com/android/deskclock/data/CityDAO.java
deleted file mode 100644
index c75298a..0000000
--- a/src/com/android/deskclock/data/CityDAO.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import androidx.annotation.VisibleForTesting;
-import android.text.TextUtils;
-import android.util.ArrayMap;
-
-import com.android.deskclock.R;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * This class encapsulates the transfer of data between {@link City} domain objects and their
- * permanent storage in {@link Resources} and {@link SharedPreferences}.
- */
-final class CityDAO {
-
- /** Regex to match numeric index values when parsing city names. */
- private static final Pattern NUMERIC_INDEX_REGEX = Pattern.compile("\\d+");
-
- /** Key to a preference that stores the number of selected cities. */
- private static final String NUMBER_OF_CITIES = "number_of_cities";
-
- /** Prefix for a key to a preference that stores the id of a selected city. */
- private static final String CITY_ID = "city_id_";
-
- private CityDAO() {}
-
- /**
- * @param cityMap maps city ids to city instances
- * @return the list of city ids selected for display by the user
- */
- static List<City> getSelectedCities(SharedPreferences prefs, Map<String, City> cityMap) {
- final int size = prefs.getInt(NUMBER_OF_CITIES, 0);
- final List<City> selectedCities = new ArrayList<>(size);
-
- for (int i = 0; i < size; i++) {
- final String id = prefs.getString(CITY_ID + i, null);
- final City city = cityMap.get(id);
- if (city != null) {
- selectedCities.add(city);
- }
- }
-
- return selectedCities;
- }
-
- /**
- * @param cities the collection of cities selected for display by the user
- */
- static void setSelectedCities(SharedPreferences prefs, Collection<City> cities) {
- final SharedPreferences.Editor editor = prefs.edit();
- editor.putInt(NUMBER_OF_CITIES, cities.size());
-
- int count = 0;
- for (City city : cities) {
- editor.putString(CITY_ID + count, city.getId());
- count++;
- }
-
- editor.apply();
- }
-
- /**
- * @return the domain of cities from which the user may choose a world clock
- */
- static Map<String, City> getCities(Context context) {
- final Resources resources = context.getResources();
- final TypedArray cityStrings = resources.obtainTypedArray(R.array.city_ids);
- final int citiesCount = cityStrings.length();
-
- final Map<String, City> cities = new ArrayMap<>(citiesCount);
- try {
- for (int i = 0; i < citiesCount; ++i) {
- // Attempt to locate the resource id defining the city as a string.
- final int cityResourceId = cityStrings.getResourceId(i, 0);
- if (cityResourceId == 0) {
- final String message = String.format(Locale.ENGLISH,
- "Unable to locate city resource id for index %d", i);
- throw new IllegalStateException(message);
- }
-
- final String id = resources.getResourceEntryName(cityResourceId);
- final String cityString = cityStrings.getString(i);
- if (cityString == null) {
- final String message = String.format("Unable to locate city with id %s", id);
- throw new IllegalStateException(message);
- }
-
- // Attempt to parse the time zone from the city entry.
- final String[] cityParts = cityString.split("[|]");
- if (cityParts.length != 2) {
- final String message = String.format(
- "Error parsing malformed city %s", cityString);
- throw new IllegalStateException(message);
- }
-
- final City city = createCity(id, cityParts[0], cityParts[1]);
- // Skip cities whose timezone cannot be resolved.
- if (city != null) {
- cities.put(id, city);
- }
- }
- } finally {
- cityStrings.recycle();
- }
-
- return Collections.unmodifiableMap(cities);
- }
-
- /**
- * @param id unique identifier for city
- * @param formattedName "[index string]=[name]" or "[index string]=[name]:[phonetic name]",
- * If [index string] is empty, use the first character of name as index,
- * If phonetic name is empty, use the name itself as phonetic name.
- * @param tzId the string id of the timezone a given city is located in
- */
- @VisibleForTesting
- static City createCity(String id, String formattedName, String tzId) {
- final TimeZone tz = TimeZone.getTimeZone(tzId);
- // If the time zone lookup fails, GMT is returned. No cities actually map to GMT.
- if ("GMT".equals(tz.getID())) {
- return null;
- }
-
- final String[] parts = formattedName.split("[=:]");
- final String name = parts[1];
- // Extract index string from input, use the first character of city name as the index string
- // if one is not explicitly provided.
- final String indexString = TextUtils.isEmpty(parts[0])
- ? name.substring(0, 1) : parts[0];
- final String phoneticName = parts.length == 3 ? parts[2] : name;
-
- final Matcher matcher = NUMERIC_INDEX_REGEX.matcher(indexString);
- final int index = matcher.find() ? Integer.parseInt(matcher.group()) : -1;
-
- return new City(id, index, indexString, name, phoneticName, tz);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityDAO.kt b/src/com/android/deskclock/data/CityDAO.kt
new file mode 100644
index 0000000..4f38b73
--- /dev/null
+++ b/src/com/android/deskclock/data/CityDAO.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Resources
+import android.content.res.TypedArray
+import android.text.TextUtils
+import android.util.ArrayMap
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.R
+
+import java.util.Locale
+import java.util.regex.Pattern
+import java.util.TimeZone
+
+/**
+ * This class encapsulates the transfer of data between [City] domain objects and their
+ * permanent storage in [Resources] and [SharedPreferences].
+ */
+internal object CityDAO {
+ /** Regex to match numeric index values when parsing city names. */
+ private val NUMERIC_INDEX_REGEX = Pattern.compile("\\d+")
+
+ /** Key to a preference that stores the number of selected cities. */
+ private const val NUMBER_OF_CITIES = "number_of_cities"
+
+ /** Prefix for a key to a preference that stores the id of a selected city. */
+ private const val CITY_ID = "city_id_"
+
+ /**
+ * @param cityMap maps city ids to city instances
+ * @return the list of city ids selected for display by the user
+ */
+ fun getSelectedCities(prefs: SharedPreferences, cityMap: Map<String, City>): List<City> {
+ val size: Int = prefs.getInt(NUMBER_OF_CITIES, 0)
+ val selectedCities: MutableList<City> = ArrayList(size)
+
+ for (i in 0 until size) {
+ val id: String? = prefs.getString(CITY_ID + i, null)
+ val city = cityMap[id]
+ if (city != null) {
+ selectedCities.add(city)
+ }
+ }
+
+ return selectedCities
+ }
+
+ /**
+ * @param cities the collection of cities selected for display by the user
+ */
+ fun setSelectedCities(prefs: SharedPreferences, cities: Collection<City>) {
+ val editor: SharedPreferences.Editor = prefs.edit()
+ editor.putInt(NUMBER_OF_CITIES, cities.size)
+
+ for ((count, city) in cities.withIndex()) {
+ editor.putString(CITY_ID + count, city.id)
+ }
+
+ editor.apply()
+ }
+
+ /**
+ * @return the domain of cities from which the user may choose a world clock
+ */
+ fun getCities(context: Context): Map<String, City> {
+ val resources: Resources = context.getResources()
+ val cityStrings: TypedArray = resources.obtainTypedArray(R.array.city_ids)
+ val citiesCount: Int = cityStrings.length()
+
+ val cities: MutableMap<String, City> = ArrayMap(citiesCount)
+ try {
+ for (i in 0 until citiesCount) {
+ // Attempt to locate the resource id defining the city as a string.
+ val cityResourceId: Int = cityStrings.getResourceId(i, 0)
+ if (cityResourceId == 0) {
+ val message = String.format(Locale.ENGLISH,
+ "Unable to locate city resource id for index %d", i)
+ throw IllegalStateException(message)
+ }
+
+ val id: String = resources.getResourceEntryName(cityResourceId)
+ val cityString: String? = cityStrings.getString(i)
+ if (cityString == null) {
+ val message = String.format("Unable to locate city with id %s", id)
+ throw IllegalStateException(message)
+ }
+
+ // Attempt to parse the time zone from the city entry.
+ val cityParts = cityString.split("[|]".toRegex()).toTypedArray()
+ if (cityParts.size != 2) {
+ val message = String.format(
+ "Error parsing malformed city %s", cityString)
+ throw IllegalStateException(message)
+ }
+
+ val city = createCity(id, cityParts[0], cityParts[1])
+ // Skip cities whose timezone cannot be resolved.
+ if (city != null) {
+ cities[id] = city
+ }
+ }
+ } finally {
+ cityStrings.recycle()
+ }
+
+ return cities
+ }
+
+ /**
+ * @param id unique identifier for city
+ * @param formattedName "[index string]=[name]" or "[index string]=[name]:[phonetic name]",
+ * If [index string] is empty, use the first character of name as index,
+ * If phonetic name is empty, use the name itself as phonetic name.
+ * @param tzId the string id of the timezone a given city is located in
+ */
+ @VisibleForTesting
+ fun createCity(id: String?, formattedName: String, tzId: String?): City? {
+ val tz = TimeZone.getTimeZone(tzId)
+ // If the time zone lookup fails, GMT is returned. No cities actually map to GMT.
+ if ("GMT" == tz.id) {
+ return null
+ }
+
+ val parts = formattedName.split("[=:]".toRegex()).toTypedArray()
+ val name = parts[1]
+ // Extract index string from input, use the first character of city name as the index string
+ // if one is not explicitly provided.
+ val indexString = if (TextUtils.isEmpty(parts[0])) name.substring(0, 1) else parts[0]
+ val phoneticName = if (parts.size == 3) parts[2] else name
+
+ val matcher = NUMERIC_INDEX_REGEX.matcher(indexString)
+ val index = if (matcher.find()) matcher.group().toInt() else -1
+
+ return City(id, index, indexString, name, phoneticName, tz)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityListener.java b/src/com/android/deskclock/data/CityListener.kt
similarity index 75%
rename from src/com/android/deskclock/data/CityListener.java
rename to src/com/android/deskclock/data/CityListener.kt
index 91f66b3..da6f089 100644
--- a/src/com/android/deskclock/data/CityListener.java
+++ b/src/com/android/deskclock/data/CityListener.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,11 @@
* limitations under the License.
*/
-package com.android.deskclock.data;
-
-import java.util.List;
+package com.android.deskclock.data
/**
* The interface through which interested parties are notified of changes to the world cities list.
*/
-public interface CityListener {
- void citiesChanged(List<City> oldCities, List<City> newCities);
+interface CityListener {
+ fun citiesChanged(oldCities: List<City>, newCities: List<City>)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityModel.java b/src/com/android/deskclock/data/CityModel.java
deleted file mode 100644
index 9f45875..0000000
--- a/src/com/android/deskclock/data/CityModel.java
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel.CitySort;
-import com.android.deskclock.settings.SettingsActivity;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimeZone;
-
-/**
- * All {@link City} data is accessed via this model.
- */
-final class CityModel {
-
- private final Context mContext;
-
- private final SharedPreferences mPrefs;
-
- /** The model from which settings are fetched. */
- private final SettingsModel mSettingsModel;
-
- /**
- * Retain a hard reference to the shared preference observer to prevent it from being garbage
- * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
- */
- @SuppressWarnings("FieldCanBeLocal")
- private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
-
- /** Clears data structures containing data that is locale-sensitive. */
- @SuppressWarnings("FieldCanBeLocal")
- private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
- /** List of listeners to invoke upon world city list change */
- private final List<CityListener> mCityListeners = new ArrayList<>();
-
- /** Maps city ID to city instance. */
- private Map<String, City> mCityMap;
-
- /** List of city instances in display order. */
- private List<City> mAllCities;
-
- /** List of selected city instances in display order. */
- private List<City> mSelectedCities;
-
- /** List of unselected city instances in display order. */
- private List<City> mUnselectedCities;
-
- /** A city instance representing the home timezone of the user. */
- private City mHomeCity;
-
- CityModel(Context context, SharedPreferences prefs, SettingsModel settingsModel) {
- mContext = context;
- mPrefs = prefs;
- mSettingsModel = settingsModel;
-
- // Clear caches affected by locale when locale changes.
- final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
- mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
-
- // Clear caches affected by preferences when preferences change.
- prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
- }
-
- void addCityListener(CityListener cityListener) {
- mCityListeners.add(cityListener);
- }
-
- void removeCityListener(CityListener cityListener) {
- mCityListeners.remove(cityListener);
- }
-
- /**
- * @return a list of all cities in their display order
- */
- List<City> getAllCities() {
- if (mAllCities == null) {
- // Create a set of selections to identify the unselected cities.
- final List<City> selected = new ArrayList<>(getSelectedCities());
-
- // Sort the selected cities alphabetically by name.
- Collections.sort(selected, new City.NameComparator());
-
- // Combine selected and unselected cities into a single list.
- final List<City> allCities = new ArrayList<>(getCityMap().size());
- allCities.addAll(selected);
- allCities.addAll(getUnselectedCities());
- mAllCities = Collections.unmodifiableList(allCities);
- }
-
- return mAllCities;
- }
-
- /**
- * @return a city representing the user's home timezone
- */
- City getHomeCity() {
- if (mHomeCity == null) {
- final String name = mContext.getString(R.string.home_label);
- final TimeZone timeZone = mSettingsModel.getHomeTimeZone();
- mHomeCity = new City(null, -1, null, name, name, timeZone);
- }
-
- return mHomeCity;
- }
-
- /**
- * @return a list of cities not selected for display
- */
- List<City> getUnselectedCities() {
- if (mUnselectedCities == null) {
- // Create a set of selections to identify the unselected cities.
- final List<City> selected = new ArrayList<>(getSelectedCities());
- final Set<City> selectedSet = Utils.newArraySet(selected);
-
- final Collection<City> all = getCityMap().values();
- final List<City> unselected = new ArrayList<>(all.size() - selectedSet.size());
- for (City city : all) {
- if (!selectedSet.contains(city)) {
- unselected.add(city);
- }
- }
-
- // Sort the unselected cities according by the user's preferred sort.
- Collections.sort(unselected, getCitySortComparator());
- mUnselectedCities = Collections.unmodifiableList(unselected);
- }
-
- return mUnselectedCities;
- }
-
- /**
- * @return a list of cities selected for display
- */
- List<City> getSelectedCities() {
- if (mSelectedCities == null) {
- final List<City> selectedCities = CityDAO.getSelectedCities(mPrefs, getCityMap());
- Collections.sort(selectedCities, new City.UtcOffsetComparator());
- mSelectedCities = Collections.unmodifiableList(selectedCities);
- }
-
- return mSelectedCities;
- }
-
- /**
- * @param cities the new collection of cities selected for display by the user
- */
- void setSelectedCities(Collection<City> cities) {
- final List<City> oldCities = getAllCities();
- CityDAO.setSelectedCities(mPrefs, cities);
-
- // Clear caches affected by this update.
- mAllCities = null;
- mSelectedCities = null;
- mUnselectedCities = null;
-
- // Broadcast the change to the selected cities for the benefit of widgets.
- fireCitiesChanged(oldCities, getAllCities());
- }
-
- /**
- * @return a comparator used to locate index positions
- */
- Comparator<City> getCityIndexComparator() {
- final CitySort citySort = mSettingsModel.getCitySort();
- switch (citySort) {
- case NAME: return new City.NameIndexComparator();
- case UTC_OFFSET: return new City.UtcOffsetIndexComparator();
- }
- throw new IllegalStateException("unexpected city sort: " + citySort);
- }
-
- /**
- * @return the order in which cities are sorted
- */
- CitySort getCitySort() {
- return mSettingsModel.getCitySort();
- }
-
- /**
- * Adjust the order in which cities are sorted.
- */
- void toggleCitySort() {
- mSettingsModel.toggleCitySort();
-
- // Clear caches affected by this update.
- mAllCities = null;
- mUnselectedCities = null;
- }
-
- private Map<String, City> getCityMap() {
- if (mCityMap == null) {
- mCityMap = CityDAO.getCities(mContext);
- }
-
- return mCityMap;
- }
-
- private Comparator<City> getCitySortComparator() {
- final CitySort citySort = mSettingsModel.getCitySort();
- switch (citySort) {
- case NAME: return new City.NameComparator();
- case UTC_OFFSET: return new City.UtcOffsetComparator();
- }
- throw new IllegalStateException("unexpected city sort: " + citySort);
- }
-
- private void fireCitiesChanged(List<City> oldCities, List<City> newCities) {
- mContext.sendBroadcast(new Intent(DataModel.ACTION_WORLD_CITIES_CHANGED));
- for (CityListener cityListener : mCityListeners) {
- cityListener.citiesChanged(oldCities, newCities);
- }
- }
-
- /**
- * Cached information that is locale-sensitive must be cleared in response to locale changes.
- */
- private final class LocaleChangedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- mCityMap = null;
- mHomeCity = null;
- mAllCities = null;
- mSelectedCities = null;
- mUnselectedCities = null;
- }
- }
-
- /**
- * This receiver is notified when shared preferences change. Cached information built on
- * preferences must be cleared.
- */
- private final class PreferenceListener implements OnSharedPreferenceChangeListener {
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- switch (key) {
- case SettingsActivity.KEY_HOME_TZ:
- mHomeCity = null;
- case SettingsActivity.KEY_AUTO_HOME_CLOCK:
- final List<City> cities = getAllCities();
- fireCitiesChanged(cities, cities);
- break;
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/data/CityModel.kt b/src/com/android/deskclock/data/CityModel.kt
new file mode 100644
index 0000000..258f179
--- /dev/null
+++ b/src/com/android/deskclock/data/CityModel.kt
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.City.NameComparator
+import com.android.deskclock.data.City.NameIndexComparator
+import com.android.deskclock.data.City.UtcOffsetComparator
+import com.android.deskclock.data.City.UtcOffsetIndexComparator
+import com.android.deskclock.data.DataModel.CitySort
+import com.android.deskclock.settings.SettingsActivity
+
+import java.util.Collections
+
+/**
+ * All [City] data is accessed via this model.
+ */
+internal class CityModel(
+ private val context: Context,
+ private val prefs: SharedPreferences,
+ /** The model from which settings are fetched. */
+ private val settingsModel: SettingsModel
+) {
+
+ /**
+ * Retain a hard reference to the shared preference observer to prevent it from being garbage
+ * collected. See [SharedPreferences.registerOnSharedPreferenceChangeListener] for detail.
+ */
+ private val mPreferenceListener: OnSharedPreferenceChangeListener = PreferenceListener()
+
+ /** Clears data structures containing data that is locale-sensitive. */
+ private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+ /** List of listeners to invoke upon world city list change */
+ private val mCityListeners: MutableList<CityListener> = ArrayList()
+
+ /** Maps city ID to city instance. */
+ private var mCityMap: Map<String, City>? = null
+
+ /** List of city instances in display order. */
+ private var mAllCities: List<City>? = null
+
+ /** List of selected city instances in display order. */
+ private var mSelectedCities: List<City>? = null
+
+ /** List of unselected city instances in display order. */
+ private var mUnselectedCities: List<City>? = null
+
+ /** A city instance representing the home timezone of the user. */
+ private var mHomeCity: City? = null
+
+ init {
+ // Clear caches affected by locale when locale changes.
+ val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+ context.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+
+ // Clear caches affected by preferences when preferences change.
+ prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener)
+ }
+
+ fun addCityListener(cityListener: CityListener) {
+ mCityListeners.add(cityListener)
+ }
+
+ fun removeCityListener(cityListener: CityListener) {
+ mCityListeners.remove(cityListener)
+ }
+
+ /**
+ * @return a list of all cities in their display order
+ */
+ val allCities: List<City>
+ get() {
+ if (mAllCities == null) {
+ // Create a set of selections to identify the unselected cities.
+ val selected: List<City> = selectedCities.toMutableList()
+
+ // Sort the selected cities alphabetically by name.
+ Collections.sort(selected, NameComparator())
+
+ // Combine selected and unselected cities into a single list.
+ val allCities: MutableList<City> = ArrayList(cityMap.size)
+ allCities.addAll(selected)
+ allCities.addAll(unselectedCities)
+ mAllCities = allCities
+ }
+
+ return mAllCities!!
+ }
+
+ /**
+ * @return a city representing the user's home timezone
+ */
+ val homeCity: City
+ get() {
+ if (mHomeCity == null) {
+ val name: String = context.getString(R.string.home_label)
+ val timeZone = settingsModel.homeTimeZone
+ mHomeCity = City(null, -1, null, name, name, timeZone)
+ }
+
+ return mHomeCity!!
+ }
+
+ /**
+ * @return a list of cities not selected for display
+ */
+ val unselectedCities: List<City>
+ get() {
+ if (mUnselectedCities == null) {
+ // Create a set of selections to identify the unselected cities.
+ val selected: List<City> = selectedCities.toMutableList()
+ val selectedSet: Set<City> = Utils.newArraySet(selected)
+
+ val all = cityMap.values
+ val unselected: MutableList<City> = ArrayList(all.size - selectedSet.size)
+ for (city in all) {
+ if (!selectedSet.contains(city)) {
+ unselected.add(city)
+ }
+ }
+
+ // Sort the unselected cities according by the user's preferred sort.
+ Collections.sort(unselected, citySortComparator)
+ mUnselectedCities = unselected
+ }
+
+ return mUnselectedCities!!
+ }
+
+ /**
+ * @return a list of cities selected for display
+ */
+ val selectedCities: List<City>
+ get() {
+ if (mSelectedCities == null) {
+ val selectedCities = CityDAO.getSelectedCities(prefs, cityMap)
+ Collections.sort(selectedCities, UtcOffsetComparator())
+ mSelectedCities = selectedCities
+ }
+
+ return mSelectedCities!!
+ }
+
+ /**
+ * @param cities the new collection of cities selected for display by the user
+ */
+ fun setSelectedCities(cities: Collection<City>) {
+ val oldCities = allCities
+ CityDAO.setSelectedCities(prefs, cities)
+
+ // Clear caches affected by this update.
+ mAllCities = null
+ mSelectedCities = null
+ mUnselectedCities = null
+
+ // Broadcast the change to the selected cities for the benefit of widgets.
+ fireCitiesChanged(oldCities, allCities)
+ }
+
+ /**
+ * @return a comparator used to locate index positions
+ */
+ val cityIndexComparator: Comparator<City>
+ get() = when (settingsModel.citySort) {
+ CitySort.NAME -> NameIndexComparator()
+ CitySort.UTC_OFFSET -> UtcOffsetIndexComparator()
+ }
+
+ /**
+ * @return the order in which cities are sorted
+ */
+ val citySort: CitySort
+ get() = settingsModel.citySort
+
+ /**
+ * Adjust the order in which cities are sorted.
+ */
+ fun toggleCitySort() {
+ settingsModel.toggleCitySort()
+
+ // Clear caches affected by this update.
+ mAllCities = null
+ mUnselectedCities = null
+ }
+
+ private val cityMap: Map<String, City>
+ get() {
+ if (mCityMap == null) {
+ mCityMap = CityDAO.getCities(context)
+ }
+
+ return mCityMap!!
+ }
+
+ private val citySortComparator: Comparator<City>
+ get() = when (settingsModel.citySort) {
+ CitySort.NAME -> NameComparator()
+ CitySort.UTC_OFFSET -> UtcOffsetComparator()
+ }
+
+ private fun fireCitiesChanged(oldCities: List<City>, newCities: List<City>) {
+ context.sendBroadcast(Intent(DataModel.ACTION_WORLD_CITIES_CHANGED))
+ for (cityListener in mCityListeners) {
+ cityListener.citiesChanged(oldCities, newCities)
+ }
+ }
+
+ /**
+ * Cached information that is locale-sensitive must be cleared in response to locale changes.
+ */
+ private inner class LocaleChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ mCityMap = null
+ mHomeCity = null
+ mAllCities = null
+ mSelectedCities = null
+ mUnselectedCities = null
+ }
+ }
+
+ /**
+ * This receiver is notified when shared preferences change. Cached information built on
+ * preferences must be cleared.
+ */
+ private inner class PreferenceListener : OnSharedPreferenceChangeListener {
+ override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
+ when (key) {
+ SettingsActivity.KEY_HOME_TZ -> {
+ mHomeCity = null
+ val cities = allCities
+ fireCitiesChanged(cities, cities)
+ }
+ SettingsActivity.KEY_AUTO_HOME_CLOCK -> {
+ val cities = allCities
+ fireCitiesChanged(cities, cities)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CustomRingtone.java b/src/com/android/deskclock/data/CustomRingtone.java
deleted file mode 100644
index d243027..0000000
--- a/src/com/android/deskclock/data/CustomRingtone.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.net.Uri;
-import androidx.annotation.NonNull;
-
-/**
- * A read-only domain object representing a custom ringtone chosen from the file system.
- */
-public final class CustomRingtone implements Comparable<CustomRingtone> {
-
- /** The unique identifier of the custom ringtone. */
- private final long mId;
-
- /** The uri that allows playback of the ringtone. */
- private final Uri mUri;
-
- /** The title describing the file at the given uri; typically the file name. */
- private final String mTitle;
-
- /** {@code true} iff the application has permission to read the content of {@code mUri uri}. */
- private final boolean mHasPermissions;
-
- CustomRingtone(long id, Uri uri, String title, boolean hasPermissions) {
- mId = id;
- mUri = uri;
- mTitle = title;
- mHasPermissions = hasPermissions;
- }
-
- public long getId() { return mId; }
- public Uri getUri() { return mUri; }
- public String getTitle() { return mTitle; }
- public boolean hasPermissions() { return mHasPermissions; }
-
- CustomRingtone setHasPermissions(boolean hasPermissions) {
- if (mHasPermissions == hasPermissions) {
- return this;
- }
-
- return new CustomRingtone(mId, mUri, mTitle, hasPermissions);
- }
-
- @Override
- public int compareTo(@NonNull CustomRingtone other) {
- return String.CASE_INSENSITIVE_ORDER.compare(getTitle(), other.getTitle());
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CustomRingtone.kt b/src/com/android/deskclock/data/CustomRingtone.kt
new file mode 100644
index 0000000..db4bcfd
--- /dev/null
+++ b/src/com/android/deskclock/data/CustomRingtone.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.net.Uri
+
+/**
+ * A read-only domain object representing a custom ringtone chosen from the file system.
+ */
+class CustomRingtone internal constructor(
+ /** The unique identifier of the custom ringtone. */
+ val id: Long,
+ /** The uri that allows playback of the ringtone. */
+ private val mUri: Uri,
+ /** The title describing the file at the given uri; typically the file name. */
+ val title: String?,
+ /** `true` iff the application has permission to read the content of `mUri uri`. */
+ private val mHasPermissions: Boolean
+) : Comparable<CustomRingtone> {
+
+ val uri: Uri
+ get() = mUri
+
+ fun hasPermissions(): Boolean = mHasPermissions
+
+ fun setHasPermissions(hasPermissions: Boolean): CustomRingtone =
+ if (mHasPermissions == hasPermissions) {
+ this
+ } else {
+ CustomRingtone(id, mUri, title, hasPermissions)
+ }
+
+ override fun compareTo(other: CustomRingtone): Int {
+ return String.CASE_INSENSITIVE_ORDER.compare(title, other.title)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CustomRingtoneDAO.java b/src/com/android/deskclock/data/CustomRingtoneDAO.java
deleted file mode 100644
index 827acb5..0000000
--- a/src/com/android/deskclock/data/CustomRingtoneDAO.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.SharedPreferences;
-import android.net.Uri;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * This class encapsulates the transfer of data between {@link CustomRingtone} domain objects and
- * their permanent storage in {@link SharedPreferences}.
- */
-final class CustomRingtoneDAO {
-
- /** Key to a preference that stores the set of all custom ringtone ids. */
- private static final String RINGTONE_IDS = "ringtone_ids";
-
- /** Key to a preference that stores the next unused ringtone id. */
- private static final String NEXT_RINGTONE_ID = "next_ringtone_id";
-
- /** Prefix for a key to a preference that stores the URI associated with the ringtone id. */
- private static final String RINGTONE_URI = "ringtone_uri_";
-
- /** Prefix for a key to a preference that stores the title associated with the ringtone id. */
- private static final String RINGTONE_TITLE = "ringtone_title_";
-
- private CustomRingtoneDAO() {}
-
- /**
- * @param uri points to an audio file located on the file system
- * @param title the title of the audio content at the given {@code uri}
- * @return the newly added custom ringtone
- */
- static CustomRingtone addCustomRingtone(SharedPreferences prefs, Uri uri, String title) {
- final long id = prefs.getLong(NEXT_RINGTONE_ID, 0);
- final Set<String> ids = getRingtoneIds(prefs);
- ids.add(String.valueOf(id));
-
- prefs.edit()
- .putString(RINGTONE_URI + id, uri.toString())
- .putString(RINGTONE_TITLE + id, title)
- .putLong(NEXT_RINGTONE_ID, id + 1)
- .putStringSet(RINGTONE_IDS, ids)
- .apply();
-
- return new CustomRingtone(id, uri, title, true);
- }
-
- /**
- * @param id identifies the ringtone to be removed
- */
- static void removeCustomRingtone(SharedPreferences prefs, long id) {
- final Set<String> ids = getRingtoneIds(prefs);
- ids.remove(String.valueOf(id));
-
- final SharedPreferences.Editor editor = prefs.edit();
- editor.remove(RINGTONE_URI + id);
- editor.remove(RINGTONE_TITLE + id);
- if (ids.isEmpty()) {
- editor.remove(RINGTONE_IDS);
- editor.remove(NEXT_RINGTONE_ID);
- } else {
- editor.putStringSet(RINGTONE_IDS, ids);
- }
- editor.apply();
- }
-
- /**
- * @return a list of all known custom ringtones
- */
- static List<CustomRingtone> getCustomRingtones(SharedPreferences prefs) {
- final Set<String> ids = prefs.getStringSet(RINGTONE_IDS, Collections.<String>emptySet());
- final List<CustomRingtone> ringtones = new ArrayList<>(ids.size());
-
- for (String id : ids) {
- final long idLong = Long.parseLong(id);
- final Uri uri = Uri.parse(prefs.getString(RINGTONE_URI + id, null));
- final String title = prefs.getString(RINGTONE_TITLE + id, null);
- ringtones.add(new CustomRingtone(idLong, uri, title, true));
- }
-
- return ringtones;
- }
-
- private static Set<String> getRingtoneIds(SharedPreferences prefs) {
- return new HashSet<>(prefs.getStringSet(RINGTONE_IDS, Collections.<String>emptySet()));
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CustomRingtoneDAO.kt b/src/com/android/deskclock/data/CustomRingtoneDAO.kt
new file mode 100644
index 0000000..dc9b11e
--- /dev/null
+++ b/src/com/android/deskclock/data/CustomRingtoneDAO.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.SharedPreferences
+import android.net.Uri
+
+/**
+ * This class encapsulates the transfer of data between [CustomRingtone] domain objects and
+ * their permanent storage in [SharedPreferences].
+ */
+internal object CustomRingtoneDAO {
+ /** Key to a preference that stores the set of all custom ringtone ids. */
+ private const val RINGTONE_IDS = "ringtone_ids"
+
+ /** Key to a preference that stores the next unused ringtone id. */
+ private const val NEXT_RINGTONE_ID = "next_ringtone_id"
+
+ /** Prefix for a key to a preference that stores the URI associated with the ringtone id. */
+ private const val RINGTONE_URI = "ringtone_uri_"
+
+ /** Prefix for a key to a preference that stores the title associated with the ringtone id. */
+ private const val RINGTONE_TITLE = "ringtone_title_"
+
+ /**
+ * @param uri points to an audio file located on the file system
+ * @param title the title of the audio content at the given `uri`
+ * @return the newly added custom ringtone
+ */
+ fun addCustomRingtone(prefs: SharedPreferences, uri: Uri, title: String?): CustomRingtone {
+ val id: Long = prefs.getLong(NEXT_RINGTONE_ID, 0)
+ val ids = getRingtoneIds(prefs)
+ ids.add(id.toString())
+
+ prefs.edit()
+ .putString(RINGTONE_URI + id, uri.toString())
+ .putString(RINGTONE_TITLE + id, title)
+ .putLong(NEXT_RINGTONE_ID, id + 1)
+ .putStringSet(RINGTONE_IDS, ids)
+ .apply()
+
+ return CustomRingtone(id, uri, title, true)
+ }
+
+ /**
+ * @param id identifies the ringtone to be removed
+ */
+ fun removeCustomRingtone(prefs: SharedPreferences, id: Long) {
+ val ids = getRingtoneIds(prefs)
+ ids.remove(id.toString())
+
+ val editor: SharedPreferences.Editor = prefs.edit()
+ editor.apply {
+ remove(RINGTONE_URI + id)
+ remove(RINGTONE_TITLE + id)
+ if (ids.isEmpty()) {
+ remove(RINGTONE_IDS)
+ remove(NEXT_RINGTONE_ID)
+ } else {
+ putStringSet(RINGTONE_IDS, ids)
+ }
+ apply()
+ }
+ }
+
+ /**
+ * @return a list of all known custom ringtones
+ */
+ fun getCustomRingtones(prefs: SharedPreferences): MutableList<CustomRingtone> {
+ val ids: Set<String> = prefs.getStringSet(RINGTONE_IDS, emptySet<String>())!!
+ val ringtones: MutableList<CustomRingtone> = ArrayList(ids.size)
+
+ for (id in ids) {
+ val idLong = id.toLong()
+ val uri: Uri = Uri.parse(prefs.getString(RINGTONE_URI + id, null))
+ val title: String? = prefs.getString(RINGTONE_TITLE + id, null)
+ ringtones.add(CustomRingtone(idLong, uri, title, true))
+ }
+
+ return ringtones
+ }
+
+ private fun getRingtoneIds(prefs: SharedPreferences): MutableSet<String> {
+ return prefs.getStringSet(RINGTONE_IDS, mutableSetOf<String>())!!
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/DataModel.java b/src/com/android/deskclock/data/DataModel.java
deleted file mode 100644
index 1b43232..0000000
--- a/src/com/android/deskclock/data/DataModel.java
+++ /dev/null
@@ -1,1076 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.media.AudioManager;
-import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
-import androidx.annotation.StringRes;
-import android.view.View;
-
-import com.android.deskclock.Predicate;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.timer.TimerService;
-
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.List;
-
-import static android.content.Context.AUDIO_SERVICE;
-import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.media.AudioManager.FLAG_SHOW_UI;
-import static android.media.AudioManager.STREAM_ALARM;
-import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS;
-import static android.provider.Settings.ACTION_SOUND_SETTINGS;
-import static com.android.deskclock.Utils.enforceMainLooper;
-import static com.android.deskclock.Utils.enforceNotMainLooper;
-
-/**
- * All application-wide data is accessible through this singleton.
- */
-public final class DataModel {
-
- /** Indicates the display style of clocks. */
- public enum ClockStyle {ANALOG, DIGITAL}
-
- /** Indicates the preferred sort order of cities. */
- public enum CitySort {NAME, UTC_OFFSET}
-
- /** Indicates the preferred behavior of hardware volume buttons when firing alarms. */
- public enum AlarmVolumeButtonBehavior {NOTHING, SNOOZE, DISMISS}
-
- /** Indicates the reason alarms may not fire or may fire silently. */
- public enum SilentSetting {
- @SuppressWarnings("unchecked")
- DO_NOT_DISTURB(R.string.alarms_blocked_by_dnd, 0, Predicate.FALSE, null),
- @SuppressWarnings("unchecked")
- MUTED_VOLUME(R.string.alarm_volume_muted,
- R.string.unmute_alarm_volume,
- Predicate.TRUE,
- new UnmuteAlarmVolumeListener()),
- SILENT_RINGTONE(R.string.silent_default_alarm_ringtone,
- R.string.change_setting_action,
- new ChangeSoundActionPredicate(),
- new ChangeSoundSettingsListener()),
- @SuppressWarnings("unchecked")
- BLOCKED_NOTIFICATIONS(R.string.app_notifications_blocked,
- R.string.change_setting_action,
- Predicate.TRUE,
- new ChangeAppNotificationSettingsListener());
-
- private final @StringRes int mLabelResId;
- private final @StringRes int mActionResId;
- private final Predicate<Context> mActionEnabled;
- private final View.OnClickListener mActionListener;
-
- SilentSetting(int labelResId, int actionResId, Predicate<Context> actionEnabled,
- View.OnClickListener actionListener) {
- mLabelResId = labelResId;
- mActionResId = actionResId;
- mActionEnabled = actionEnabled;
- mActionListener = actionListener;
- }
-
- public @StringRes int getLabelResId() { return mLabelResId; }
- public @StringRes int getActionResId() { return mActionResId; }
- public View.OnClickListener getActionListener() { return mActionListener; }
- public boolean isActionEnabled(Context context) {
- return mLabelResId != 0 && mActionEnabled.apply(context);
- }
-
- private static class UnmuteAlarmVolumeListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- // Set the alarm volume to 11/16th of max and show the slider UI.
- // 11/16th of max is the initial volume of the alarm stream on a fresh install.
- final Context context = v.getContext();
- final AudioManager am = (AudioManager) context.getSystemService(AUDIO_SERVICE);
- final int index = Math.round(am.getStreamMaxVolume(STREAM_ALARM) * 11f / 16f);
- am.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI);
- }
- }
-
- private static class ChangeSoundSettingsListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- final Context context = v.getContext();
- context.startActivity(new Intent(ACTION_SOUND_SETTINGS)
- .addFlags(FLAG_ACTIVITY_NEW_TASK));
- }
- }
-
- private static class ChangeSoundActionPredicate implements Predicate<Context> {
- @Override
- public boolean apply(Context context) {
- final Intent intent = new Intent(ACTION_SOUND_SETTINGS);
- return intent.resolveActivity(context.getPackageManager()) != null;
- }
- }
-
- private static class ChangeAppNotificationSettingsListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- final Context context = v.getContext();
- if (Utils.isLOrLater()) {
- try {
- // Attempt to open the notification settings for this app.
- context.startActivity(
- new Intent("android.settings.APP_NOTIFICATION_SETTINGS")
- .putExtra("app_package", context.getPackageName())
- .putExtra("app_uid", context.getApplicationInfo().uid)
- .addFlags(FLAG_ACTIVITY_NEW_TASK));
- return;
- } catch (Exception ignored) {
- // best attempt only; recovery code below
- }
- }
-
- // Fall back to opening the app settings page.
- context.startActivity(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
- .setData(Uri.fromParts("package", context.getPackageName(), null))
- .addFlags(FLAG_ACTIVITY_NEW_TASK));
- }
- }
- }
-
- public static final String ACTION_WORLD_CITIES_CHANGED =
- "com.android.deskclock.WORLD_CITIES_CHANGED";
-
- /** The single instance of this data model that exists for the life of the application. */
- private static final DataModel sDataModel = new DataModel();
-
- private Handler mHandler;
-
- private Context mContext;
-
- /** The model from which settings are fetched. */
- private SettingsModel mSettingsModel;
-
- /** The model from which city data are fetched. */
- private CityModel mCityModel;
-
- /** The model from which timer data are fetched. */
- private TimerModel mTimerModel;
-
- /** The model from which alarm data are fetched. */
- private AlarmModel mAlarmModel;
-
- /** The model from which widget data are fetched. */
- private WidgetModel mWidgetModel;
-
- /** The model from which data about settings that silence alarms are fetched. */
- private SilentSettingsModel mSilentSettingsModel;
-
- /** The model from which stopwatch data are fetched. */
- private StopwatchModel mStopwatchModel;
-
- /** The model from which notification data are fetched. */
- private NotificationModel mNotificationModel;
-
- /** The model from which time data are fetched. */
- private TimeModel mTimeModel;
-
- /** The model from which ringtone data are fetched. */
- private RingtoneModel mRingtoneModel;
-
- public static DataModel getDataModel() {
- return sDataModel;
- }
-
- private DataModel() {}
-
- /**
- * Initializes the data model with the context and shared preferences to be used.
- */
- public void init(Context context, SharedPreferences prefs) {
- if (mContext != context) {
- mContext = context.getApplicationContext();
-
- mTimeModel = new TimeModel(mContext);
- mWidgetModel = new WidgetModel(prefs);
- mNotificationModel = new NotificationModel();
- mRingtoneModel = new RingtoneModel(mContext, prefs);
- mSettingsModel = new SettingsModel(mContext, prefs, mTimeModel);
- mCityModel = new CityModel(mContext, prefs, mSettingsModel);
- mAlarmModel = new AlarmModel(mContext, mSettingsModel);
- mSilentSettingsModel = new SilentSettingsModel(mContext, mNotificationModel);
- mStopwatchModel = new StopwatchModel(mContext, prefs, mNotificationModel);
- mTimerModel = new TimerModel(mContext, prefs, mSettingsModel, mRingtoneModel,
- mNotificationModel);
- }
- }
-
- /**
- * Convenience for {@code run(runnable, 0)}, i.e. waits indefinitely.
- */
- public void run(Runnable runnable) {
- try {
- run(runnable, 0 /* waitMillis */);
- } catch (InterruptedException ignored) {
- }
- }
-
- /**
- * Updates all timers and the stopwatch after the device has shutdown and restarted.
- */
- public void updateAfterReboot() {
- enforceMainLooper();
- mTimerModel.updateTimersAfterReboot();
- mStopwatchModel.setStopwatch(getStopwatch().updateAfterReboot());
- }
-
- /**
- * Updates all timers and the stopwatch after the device's time has changed.
- */
- public void updateAfterTimeSet() {
- enforceMainLooper();
- mTimerModel.updateTimersAfterTimeSet();
- mStopwatchModel.setStopwatch(getStopwatch().updateAfterTimeSet());
- }
-
- /**
- * Posts a runnable to the main thread and blocks until the runnable executes. Used to access
- * the data model from the main thread.
- */
- public void run(Runnable runnable, long waitMillis) throws InterruptedException {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- runnable.run();
- return;
- }
-
- final ExecutedRunnable er = new ExecutedRunnable(runnable);
- getHandler().post(er);
-
- // Wait for the data to arrive, if it has not.
- synchronized (er) {
- if (!er.isExecuted()) {
- er.wait(waitMillis);
- }
- }
- }
-
- /**
- * @return a handler associated with the main thread
- */
- private synchronized Handler getHandler() {
- if (mHandler == null) {
- mHandler = new Handler(Looper.getMainLooper());
- }
- return mHandler;
- }
-
- //
- // Application
- //
-
- /**
- * @param inForeground {@code true} to indicate the application is open in the foreground
- */
- public void setApplicationInForeground(boolean inForeground) {
- enforceMainLooper();
-
- if (mNotificationModel.isApplicationInForeground() != inForeground) {
- mNotificationModel.setApplicationInForeground(inForeground);
-
- // Refresh all notifications in response to a change in app open state.
- mTimerModel.updateNotification();
- mTimerModel.updateMissedNotification();
- mStopwatchModel.updateNotification();
- mSilentSettingsModel.updateSilentState();
- }
- }
-
- /**
- * @return {@code true} when the application is open in the foreground; {@code false} otherwise
- */
- public boolean isApplicationInForeground() {
- enforceMainLooper();
- return mNotificationModel.isApplicationInForeground();
- }
-
- /**
- * Called when the notifications may be stale or absent from the notification manager and must
- * be rebuilt. e.g. after upgrading the application
- */
- public void updateAllNotifications() {
- enforceMainLooper();
- mTimerModel.updateNotification();
- mTimerModel.updateMissedNotification();
- mStopwatchModel.updateNotification();
- }
-
- //
- // Cities
- //
-
- /**
- * @return a list of all cities in their display order
- */
- public List<City> getAllCities() {
- enforceMainLooper();
- return mCityModel.getAllCities();
- }
-
- /**
- * @return a city representing the user's home timezone
- */
- public City getHomeCity() {
- enforceMainLooper();
- return mCityModel.getHomeCity();
- }
-
- /**
- * @return a list of cities not selected for display
- */
- public List<City> getUnselectedCities() {
- enforceMainLooper();
- return mCityModel.getUnselectedCities();
- }
-
- /**
- * @return a list of cities selected for display
- */
- public List<City> getSelectedCities() {
- enforceMainLooper();
- return mCityModel.getSelectedCities();
- }
-
- /**
- * @param cities the new collection of cities selected for display by the user
- */
- public void setSelectedCities(Collection<City> cities) {
- enforceMainLooper();
- mCityModel.setSelectedCities(cities);
- }
-
- /**
- * @return a comparator used to locate index positions
- */
- public Comparator<City> getCityIndexComparator() {
- enforceMainLooper();
- return mCityModel.getCityIndexComparator();
- }
-
- /**
- * @return the order in which cities are sorted
- */
- public CitySort getCitySort() {
- enforceMainLooper();
- return mCityModel.getCitySort();
- }
-
- /**
- * Adjust the order in which cities are sorted.
- */
- public void toggleCitySort() {
- enforceMainLooper();
- mCityModel.toggleCitySort();
- }
-
- /**
- * @param cityListener listener to be notified when the world city list changes
- */
- public void addCityListener(CityListener cityListener) {
- enforceMainLooper();
- mCityModel.addCityListener(cityListener);
- }
-
- /**
- * @param cityListener listener that no longer needs to be notified of world city list changes
- */
- public void removeCityListener(CityListener cityListener) {
- enforceMainLooper();
- mCityModel.removeCityListener(cityListener);
- }
-
- //
- // Timers
- //
-
- /**
- * @param timerListener to be notified when timers are added, updated and removed
- */
- public void addTimerListener(TimerListener timerListener) {
- enforceMainLooper();
- mTimerModel.addTimerListener(timerListener);
- }
-
- /**
- * @param timerListener to no longer be notified when timers are added, updated and removed
- */
- public void removeTimerListener(TimerListener timerListener) {
- enforceMainLooper();
- mTimerModel.removeTimerListener(timerListener);
- }
-
- /**
- * @return a list of timers for display
- */
- public List<Timer> getTimers() {
- enforceMainLooper();
- return mTimerModel.getTimers();
- }
-
- /**
- * @return a list of expired timers for display
- */
- public List<Timer> getExpiredTimers() {
- enforceMainLooper();
- return mTimerModel.getExpiredTimers();
- }
-
- /**
- * @param timerId identifies the timer to return
- * @return the timer with the given {@code timerId}
- */
- public Timer getTimer(int timerId) {
- enforceMainLooper();
- return mTimerModel.getTimer(timerId);
- }
-
- /**
- * @return the timer that last expired and is still expired now; {@code null} if no timers are
- * expired
- */
- public Timer getMostRecentExpiredTimer() {
- enforceMainLooper();
- return mTimerModel.getMostRecentExpiredTimer();
- }
-
- /**
- * @param length the length of the timer in milliseconds
- * @param label describes the purpose of the timer
- * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
- * @return the newly added timer
- */
- public Timer addTimer(long length, String label, boolean deleteAfterUse) {
- enforceMainLooper();
- return mTimerModel.addTimer(length, label, deleteAfterUse);
- }
-
- /**
- * @param timer the timer to be removed
- */
- public void removeTimer(Timer timer) {
- enforceMainLooper();
- mTimerModel.removeTimer(timer);
- }
-
- /**
- * @param timer the timer to be started
- */
- public void startTimer(Timer timer) {
- startTimer(null, timer);
- }
-
- /**
- * @param service used to start foreground notifications for expired timers
- * @param timer the timer to be started
- */
- public void startTimer(Service service, Timer timer) {
- enforceMainLooper();
- final Timer started = timer.start();
- mTimerModel.updateTimer(started);
- if (timer.getRemainingTime() <= 0) {
- if (service != null) {
- expireTimer(service, started);
- } else {
- mContext.startService(TimerService.createTimerExpiredIntent(mContext, started));
- }
- }
- }
-
- /**
- * @param timer the timer to be paused
- */
- public void pauseTimer(Timer timer) {
- enforceMainLooper();
- mTimerModel.updateTimer(timer.pause());
- }
-
- /**
- * @param service used to start foreground notifications for expired timers
- * @param timer the timer to be expired
- */
- public void expireTimer(Service service, Timer timer) {
- enforceMainLooper();
- mTimerModel.expireTimer(service, timer);
- }
-
- /**
- * @param timer the timer to be reset
- * @return the reset {@code timer}
- */
- public Timer resetTimer(Timer timer) {
- enforceMainLooper();
- return mTimerModel.resetTimer(timer, false /* allowDelete */, 0 /* eventLabelId */);
- }
-
- /**
- * If the given {@code timer} is expired and marked for deletion after use then this method
- * removes the the timer. The timer is otherwise transitioned to the reset state and continues
- * to exist.
- *
- * @param timer the timer to be reset
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- * @return the reset {@code timer} or {@code null} if the timer was deleted
- */
- public Timer resetOrDeleteTimer(Timer timer, @StringRes int eventLabelId) {
- enforceMainLooper();
- return mTimerModel.resetTimer(timer, true /* allowDelete */, eventLabelId);
- }
-
- /**
- * Resets all expired timers.
- *
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- */
- public void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) {
- enforceMainLooper();
- mTimerModel.resetOrDeleteExpiredTimers(eventLabelId);
- }
-
- /**
- * Resets all unexpired timers.
- *
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- */
- public void resetUnexpiredTimers(@StringRes int eventLabelId) {
- enforceMainLooper();
- mTimerModel.resetUnexpiredTimers(eventLabelId);
- }
-
- /**
- * Resets all missed timers.
- *
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- */
- public void resetMissedTimers(@StringRes int eventLabelId) {
- enforceMainLooper();
- mTimerModel.resetMissedTimers(eventLabelId);
- }
-
- /**
- * @param timer the timer to which a minute should be added to the remaining time
- */
- public void addTimerMinute(Timer timer) {
- enforceMainLooper();
- mTimerModel.updateTimer(timer.addMinute());
- }
-
- /**
- * @param timer the timer to which the new {@code label} belongs
- * @param label the new label to store for the {@code timer}
- */
- public void setTimerLabel(Timer timer, String label) {
- enforceMainLooper();
- mTimerModel.updateTimer(timer.setLabel(label));
- }
-
- /**
- * @param timer the timer whose {@code length} to change
- * @param length the new length of the timer in milliseconds
- */
- public void setTimerLength(Timer timer, long length) {
- enforceMainLooper();
- mTimerModel.updateTimer(timer.setLength(length));
- }
-
- /**
- * @param timer the timer whose {@code remainingTime} to change
- * @param remainingTime the new remaining time of the timer in milliseconds
- */
- public void setRemainingTime(Timer timer, long remainingTime) {
- enforceMainLooper();
-
- final Timer updated = timer.setRemainingTime(remainingTime);
- mTimerModel.updateTimer(updated);
- if (timer.isRunning() && timer.getRemainingTime() <= 0) {
- mContext.startService(TimerService.createTimerExpiredIntent(mContext, updated));
- }
- }
-
- /**
- * Updates the timer notifications to be current.
- */
- public void updateTimerNotification() {
- enforceMainLooper();
- mTimerModel.updateNotification();
- }
-
- /**
- * @return the uri of the default ringtone to play for all timers when no user selection exists
- */
- public Uri getDefaultTimerRingtoneUri() {
- enforceMainLooper();
- return mTimerModel.getDefaultTimerRingtoneUri();
- }
-
- /**
- * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
- */
- public boolean isTimerRingtoneSilent() {
- enforceMainLooper();
- return mTimerModel.isTimerRingtoneSilent();
- }
-
- /**
- * @return the uri of the ringtone to play for all timers
- */
- public Uri getTimerRingtoneUri() {
- enforceMainLooper();
- return mTimerModel.getTimerRingtoneUri();
- }
-
- /**
- * @param uri the uri of the ringtone to play for all timers
- */
- public void setTimerRingtoneUri(Uri uri) {
- enforceMainLooper();
- mTimerModel.setTimerRingtoneUri(uri);
- }
-
- /**
- * @return the title of the ringtone that is played for all timers
- */
- public String getTimerRingtoneTitle() {
- enforceMainLooper();
- return mTimerModel.getTimerRingtoneTitle();
- }
-
- /**
- * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
- * {@code 0} implies no crescendo should be applied
- */
- public long getTimerCrescendoDuration() {
- enforceMainLooper();
- return mTimerModel.getTimerCrescendoDuration();
- }
-
- /**
- * @return whether vibrate is enabled for all timers.
- */
- public boolean getTimerVibrate() {
- enforceMainLooper();
- return mTimerModel.getTimerVibrate();
- }
-
- /**
- * @param enabled whether vibrate is enabled for all timers.
- */
- public void setTimerVibrate(boolean enabled) {
- enforceMainLooper();
- mTimerModel.setTimerVibrate(enabled);
- }
-
- //
- // Alarms
- //
-
- /**
- * @return the uri of the ringtone to which all new alarms default
- */
- public Uri getDefaultAlarmRingtoneUri() {
- enforceMainLooper();
- return mAlarmModel.getDefaultAlarmRingtoneUri();
- }
-
- /**
- * @param uri the uri of the ringtone to which future new alarms will default
- */
- public void setDefaultAlarmRingtoneUri(Uri uri) {
- enforceMainLooper();
- mAlarmModel.setDefaultAlarmRingtoneUri(uri);
- }
-
- /**
- * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
- * {@code 0} implies no crescendo should be applied
- */
- public long getAlarmCrescendoDuration() {
- enforceMainLooper();
- return mAlarmModel.getAlarmCrescendoDuration();
- }
-
- /**
- * @return the behavior to execute when volume buttons are pressed while firing an alarm
- */
- public AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
- enforceMainLooper();
- return mAlarmModel.getAlarmVolumeButtonBehavior();
- }
-
- /**
- * @return the number of minutes an alarm may ring before it has timed out and becomes missed
- */
- public int getAlarmTimeout() {
- return mAlarmModel.getAlarmTimeout();
- }
-
- /**
- * @return the number of minutes an alarm will remain snoozed before it rings again
- */
- public int getSnoozeLength() {
- return mAlarmModel.getSnoozeLength();
- }
-
- //
- // Stopwatch
- //
-
- /**
- * @param stopwatchListener to be notified when stopwatch changes or laps are added
- */
- public void addStopwatchListener(StopwatchListener stopwatchListener) {
- enforceMainLooper();
- mStopwatchModel.addStopwatchListener(stopwatchListener);
- }
-
- /**
- * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
- */
- public void removeStopwatchListener(StopwatchListener stopwatchListener) {
- enforceMainLooper();
- mStopwatchModel.removeStopwatchListener(stopwatchListener);
- }
-
- /**
- * @return the current state of the stopwatch
- */
- public Stopwatch getStopwatch() {
- enforceMainLooper();
- return mStopwatchModel.getStopwatch();
- }
-
- /**
- * @return the stopwatch after being started
- */
- public Stopwatch startStopwatch() {
- enforceMainLooper();
- return mStopwatchModel.setStopwatch(getStopwatch().start());
- }
-
- /**
- * @return the stopwatch after being paused
- */
- public Stopwatch pauseStopwatch() {
- enforceMainLooper();
- return mStopwatchModel.setStopwatch(getStopwatch().pause());
- }
-
- /**
- * @return the stopwatch after being reset
- */
- public Stopwatch resetStopwatch() {
- enforceMainLooper();
- return mStopwatchModel.setStopwatch(getStopwatch().reset());
- }
-
- /**
- * @return the laps recorded for this stopwatch
- */
- public List<Lap> getLaps() {
- enforceMainLooper();
- return mStopwatchModel.getLaps();
- }
-
- /**
- * @return a newly recorded lap completed now; {@code null} if no more laps can be added
- */
- public Lap addLap() {
- enforceMainLooper();
- return mStopwatchModel.addLap();
- }
-
- /**
- * @return {@code true} iff more laps can be recorded
- */
- public boolean canAddMoreLaps() {
- enforceMainLooper();
- return mStopwatchModel.canAddMoreLaps();
- }
-
- /**
- * @return the longest lap time of all recorded laps and the current lap
- */
- public long getLongestLapTime() {
- enforceMainLooper();
- return mStopwatchModel.getLongestLapTime();
- }
-
- /**
- * @param time a point in time after the end of the last lap
- * @return the elapsed time between the given {@code time} and the end of the previous lap
- */
- public long getCurrentLapTime(long time) {
- enforceMainLooper();
- return mStopwatchModel.getCurrentLapTime(time);
- }
-
- //
- // Time
- // (Time settings/values are accessible from any Thread so no Thread-enforcement exists.)
- //
-
- /**
- * @return the current time in milliseconds
- */
- public long currentTimeMillis() {
- return mTimeModel.currentTimeMillis();
- }
-
- /**
- * @return milliseconds since boot, including time spent in sleep
- */
- public long elapsedRealtime() {
- return mTimeModel.elapsedRealtime();
- }
-
- /**
- * @return {@code true} if 24 hour time format is selected; {@code false} otherwise
- */
- public boolean is24HourFormat() {
- return mTimeModel.is24HourFormat();
- }
-
- /**
- * @return a new calendar object initialized to the {@link #currentTimeMillis()}
- */
- public Calendar getCalendar() {
- return mTimeModel.getCalendar();
- }
-
- //
- // Ringtones
- //
-
- /**
- * Ringtone titles are cached because loading them is expensive. This method
- * <strong>must</strong> be called on a background thread and is responsible for priming the
- * cache of ringtone titles to avoid later fetching titles on the main thread.
- */
- public void loadRingtoneTitles() {
- enforceNotMainLooper();
- mRingtoneModel.loadRingtoneTitles();
- }
-
- /**
- * Recheck the permission to read each custom ringtone.
- */
- public void loadRingtonePermissions() {
- enforceNotMainLooper();
- mRingtoneModel.loadRingtonePermissions();
- }
-
- /**
- * @param uri the uri of a ringtone
- * @return the title of the ringtone with the {@code uri}; {@code null} if it cannot be fetched
- */
- public String getRingtoneTitle(Uri uri) {
- enforceMainLooper();
- return mRingtoneModel.getRingtoneTitle(uri);
- }
-
- /**
- * @param uri the uri of an audio file to use as a ringtone
- * @param title the title of the audio content at the given {@code uri}
- * @return the ringtone instance created for the audio file
- */
- public CustomRingtone addCustomRingtone(Uri uri, String title) {
- enforceMainLooper();
- return mRingtoneModel.addCustomRingtone(uri, title);
- }
-
- /**
- * @param uri identifies the ringtone to remove
- */
- public void removeCustomRingtone(Uri uri) {
- enforceMainLooper();
- mRingtoneModel.removeCustomRingtone(uri);
- }
-
- /**
- * @return all available custom ringtones
- */
- public List<CustomRingtone> getCustomRingtones() {
- enforceMainLooper();
- return mRingtoneModel.getCustomRingtones();
- }
-
- //
- // Widgets
- //
-
- /**
- * @param widgetClass indicates the type of widget being counted
- * @param count the number of widgets of the given type
- * @param eventCategoryId identifies the category of event to send
- */
- public void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
- enforceMainLooper();
- mWidgetModel.updateWidgetCount(widgetClass, count, eventCategoryId);
- }
-
- //
- // Settings
- //
-
- /**
- * @param silentSettingsListener to be notified when alarm-silencing settings change
- */
- public void addSilentSettingsListener(OnSilentSettingsListener silentSettingsListener) {
- enforceMainLooper();
- mSilentSettingsModel.addSilentSettingsListener(silentSettingsListener);
- }
-
- /**
- * @param silentSettingsListener to no longer be notified when alarm-silencing settings change
- */
- public void removeSilentSettingsListener(OnSilentSettingsListener silentSettingsListener) {
- enforceMainLooper();
- mSilentSettingsModel.removeSilentSettingsListener(silentSettingsListener);
- }
-
- /**
- * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
- */
- public int getGlobalIntentId() {
- return mSettingsModel.getGlobalIntentId();
- }
-
- /**
- * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
- */
- public void updateGlobalIntentId() {
- enforceMainLooper();
- mSettingsModel.updateGlobalIntentId();
- }
-
- /**
- * @return the style of clock to display in the clock application
- */
- public ClockStyle getClockStyle() {
- enforceMainLooper();
- return mSettingsModel.getClockStyle();
- }
-
- /**
- * @return the style of clock to display in the clock application
- */
- public boolean getDisplayClockSeconds() {
- enforceMainLooper();
- return mSettingsModel.getDisplayClockSeconds();
- }
-
- /**
- * @param displaySeconds whether or not to display seconds for main clock
- */
- public void setDisplayClockSeconds(boolean displaySeconds) {
- enforceMainLooper();
- mSettingsModel.setDisplayClockSeconds(displaySeconds);
- }
-
- /**
- * @return the style of clock to display in the clock screensaver
- */
- public ClockStyle getScreensaverClockStyle() {
- enforceMainLooper();
- return mSettingsModel.getScreensaverClockStyle();
- }
-
- /**
- * @return {@code true} if the screen saver should be dimmed for lower contrast at night
- */
- public boolean getScreensaverNightModeOn() {
- enforceMainLooper();
- return mSettingsModel.getScreensaverNightModeOn();
- }
-
- /**
- * @return {@code true} if the users wants to automatically show a clock for their home timezone
- * when they have travelled outside of that timezone
- */
- public boolean getShowHomeClock() {
- enforceMainLooper();
- return mSettingsModel.getShowHomeClock();
- }
-
- /**
- * @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
- * {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
- */
- public Weekdays.Order getWeekdayOrder() {
- enforceMainLooper();
- return mSettingsModel.getWeekdayOrder();
- }
-
- /**
- * @return {@code true} if the restore process (of backup and restore) has completed
- */
- public boolean isRestoreBackupFinished() {
- return mSettingsModel.isRestoreBackupFinished();
- }
-
- /**
- * @param finished {@code true} means the restore process (of backup and restore) has completed
- */
- public void setRestoreBackupFinished(boolean finished) {
- mSettingsModel.setRestoreBackupFinished(finished);
- }
-
- /**
- * @return a description of the time zones available for selection
- */
- public TimeZones getTimeZones() {
- enforceMainLooper();
- return mSettingsModel.getTimeZones();
- }
-
- /**
- * Used to execute a delegate runnable and track its completion.
- */
- private static class ExecutedRunnable implements Runnable {
-
- private final Runnable mDelegate;
- private boolean mExecuted;
-
- private ExecutedRunnable(Runnable delegate) {
- this.mDelegate = delegate;
- }
-
- @Override
- public void run() {
- mDelegate.run();
-
- synchronized (this) {
- mExecuted = true;
- notifyAll();
- }
- }
-
- private boolean isExecuted() {
- return mExecuted;
- }
- }
-}
diff --git a/src/com/android/deskclock/data/DataModel.kt b/src/com/android/deskclock/data/DataModel.kt
new file mode 100644
index 0000000..72e4189
--- /dev/null
+++ b/src/com/android/deskclock/data/DataModel.kt
@@ -0,0 +1,1075 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.app.Service
+import android.content.Context
+import android.content.Context.AUDIO_SERVICE
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.SharedPreferences
+import android.media.AudioManager
+import android.media.AudioManager.FLAG_SHOW_UI
+import android.media.AudioManager.STREAM_ALARM
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+import android.provider.Settings.ACTION_SOUND_SETTINGS
+import android.view.View
+import androidx.annotation.Keep
+import androidx.annotation.StringRes
+
+import com.android.deskclock.Predicate
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.timer.TimerService
+
+import java.util.Calendar
+
+import kotlin.Comparator
+import kotlin.math.roundToInt
+
+/**
+ * All application-wide data is accessible through this singleton.
+ */
+class DataModel private constructor() {
+
+ /** Indicates the display style of clocks. */
+ enum class ClockStyle {
+ ANALOG, DIGITAL
+ }
+
+ /** Indicates the preferred sort order of cities. */
+ enum class CitySort {
+ NAME, UTC_OFFSET
+ }
+
+ /** Indicates the preferred behavior of hardware volume buttons when firing alarms. */
+ enum class AlarmVolumeButtonBehavior {
+ NOTHING, SNOOZE, DISMISS
+ }
+
+ /** Indicates the reason alarms may not fire or may fire silently. */
+ enum class SilentSetting(
+ @field:StringRes @get:StringRes val labelResId: Int,
+ @field:StringRes @get:StringRes val actionResId: Int,
+ private val mActionEnabled: Predicate<Context>,
+ private val mActionListener: View.OnClickListener?
+ ) {
+
+ DO_NOT_DISTURB(R.string.alarms_blocked_by_dnd,
+ 0,
+ Predicate.FALSE as Predicate<Context>,
+ mActionListener = null),
+ MUTED_VOLUME(R.string.alarm_volume_muted,
+ R.string.unmute_alarm_volume,
+ Predicate.TRUE as Predicate<Context>,
+ UnmuteAlarmVolumeListener()),
+ SILENT_RINGTONE(R.string.silent_default_alarm_ringtone,
+ R.string.change_setting_action,
+ ChangeSoundActionPredicate(),
+ ChangeSoundSettingsListener()),
+ BLOCKED_NOTIFICATIONS(R.string.app_notifications_blocked,
+ R.string.change_setting_action,
+ Predicate.TRUE as Predicate<Context>,
+ ChangeAppNotificationSettingsListener());
+
+ val actionListener: View.OnClickListener?
+ get() = mActionListener
+
+ fun isActionEnabled(context: Context): Boolean {
+ return labelResId != 0 && mActionEnabled.apply(context)
+ }
+
+ private class UnmuteAlarmVolumeListener : View.OnClickListener {
+ override fun onClick(v: View) {
+ // Set the alarm volume to 11/16th of max and show the slider UI.
+ // 11/16th of max is the initial volume of the alarm stream on a fresh install.
+ val context: Context = v.context
+ val am: AudioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager
+ val index = (am.getStreamMaxVolume(STREAM_ALARM) * 11f / 16f).roundToInt()
+ am.setStreamVolume(STREAM_ALARM, index, FLAG_SHOW_UI)
+ }
+ }
+
+ private class ChangeSoundSettingsListener : View.OnClickListener {
+ override fun onClick(v: View) {
+ val context: Context = v.context
+ context.startActivity(Intent(ACTION_SOUND_SETTINGS)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK))
+ }
+ }
+
+ private class ChangeSoundActionPredicate : Predicate<Context> {
+ override fun apply(context: Context): Boolean {
+ val intent = Intent(ACTION_SOUND_SETTINGS)
+ return intent.resolveActivity(context.packageManager) != null
+ }
+ }
+
+ private class ChangeAppNotificationSettingsListener : View.OnClickListener {
+ override fun onClick(v: View) {
+ val context: Context = v.context
+ if (Utils.isLOrLater) {
+ try {
+ // Attempt to open the notification settings for this app.
+ context.startActivity(
+ Intent("android.settings.APP_NOTIFICATION_SETTINGS")
+ .putExtra("app_package", context.packageName)
+ .putExtra("app_uid", context.applicationInfo.uid)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK))
+ return
+ } catch (ignored: Exception) {
+ // best attempt only; recovery code below
+ }
+ }
+
+ // Fall back to opening the app settings page.
+ context.startActivity(Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", context.packageName, null))
+ .addFlags(FLAG_ACTIVITY_NEW_TASK))
+ }
+ }
+ }
+
+ private var mHandler: Handler? = null
+ private var mContext: Context? = null
+
+ /** The model from which settings are fetched. */
+ private var mSettingsModel: SettingsModel? = null
+
+ /** The model from which city data are fetched. */
+ private var mCityModel: CityModel? = null
+
+ /** The model from which timer data are fetched. */
+ private var mTimerModel: TimerModel? = null
+
+ /** The model from which alarm data are fetched. */
+ private var mAlarmModel: AlarmModel? = null
+
+ /** The model from which widget data are fetched. */
+ private var mWidgetModel: WidgetModel? = null
+
+ /** The model from which data about settings that silence alarms are fetched. */
+ private var mSilentSettingsModel: SilentSettingsModel? = null
+
+ /** The model from which stopwatch data are fetched. */
+ private var mStopwatchModel: StopwatchModel? = null
+
+ /** The model from which notification data are fetched. */
+ private var mNotificationModel: NotificationModel? = null
+
+ /** The model from which time data are fetched. */
+ private var mTimeModel: TimeModel? = null
+
+ /** The model from which ringtone data are fetched. */
+ private var mRingtoneModel: RingtoneModel? = null
+
+ /**
+ * Initializes the data model with the context and shared preferences to be used.
+ */
+ fun init(context: Context, prefs: SharedPreferences) {
+ if (mContext !== context) {
+ mContext = context.applicationContext
+ mTimeModel = TimeModel(mContext!!)
+ mWidgetModel = WidgetModel(prefs)
+ mNotificationModel = NotificationModel()
+ mRingtoneModel = RingtoneModel(mContext!!, prefs)
+ mSettingsModel = SettingsModel(mContext!!, prefs, mTimeModel!!)
+ mCityModel = CityModel(mContext!!, prefs, mSettingsModel!!)
+ mAlarmModel = AlarmModel(mContext!!, mSettingsModel!!)
+ mSilentSettingsModel = SilentSettingsModel(mContext!!, mNotificationModel!!)
+ mStopwatchModel = StopwatchModel(mContext!!, prefs, mNotificationModel!!)
+ mTimerModel = TimerModel(mContext!!, prefs, mSettingsModel!!, mRingtoneModel!!,
+ mNotificationModel!!)
+ }
+ }
+
+ /**
+ * Convenience for `run(runnable, 0)`, i.e. waits indefinitely.
+ */
+ fun run(runnable: Runnable) {
+ try {
+ run(runnable, 0 /* waitMillis */)
+ } catch (ignored: InterruptedException) {
+ }
+ }
+
+ /**
+ * Updates all timers and the stopwatch after the device has shutdown and restarted.
+ */
+ fun updateAfterReboot() {
+ Utils.enforceMainLooper()
+ mTimerModel!!.updateTimersAfterReboot()
+ mStopwatchModel!!.setStopwatch(stopwatch.updateAfterReboot())
+ }
+
+ /**
+ * Updates all timers and the stopwatch after the device's time has changed.
+ */
+ fun updateAfterTimeSet() {
+ Utils.enforceMainLooper()
+ mTimerModel!!.updateTimersAfterTimeSet()
+ mStopwatchModel!!.setStopwatch(stopwatch.updateAfterTimeSet())
+ }
+
+ /**
+ * Posts a runnable to the main thread and blocks until the runnable executes. Used to access
+ * the data model from the main thread.
+ */
+ @Throws(InterruptedException::class)
+ fun run(runnable: Runnable, waitMillis: Long) {
+ if (Looper.myLooper() === Looper.getMainLooper()) {
+ runnable.run()
+ return
+ }
+
+ val er = ExecutedRunnable(runnable)
+ handler.post(er)
+
+ // Wait for the data to arrive, if it has not.
+ synchronized(er) {
+ if (!er.isExecuted) {
+ er.wait(waitMillis)
+ }
+ }
+ }
+
+ /**
+ * @return a handler associated with the main thread
+ */
+ @get:Synchronized
+ private val handler: Handler
+ get() {
+ if (mHandler == null) {
+ mHandler = Handler(Looper.getMainLooper())
+ }
+ return mHandler!!
+ }
+
+ //
+ // Application
+ //
+
+ var isApplicationInForeground: Boolean
+ /**
+ * @return `true` when the application is open in the foreground; `false` otherwise
+ */
+ get() {
+ Utils.enforceMainLooper()
+ return mNotificationModel!!.isApplicationInForeground
+ }
+ /**
+ * @param inForeground `true` to indicate the application is open in the foreground
+ */
+ set(inForeground) {
+ Utils.enforceMainLooper()
+ if (mNotificationModel!!.isApplicationInForeground != inForeground) {
+ mNotificationModel!!.isApplicationInForeground = inForeground
+
+ // Refresh all notifications in response to a change in app open state.
+ mTimerModel!!.updateNotification()
+ mTimerModel!!.updateMissedNotification()
+ mStopwatchModel!!.updateNotification()
+ mSilentSettingsModel!!.updateSilentState()
+ }
+ }
+
+ /**
+ * Called when the notifications may be stale or absent from the notification manager and must
+ * be rebuilt. e.g. after upgrading the application
+ */
+ fun updateAllNotifications() {
+ Utils.enforceMainLooper()
+ mTimerModel!!.updateNotification()
+ mTimerModel!!.updateMissedNotification()
+ mStopwatchModel!!.updateNotification()
+ }
+
+ //
+ // Cities
+ //
+
+ /**
+ * @return a list of all cities in their display order
+ */
+ val allCities: List<City>
+ get() {
+ Utils.enforceMainLooper()
+ return mCityModel!!.allCities
+ }
+
+ /**
+ * @return a city representing the user's home timezone
+ */
+ val homeCity: City
+ get() {
+ Utils.enforceMainLooper()
+ return mCityModel!!.homeCity
+ }
+
+ /**
+ * @return a list of cities not selected for display
+ */
+ val unselectedCities: List<City>
+ get() {
+ Utils.enforceMainLooper()
+ return mCityModel!!.unselectedCities
+ }
+
+ var selectedCities: Collection<City>
+ /**
+ * @return a list of cities selected for display
+ */
+ get() {
+ Utils.enforceMainLooper()
+ return mCityModel!!.selectedCities
+ }
+ /**
+ * @param cities the new collection of cities selected for display by the user
+ */
+ set(cities) {
+ Utils.enforceMainLooper()
+ mCityModel?.setSelectedCities(cities)
+ }
+
+ /**
+ * @return a comparator used to locate index positions
+ */
+ val cityIndexComparator: Comparator<City>
+ get() {
+ Utils.enforceMainLooper()
+ return mCityModel!!.cityIndexComparator
+ }
+
+ /**
+ * @return the order in which cities are sorted
+ */
+ val citySort: CitySort
+ get() {
+ Utils.enforceMainLooper()
+ return mCityModel!!.citySort
+ }
+
+ /**
+ * Adjust the order in which cities are sorted.
+ */
+ fun toggleCitySort() {
+ Utils.enforceMainLooper()
+ mCityModel?.toggleCitySort()
+ }
+
+ /**
+ * @param cityListener listener to be notified when the world city list changes
+ */
+ fun addCityListener(cityListener: CityListener) {
+ Utils.enforceMainLooper()
+ mCityModel?.addCityListener(cityListener)
+ }
+
+ /**
+ * @param cityListener listener that no longer needs to be notified of world city list changes
+ */
+ fun removeCityListener(cityListener: CityListener) {
+ Utils.enforceMainLooper()
+ mCityModel?.removeCityListener(cityListener)
+ }
+
+ //
+ // Timers
+ //
+
+ /**
+ * @param timerListener to be notified when timers are added, updated and removed
+ */
+ fun addTimerListener(timerListener: TimerListener) {
+ Utils.enforceMainLooper()
+ mTimerModel?.addTimerListener(timerListener)
+ }
+
+ /**
+ * @param timerListener to no longer be notified when timers are added, updated and removed
+ */
+ fun removeTimerListener(timerListener: TimerListener) {
+ Utils.enforceMainLooper()
+ mTimerModel?.removeTimerListener(timerListener)
+ }
+
+ /**
+ * @return a list of timers for display
+ */
+ val timers: List<Timer>
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.timers
+ }
+
+ /**
+ * @return a list of expired timers for display
+ */
+ val expiredTimers: List<Timer>
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.expiredTimers
+ }
+
+ /**
+ * @param timerId identifies the timer to return
+ * @return the timer with the given `timerId`
+ */
+ fun getTimer(timerId: Int): Timer? {
+ Utils.enforceMainLooper()
+ return mTimerModel?.getTimer(timerId)
+ }
+
+ /**
+ * @return the timer that last expired and is still expired now; `null` if no timers are
+ * expired
+ */
+ val mostRecentExpiredTimer: Timer?
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel?.mostRecentExpiredTimer
+ }
+
+ /**
+ * @param length the length of the timer in milliseconds
+ * @param label describes the purpose of the timer
+ * @param deleteAfterUse `true` indicates the timer should be deleted when it is reset
+ * @return the newly added timer
+ */
+ fun addTimer(length: Long, label: String?, deleteAfterUse: Boolean): Timer {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.addTimer(length, label, deleteAfterUse)
+ }
+
+ /**
+ * @param timer the timer to be removed
+ */
+ fun removeTimer(timer: Timer) {
+ Utils.enforceMainLooper()
+ mTimerModel?.removeTimer(timer)
+ }
+
+ /**
+ * @param timer the timer to be started
+ */
+ fun startTimer(timer: Timer) {
+ startTimer(null, timer)
+ }
+
+ /**
+ * @param service used to start foreground notifications for expired timers
+ * @param timer the timer to be started
+ */
+ fun startTimer(service: Service?, timer: Timer) {
+ Utils.enforceMainLooper()
+ val started = timer.start()
+ mTimerModel?.updateTimer(started)
+ if (timer.remainingTime <= 0) {
+ if (service != null) {
+ expireTimer(service, started)
+ } else {
+ mContext!!.startService(TimerService.createTimerExpiredIntent(mContext!!, started))
+ }
+ }
+ }
+
+ /**
+ * @param timer the timer to be paused
+ */
+ fun pauseTimer(timer: Timer) {
+ Utils.enforceMainLooper()
+ mTimerModel?.updateTimer(timer.pause())
+ }
+
+ /**
+ * @param service used to start foreground notifications for expired timers
+ * @param timer the timer to be expired
+ */
+ fun expireTimer(service: Service?, timer: Timer) {
+ Utils.enforceMainLooper()
+ mTimerModel?.expireTimer(service, timer)
+ }
+
+ /**
+ * @param timer the timer to be reset
+ * @return the reset `timer`
+ */
+ @Keep
+ fun resetTimer(timer: Timer): Timer? {
+ Utils.enforceMainLooper()
+ return mTimerModel?.resetTimer(timer, false /* allowDelete */, 0 /* eventLabelId */)
+ }
+
+ /**
+ * If the given `timer` is expired and marked for deletion after use then this method
+ * removes the timer. The timer is otherwise transitioned to the reset state and continues
+ * to exist.
+ *
+ * @param timer the timer to be reset
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ * @return the reset `timer` or `null` if the timer was deleted
+ */
+ fun resetOrDeleteTimer(timer: Timer, @StringRes eventLabelId: Int): Timer? {
+ Utils.enforceMainLooper()
+ return mTimerModel?.resetTimer(timer, true /* allowDelete */, eventLabelId)
+ }
+
+ /**
+ * Resets all expired timers.
+ *
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ */
+ fun resetOrDeleteExpiredTimers(@StringRes eventLabelId: Int) {
+ Utils.enforceMainLooper()
+ mTimerModel?.resetOrDeleteExpiredTimers(eventLabelId)
+ }
+
+ /**
+ * Resets all unexpired timers.
+ *
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ */
+ fun resetUnexpiredTimers(@StringRes eventLabelId: Int) {
+ Utils.enforceMainLooper()
+ mTimerModel?.resetUnexpiredTimers(eventLabelId)
+ }
+
+ /**
+ * Resets all missed timers.
+ *
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ */
+ fun resetMissedTimers(@StringRes eventLabelId: Int) {
+ Utils.enforceMainLooper()
+ mTimerModel?.resetMissedTimers(eventLabelId)
+ }
+
+ /**
+ * @param timer the timer to which a minute should be added to the remaining time
+ */
+ fun addTimerMinute(timer: Timer) {
+ Utils.enforceMainLooper()
+ mTimerModel?.updateTimer(timer.addMinute())
+ }
+
+ /**
+ * @param timer the timer to which the new `label` belongs
+ * @param label the new label to store for the `timer`
+ */
+ fun setTimerLabel(timer: Timer, label: String?) {
+ Utils.enforceMainLooper()
+ mTimerModel?.updateTimer(timer.setLabel(label))
+ }
+
+ /**
+ * @param timer the timer whose `length` to change
+ * @param length the new length of the timer in milliseconds
+ */
+ fun setTimerLength(timer: Timer, length: Long) {
+ Utils.enforceMainLooper()
+ mTimerModel?.updateTimer(timer.setLength(length))
+ }
+
+ /**
+ * @param timer the timer whose `remainingTime` to change
+ * @param remainingTime the new remaining time of the timer in milliseconds
+ */
+ fun setRemainingTime(timer: Timer, remainingTime: Long) {
+ Utils.enforceMainLooper()
+
+ val updated = timer.setRemainingTime(remainingTime)
+ mTimerModel?.updateTimer(updated)
+ if (timer.isRunning && timer.remainingTime <= 0) {
+ mContext?.startService(TimerService.createTimerExpiredIntent(mContext!!, updated))
+ }
+ }
+
+ /**
+ * Updates the timer notifications to be current.
+ */
+ fun updateTimerNotification() {
+ Utils.enforceMainLooper()
+ mTimerModel?.updateNotification()
+ }
+
+ /**
+ * @return the uri of the default ringtone to play for all timers when no user selection exists
+ */
+ val defaultTimerRingtoneUri: Uri
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.defaultTimerRingtoneUri
+ }
+
+ /**
+ * @return `true` iff the ringtone to play for all timers is the silent ringtone
+ */
+ val isTimerRingtoneSilent: Boolean
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.isTimerRingtoneSilent
+ }
+
+ var timerRingtoneUri: Uri
+ /**
+ * @return the uri of the ringtone to play for all timers
+ */
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.timerRingtoneUri
+ }
+ /**
+ * @param uri the uri of the ringtone to play for all timers
+ */
+ set(uri) {
+ Utils.enforceMainLooper()
+ mTimerModel!!.timerRingtoneUri = uri
+ }
+
+ /**
+ * @return the title of the ringtone that is played for all timers
+ */
+ val timerRingtoneTitle: String
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.timerRingtoneTitle
+ }
+
+ /**
+ * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+ * `0` implies no crescendo should be applied
+ */
+ val timerCrescendoDuration: Long
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.timerCrescendoDuration
+ }
+
+ var timerVibrate: Boolean
+ /**
+ * @return whether vibrate is enabled for all timers.
+ */
+ get() {
+ Utils.enforceMainLooper()
+ return mTimerModel!!.timerVibrate
+ }
+ /**
+ * @param enabled whether vibrate is enabled for all timers.
+ */
+ set(enabled) {
+ Utils.enforceMainLooper()
+ mTimerModel!!.timerVibrate = enabled
+ }
+
+ //
+ // Alarms
+ //
+
+ var defaultAlarmRingtoneUri: Uri
+ /**
+ * @return the uri of the ringtone to which all new alarms default
+ */
+ get() {
+ Utils.enforceMainLooper()
+ return mAlarmModel!!.defaultAlarmRingtoneUri
+ }
+ /**
+ * @param uri the uri of the ringtone to which future new alarms will default
+ */
+ set(uri) {
+ Utils.enforceMainLooper()
+ mAlarmModel!!.defaultAlarmRingtoneUri = uri
+ }
+
+ /**
+ * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
+ * `0` implies no crescendo should be applied
+ */
+ val alarmCrescendoDuration: Long
+ get() {
+ Utils.enforceMainLooper()
+ return mAlarmModel!!.alarmCrescendoDuration
+ }
+
+ /**
+ * @return the behavior to execute when volume buttons are pressed while firing an alarm
+ */
+ val alarmVolumeButtonBehavior: AlarmVolumeButtonBehavior
+ get() {
+ Utils.enforceMainLooper()
+ return mAlarmModel!!.alarmVolumeButtonBehavior
+ }
+
+ /**
+ * @return the number of minutes an alarm may ring before it has timed out and becomes missed
+ */
+ val alarmTimeout: Int
+ get() = mAlarmModel!!.alarmTimeout
+
+ /**
+ * @return the number of minutes an alarm will remain snoozed before it rings again
+ */
+ val snoozeLength: Int
+ get() = mAlarmModel!!.snoozeLength
+
+ //
+ // Stopwatch
+ //
+
+ /**
+ * @param stopwatchListener to be notified when stopwatch changes or laps are added
+ */
+ fun addStopwatchListener(stopwatchListener: StopwatchListener) {
+ Utils.enforceMainLooper()
+ mStopwatchModel?.addStopwatchListener(stopwatchListener)
+ }
+
+ /**
+ * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
+ */
+ fun removeStopwatchListener(stopwatchListener: StopwatchListener) {
+ Utils.enforceMainLooper()
+ mStopwatchModel?.removeStopwatchListener(stopwatchListener)
+ }
+
+ /**
+ * @return the current state of the stopwatch
+ */
+ val stopwatch: Stopwatch
+ get() {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.stopwatch
+ }
+
+ /**
+ * @return the stopwatch after being started
+ */
+ fun startStopwatch(): Stopwatch {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.setStopwatch(stopwatch.start())
+ }
+
+ /**
+ * @return the stopwatch after being paused
+ */
+ fun pauseStopwatch(): Stopwatch {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.setStopwatch(stopwatch.pause())
+ }
+
+ /**
+ * @return the stopwatch after being reset
+ */
+ fun resetStopwatch(): Stopwatch {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.setStopwatch(stopwatch.reset())
+ }
+
+ /**
+ * @return the laps recorded for this stopwatch
+ */
+ val laps: List<Lap>
+ get() {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.laps
+ }
+
+ /**
+ * @return a newly recorded lap completed now; `null` if no more laps can be added
+ */
+ fun addLap(): Lap? {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.addLap()
+ }
+
+ /**
+ * @return `true` iff more laps can be recorded
+ */
+ fun canAddMoreLaps(): Boolean {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.canAddMoreLaps()
+ }
+
+ /**
+ * @return the longest lap time of all recorded laps and the current lap
+ */
+ val longestLapTime: Long
+ get() {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.longestLapTime
+ }
+
+ /**
+ * @param time a point in time after the end of the last lap
+ * @return the elapsed time between the given `time` and the end of the previous lap
+ */
+ fun getCurrentLapTime(time: Long): Long {
+ Utils.enforceMainLooper()
+ return mStopwatchModel!!.getCurrentLapTime(time)
+ }
+
+ //
+ // Time
+ // (Time settings/values are accessible from any Thread so no Thread-enforcement exists.)
+ //
+
+ /**
+ * @return the current time in milliseconds
+ */
+ fun currentTimeMillis(): Long {
+ return mTimeModel!!.currentTimeMillis()
+ }
+
+ /**
+ * @return milliseconds since boot, including time spent in sleep
+ */
+ fun elapsedRealtime(): Long {
+ return mTimeModel!!.elapsedRealtime()
+ }
+
+ /**
+ * @return `true` if 24 hour time format is selected; `false` otherwise
+ */
+ fun is24HourFormat(): Boolean {
+ return mTimeModel!!.is24HourFormat()
+ }
+
+ /**
+ * @return a new calendar object initialized to the [.currentTimeMillis]
+ */
+ val calendar: Calendar
+ get() = mTimeModel!!.calendar
+
+ //
+ // Ringtones
+ //
+
+ /**
+ * Ringtone titles are cached because loading them is expensive. This method
+ * **must** be called on a background thread and is responsible for priming the
+ * cache of ringtone titles to avoid later fetching titles on the main thread.
+ */
+ fun loadRingtoneTitles() {
+ Utils.enforceNotMainLooper()
+ mRingtoneModel?.loadRingtoneTitles()
+ }
+
+ /**
+ * Recheck the permission to read each custom ringtone.
+ */
+ fun loadRingtonePermissions() {
+ Utils.enforceNotMainLooper()
+ mRingtoneModel?.loadRingtonePermissions()
+ }
+
+ /**
+ * @param uri the uri of a ringtone
+ * @return the title of the ringtone with the `uri`; `null` if it cannot be fetched
+ */
+ fun getRingtoneTitle(uri: Uri): String? {
+ Utils.enforceMainLooper()
+ return mRingtoneModel?.getRingtoneTitle(uri)
+ }
+
+ /**
+ * @param uri the uri of an audio file to use as a ringtone
+ * @param title the title of the audio content at the given `uri`
+ * @return the ringtone instance created for the audio file
+ */
+ fun addCustomRingtone(uri: Uri, title: String?): CustomRingtone? {
+ Utils.enforceMainLooper()
+ return mRingtoneModel?.addCustomRingtone(uri, title)
+ }
+
+ /**
+ * @param uri identifies the ringtone to remove
+ */
+ fun removeCustomRingtone(uri: Uri) {
+ Utils.enforceMainLooper()
+ mRingtoneModel?.removeCustomRingtone(uri)
+ }
+
+ /**
+ * @return all available custom ringtones
+ */
+ val customRingtones: List<CustomRingtone>
+ get() {
+ Utils.enforceMainLooper()
+ return mRingtoneModel!!.customRingtones
+ }
+
+ //
+ // Widgets
+ //
+
+ /**
+ * @param widgetClass indicates the type of widget being counted
+ * @param count the number of widgets of the given type
+ * @param eventCategoryId identifies the category of event to send
+ */
+ fun updateWidgetCount(widgetClass: Class<*>?, count: Int, @StringRes eventCategoryId: Int) {
+ Utils.enforceMainLooper()
+ mWidgetModel!!.updateWidgetCount(widgetClass!!, count, eventCategoryId)
+ }
+
+ //
+ // Settings
+ //
+
+ /**
+ * @param silentSettingsListener to be notified when alarm-silencing settings change
+ */
+ fun addSilentSettingsListener(silentSettingsListener: OnSilentSettingsListener) {
+ Utils.enforceMainLooper()
+ mSilentSettingsModel?.addSilentSettingsListener(silentSettingsListener)
+ }
+
+ /**
+ * @param silentSettingsListener to no longer be notified when alarm-silencing settings change
+ */
+ fun removeSilentSettingsListener(silentSettingsListener: OnSilentSettingsListener) {
+ Utils.enforceMainLooper()
+ mSilentSettingsModel?.removeSilentSettingsListener(silentSettingsListener)
+ }
+
+ /**
+ * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ val globalIntentId: Int
+ get() = mSettingsModel!!.globalIntentId
+
+ /**
+ * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ fun updateGlobalIntentId() {
+ Utils.enforceMainLooper()
+ mSettingsModel!!.updateGlobalIntentId()
+ }
+
+ /**
+ * @return the style of clock to display in the clock application
+ */
+ val clockStyle: ClockStyle
+ get() {
+ Utils.enforceMainLooper()
+ return mSettingsModel!!.clockStyle
+ }
+
+ var displayClockSeconds: Boolean
+ /**
+ * @return the style of clock to display in the clock application
+ */
+ get() {
+ Utils.enforceMainLooper()
+ return mSettingsModel!!.displayClockSeconds
+ }
+ /**
+ * @param displaySeconds whether or not to display seconds for main clock
+ */
+ set(displaySeconds) {
+ Utils.enforceMainLooper()
+ mSettingsModel!!.displayClockSeconds = displaySeconds
+ }
+
+ /**
+ * @return the style of clock to display in the clock screensaver
+ */
+ val screensaverClockStyle: ClockStyle
+ get() {
+ Utils.enforceMainLooper()
+ return mSettingsModel!!.screensaverClockStyle
+ }
+
+ /**
+ * @return `true` if the screen saver should be dimmed for lower contrast at night
+ */
+ val screensaverNightModeOn: Boolean
+ get() {
+ Utils.enforceMainLooper()
+ return mSettingsModel!!.screensaverNightModeOn
+ }
+
+ /**
+ * @return `true` if the users wants to automatically show a clock for their home timezone
+ * when they have travelled outside of that timezone
+ */
+ val showHomeClock: Boolean
+ get() {
+ Utils.enforceMainLooper()
+ return mSettingsModel!!.showHomeClock
+ }
+
+ /**
+ * @return the display order of the weekdays, which can start with [Calendar.SATURDAY],
+ * [Calendar.SUNDAY] or [Calendar.MONDAY]
+ */
+ val weekdayOrder: Weekdays.Order
+ get() {
+ Utils.enforceMainLooper()
+ return mSettingsModel!!.weekdayOrder
+ }
+
+ var isRestoreBackupFinished: Boolean
+ /**
+ * @return `true` if the restore process (of backup and restore) has completed
+ */
+ get() = mSettingsModel!!.isRestoreBackupFinished
+ /**
+ * @param finished `true` means the restore process (of backup and restore) has completed
+ */
+ set(finished) {
+ mSettingsModel!!.isRestoreBackupFinished = finished
+ }
+
+ /**
+ * @return a description of the time zones available for selection
+ */
+ val timeZones: TimeZones
+ get() {
+ Utils.enforceMainLooper()
+ return mSettingsModel!!.timeZones
+ }
+
+ /**
+ * Used to execute a delegate runnable and track its completion.
+ */
+ private class ExecutedRunnable(private val mDelegate: Runnable) : Runnable, java.lang.Object() {
+ var isExecuted = false
+ override fun run() {
+ mDelegate.run()
+ synchronized(this) {
+ isExecuted = true
+ notifyAll()
+ }
+ }
+ }
+
+ companion object {
+ const val ACTION_WORLD_CITIES_CHANGED = "com.android.deskclock.WORLD_CITIES_CHANGED"
+
+ /** The single instance of this data model that exists for the life of the application. */
+ val sDataModel = DataModel()
+
+ @get:JvmStatic
+ @get:Keep
+ val dataModel
+ get() = sDataModel
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Lap.java b/src/com/android/deskclock/data/Lap.java
deleted file mode 100644
index 1c42fd8..0000000
--- a/src/com/android/deskclock/data/Lap.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-/**
- * A read-only domain object representing a stopwatch lap.
- */
-public final class Lap {
-
- /** The 1-based position of the lap. */
- private final int mLapNumber;
-
- /** Elapsed time in ms since the lap was last started. */
- private final long mLapTime;
-
- /** Elapsed time in ms accumulated for all laps up to and including this one. */
- private final long mAccumulatedTime;
-
- Lap(int lapNumber, long lapTime, long accumulatedTime) {
- mLapNumber = lapNumber;
- mLapTime = lapTime;
- mAccumulatedTime = accumulatedTime;
- }
-
- public int getLapNumber() { return mLapNumber; }
- public long getLapTime() { return mLapTime; }
- public long getAccumulatedTime() { return mAccumulatedTime; }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Lap.kt b/src/com/android/deskclock/data/Lap.kt
new file mode 100644
index 0000000..bb109ed
--- /dev/null
+++ b/src/com/android/deskclock/data/Lap.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+/**
+ * A read-only domain object representing a stopwatch lap.
+ */
+data class Lap(
+ /** The 1-based position of the lap. */
+ val lapNumber: Int,
+ /** Elapsed time in ms since the lap was last started. */
+ val lapTime: Long,
+ /** Elapsed time in ms accumulated for all laps up to and including this one. */
+ val accumulatedTime: Long
+)
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/NotificationModel.java b/src/com/android/deskclock/data/NotificationModel.java
deleted file mode 100644
index b0b00f3..0000000
--- a/src/com/android/deskclock/data/NotificationModel.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-/**
- * Data that must be coordinated across all notifications is accessed via this model.
- */
-final class NotificationModel {
-
- private boolean mApplicationInForeground;
-
- /**
- * @param inForeground {@code true} to indicate the application is open in the foreground
- */
- void setApplicationInForeground(boolean inForeground) {
- mApplicationInForeground = inForeground;
- }
-
- /**
- * @return {@code true} while the application is open in the foreground
- */
- boolean isApplicationInForeground() {
- return mApplicationInForeground;
- }
-
- //
- // Notification IDs
- //
- // Used elsewhere:
- // Integer.MAX_VALUE - 4
- // Integer.MAX_VALUE - 5
- // Integer.MAX_VALUE - 7
- //
-
- /**
- * @return a value that identifies the stopwatch notification
- */
- int getStopwatchNotificationId() {
- return Integer.MAX_VALUE - 1;
- }
-
- /**
- * @return a value that identifies the notification for running/paused timers
- */
- int getUnexpiredTimerNotificationId() {
- return Integer.MAX_VALUE - 2;
- }
-
- /**
- * @return a value that identifies the notification for expired timers
- */
- int getExpiredTimerNotificationId() {
- return Integer.MAX_VALUE - 3;
- }
-
- /**
- * @return a value that identifies the notification for missed timers
- */
- int getMissedTimerNotificationId() {
- return Integer.MAX_VALUE - 6;
- }
-
- //
- // Notification Group keys
- //
- // Used elsewhere:
- // "1"
- // "4"
-
- /**
- * @return the group key for the stopwatch notification
- */
- String getStopwatchNotificationGroupKey() {
- return "3";
- }
-
- /**
- * @return the group key for the timer notification
- */
- String getTimerNotificationGroupKey() {
- return "2";
- }
-
- //
- // Notification Sort keys
- //
-
- /**
- * @return the sort key for the timer notification
- */
- String getTimerNotificationSortKey() {
- return "0";
- }
-
- /**
- * @return the sort key for the missed timer notification
- */
- String getTimerNotificationMissedSortKey() {
- return "1";
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/NotificationModel.kt b/src/com/android/deskclock/data/NotificationModel.kt
new file mode 100644
index 0000000..f4c0faa
--- /dev/null
+++ b/src/com/android/deskclock/data/NotificationModel.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+/**
+ * Data that must be coordinated across all notifications is accessed via this model.
+ */
+internal class NotificationModel {
+ /**
+ * @return `true` while the application is open in the foreground
+ */
+ /**
+ * @param inForeground `true` to indicate the application is open in the foreground
+ */
+ var isApplicationInForeground = false
+
+ //
+ // Notification IDs
+ //
+ // Used elsewhere:
+ // Integer.MAX_VALUE - 4
+ // Integer.MAX_VALUE - 5
+ // Integer.MAX_VALUE - 7
+ //
+
+ /**
+ * @return a value that identifies the stopwatch notification
+ */
+ val stopwatchNotificationId: Int
+ get() = Int.MAX_VALUE - 1
+
+ /**
+ * @return a value that identifies the notification for running/paused timers
+ */
+ val unexpiredTimerNotificationId: Int
+ get() = Int.MAX_VALUE - 2
+
+ /**
+ * @return a value that identifies the notification for expired timers
+ */
+ val expiredTimerNotificationId: Int
+ get() = Int.MAX_VALUE - 3
+
+ /**
+ * @return a value that identifies the notification for missed timers
+ */
+ val missedTimerNotificationId: Int
+ get() = Int.MAX_VALUE - 6
+
+ //
+ // Notification Group keys
+ //
+ // Used elsewhere:
+ // "1"
+ // "4"
+
+ /**
+ * @return the group key for the stopwatch notification
+ */
+ val stopwatchNotificationGroupKey: String
+ get() = "3"
+
+ /**
+ * @return the group key for the timer notification
+ */
+ val timerNotificationGroupKey: String
+ get() = "2"
+
+ //
+ // Notification Sort keys
+ //
+
+ /**
+ * @return the sort key for the timer notification
+ */
+ val timerNotificationSortKey: String
+ get() = "0"
+
+ /**
+ * @return the sort key for the missed timer notification
+ */
+ val timerNotificationMissedSortKey: String
+ get() = "1"
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/OnSilentSettingsListener.java b/src/com/android/deskclock/data/OnSilentSettingsListener.kt
similarity index 72%
rename from src/com/android/deskclock/data/OnSilentSettingsListener.java
rename to src/com/android/deskclock/data/OnSilentSettingsListener.kt
index 783b6e0..47d5336 100644
--- a/src/com/android/deskclock/data/OnSilentSettingsListener.java
+++ b/src/com/android/deskclock/data/OnSilentSettingsListener.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,14 @@
* limitations under the License.
*/
-package com.android.deskclock.data;
+package com.android.deskclock.data
+
+import com.android.deskclock.data.DataModel.SilentSetting
/**
* The interface through which interested parties are notified of changes to device settings that
* silence firing alarms.
*/
-public interface OnSilentSettingsListener {
- void onSilentSettingsChange(DataModel.SilentSetting before, DataModel.SilentSetting after);
+interface OnSilentSettingsListener {
+ fun onSilentSettingsChange(before: SilentSetting?, after: SilentSetting?)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/RingtoneModel.java b/src/com/android/deskclock/data/RingtoneModel.java
deleted file mode 100644
index 90b7f91..0000000
--- a/src/com/android/deskclock/data/RingtoneModel.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.annotation.SuppressLint;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.UriPermission;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.provider.Alarm;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.Set;
-
-import static android.media.AudioManager.STREAM_ALARM;
-import static android.media.RingtoneManager.TITLE_COLUMN_INDEX;
-
-/**
- * All ringtone data is accessed via this model.
- */
-final class RingtoneModel {
-
- private final Context mContext;
-
- private final SharedPreferences mPrefs;
-
- /** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive. */
- private final Map<Uri, String> mRingtoneTitles = new ArrayMap<>(16);
-
- /** Clears data structures containing data that is locale-sensitive. */
- @SuppressWarnings("FieldCanBeLocal")
- private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
- /** A mutable copy of the custom ringtones. */
- private List<CustomRingtone> mCustomRingtones;
-
- RingtoneModel(Context context, SharedPreferences prefs) {
- mContext = context;
- mPrefs = prefs;
-
- // Clear caches affected by system settings when system settings change.
- final ContentResolver cr = mContext.getContentResolver();
- final ContentObserver observer = new SystemAlarmAlertChangeObserver();
- cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer);
-
- // Clear caches affected by locale when locale changes.
- final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
- mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
- }
-
- CustomRingtone addCustomRingtone(Uri uri, String title) {
- // If the uri is already present in an existing ringtone, do nothing.
- final CustomRingtone existing = getCustomRingtone(uri);
- if (existing != null) {
- return existing;
- }
-
- final CustomRingtone ringtone = CustomRingtoneDAO.addCustomRingtone(mPrefs, uri, title);
- getMutableCustomRingtones().add(ringtone);
- Collections.sort(getMutableCustomRingtones());
- return ringtone;
- }
-
- void removeCustomRingtone(Uri uri) {
- final List<CustomRingtone> ringtones = getMutableCustomRingtones();
- for (CustomRingtone ringtone : ringtones) {
- if (ringtone.getUri().equals(uri)) {
- CustomRingtoneDAO.removeCustomRingtone(mPrefs, ringtone.getId());
- ringtones.remove(ringtone);
- break;
- }
- }
- }
-
- private CustomRingtone getCustomRingtone(Uri uri) {
- for (CustomRingtone ringtone : getMutableCustomRingtones()) {
- if (ringtone.getUri().equals(uri)) {
- return ringtone;
- }
- }
-
- return null;
- }
-
- List<CustomRingtone> getCustomRingtones() {
- return Collections.unmodifiableList(getMutableCustomRingtones());
- }
-
- @SuppressLint("NewApi")
- void loadRingtonePermissions() {
- final List<CustomRingtone> ringtones = getMutableCustomRingtones();
- if (ringtones.isEmpty()) {
- return;
- }
-
- final List<UriPermission> uriPermissions =
- mContext.getContentResolver().getPersistedUriPermissions();
- final Set<Uri> permissions = new ArraySet<>(uriPermissions.size());
- for (UriPermission uriPermission : uriPermissions) {
- permissions.add(uriPermission.getUri());
- }
-
- for (ListIterator<CustomRingtone> i = ringtones.listIterator(); i.hasNext();) {
- final CustomRingtone ringtone = i.next();
- i.set(ringtone.setHasPermissions(permissions.contains(ringtone.getUri())));
- }
- }
-
- void loadRingtoneTitles() {
- // Early return if the cache is already primed.
- if (!mRingtoneTitles.isEmpty()) {
- return;
- }
-
- final RingtoneManager ringtoneManager = new RingtoneManager(mContext);
- ringtoneManager.setType(STREAM_ALARM);
-
- // Cache a title for each system ringtone.
- try (Cursor cursor = ringtoneManager.getCursor()) {
- for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
- final String ringtoneTitle = cursor.getString(TITLE_COLUMN_INDEX);
- final Uri ringtoneUri = ringtoneManager.getRingtoneUri(cursor.getPosition());
- mRingtoneTitles.put(ringtoneUri, ringtoneTitle);
- }
- } catch (Throwable ignored) {
- // best attempt only
- LogUtils.e("Error loading ringtone title cache", ignored);
- }
- }
-
- String getRingtoneTitle(Uri uri) {
- // Special case: no ringtone has a title of "Silent".
- if (Alarm.NO_RINGTONE_URI.equals(uri)) {
- return mContext.getString(R.string.silent_ringtone_title);
- }
-
- // If the ringtone is custom, it has its own title.
- final CustomRingtone customRingtone = getCustomRingtone(uri);
- if (customRingtone != null) {
- return customRingtone.getTitle();
- }
-
- // Check the cache.
- String title = mRingtoneTitles.get(uri);
-
- if (title == null) {
- // This is slow because a media player is created during Ringtone object creation.
- final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri);
- if (ringtone == null) {
- LogUtils.e("No ringtone for uri: %s", uri);
- return mContext.getString(R.string.unknown_ringtone_title);
- }
-
- // Cache the title for later use.
- title = ringtone.getTitle(mContext);
- mRingtoneTitles.put(uri, title);
- }
- return title;
- }
-
- private List<CustomRingtone> getMutableCustomRingtones() {
- if (mCustomRingtones == null) {
- mCustomRingtones = CustomRingtoneDAO.getCustomRingtones(mPrefs);
- Collections.sort(mCustomRingtones);
- }
-
- return mCustomRingtones;
- }
-
- /**
- * This receiver is notified when system settings change. Cached information built on
- * those system settings must be cleared.
- */
- private final class SystemAlarmAlertChangeObserver extends ContentObserver {
-
- private SystemAlarmAlertChangeObserver() {
- super(new Handler());
- }
-
- @Override
- public void onChange(boolean selfChange) {
- super.onChange(selfChange);
-
- // Titles such as "Default ringtone (Oxygen)" are wrong after default ringtone changes.
- mRingtoneTitles.clear();
- }
- }
-
- /**
- * Cached information that is locale-sensitive must be cleared in response to locale changes.
- */
- private final class LocaleChangedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- // Titles such as "Default ringtone (Oxygen)" are wrong after locale changes.
- mRingtoneTitles.clear();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/RingtoneModel.kt b/src/com/android/deskclock/data/RingtoneModel.kt
new file mode 100644
index 0000000..42349d6
--- /dev/null
+++ b/src/com/android/deskclock/data/RingtoneModel.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import android.content.UriPermission
+import android.database.ContentObserver
+import android.database.Cursor
+import android.media.AudioManager.STREAM_ALARM
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.media.RingtoneManager.TITLE_COLUMN_INDEX
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.util.ArrayMap
+import android.util.ArraySet
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+
+/**
+ * All ringtone data is accessed via this model.
+ */
+internal class RingtoneModel(private val mContext: Context, private val mPrefs: SharedPreferences) {
+ /** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive. */
+ private val mRingtoneTitles: MutableMap<Uri, String?> = ArrayMap(16)
+
+ /** Clears data structures containing data that is locale-sensitive. */
+ private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+ /** A mutable copy of the custom ringtones. */
+ private var mCustomRingtones: MutableList<CustomRingtone>? = null
+
+ init {
+ // Clear caches affected by system settings when system settings change.
+ val cr: ContentResolver = mContext.getContentResolver()
+ val observer: ContentObserver = SystemAlarmAlertChangeObserver()
+ cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer)
+
+ // Clear caches affected by locale when locale changes.
+ val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+ mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+ }
+
+ fun addCustomRingtone(uri: Uri, title: String?): CustomRingtone? {
+ // If the uri is already present in an existing ringtone, do nothing.
+ val existing = getCustomRingtone(uri)
+ if (existing != null) {
+ return existing
+ }
+
+ val ringtone = CustomRingtoneDAO.addCustomRingtone(mPrefs, uri, title)
+ mutableCustomRingtones.add(ringtone)
+ mutableCustomRingtones.sort()
+ return ringtone
+ }
+
+ fun removeCustomRingtone(uri: Uri) {
+ val ringtones = mutableCustomRingtones
+ for (ringtone in ringtones) {
+ if (ringtone.uri.equals(uri)) {
+ CustomRingtoneDAO.removeCustomRingtone(mPrefs, ringtone.id)
+ ringtones.remove(ringtone)
+ break
+ }
+ }
+ }
+
+ private fun getCustomRingtone(uri: Uri): CustomRingtone? {
+ for (ringtone in mutableCustomRingtones) {
+ if (ringtone.uri.equals(uri)) {
+ return ringtone
+ }
+ }
+
+ return null
+ }
+
+ val customRingtones: List<CustomRingtone>
+ get() = mutableCustomRingtones
+
+ @SuppressLint("NewApi")
+ fun loadRingtonePermissions() {
+ val ringtones = mutableCustomRingtones
+ if (ringtones.isEmpty()) {
+ return
+ }
+
+ val uriPermissions: List<UriPermission> =
+ mContext.getContentResolver().getPersistedUriPermissions()
+ val permissions: MutableSet<Uri?> = ArraySet(uriPermissions.size)
+ for (uriPermission in uriPermissions) {
+ permissions.add(uriPermission.getUri())
+ }
+
+ val i = ringtones.listIterator()
+ while (i.hasNext()) {
+ val ringtone = i.next()
+ i.set(ringtone.setHasPermissions(permissions.contains(ringtone.uri)))
+ }
+ }
+
+ fun loadRingtoneTitles() {
+ // Early return if the cache is already primed.
+ if (mRingtoneTitles.isNotEmpty()) {
+ return
+ }
+
+ val ringtoneManager = RingtoneManager(mContext)
+ ringtoneManager.setType(STREAM_ALARM)
+
+ // Cache a title for each system ringtone.
+ try {
+ val cursor: Cursor? = ringtoneManager.getCursor()
+ cursor?.let {
+ cursor.moveToFirst()
+ while (!cursor.isAfterLast()) {
+ val ringtoneTitle: String = cursor.getString(TITLE_COLUMN_INDEX)
+ val ringtoneUri: Uri = ringtoneManager.getRingtoneUri(cursor.getPosition())
+ mRingtoneTitles[ringtoneUri] = ringtoneTitle
+ cursor.moveToNext()
+ }
+ }
+ } catch (ignored: Throwable) {
+ // best attempt only
+ LogUtils.e("Error loading ringtone title cache", ignored)
+ }
+ }
+
+ fun getRingtoneTitle(uri: Uri): String? {
+ // Special case: no ringtone has a title of "Silent".
+ if (AlarmSettingColumns.NO_RINGTONE_URI.equals(uri)) {
+ return mContext.getString(R.string.silent_ringtone_title)
+ }
+
+ // If the ringtone is custom, it has its own title.
+ val customRingtone = getCustomRingtone(uri)
+ if (customRingtone != null) {
+ return customRingtone.title
+ }
+
+ // Check the cache.
+ var title = mRingtoneTitles[uri]
+
+ if (title == null) {
+ // This is slow because a media player is created during Ringtone object creation.
+ val ringtone: Ringtone? = RingtoneManager.getRingtone(mContext, uri)
+ if (ringtone == null) {
+ LogUtils.e("No ringtone for uri: %s", uri)
+ return mContext.getString(R.string.unknown_ringtone_title)
+ }
+
+ // Cache the title for later use.
+ title = ringtone.getTitle(mContext)
+ mRingtoneTitles[uri] = title
+ }
+ return title
+ }
+
+ private val mutableCustomRingtones: MutableList<CustomRingtone>
+ get() {
+ if (mCustomRingtones == null) {
+ mCustomRingtones = CustomRingtoneDAO.getCustomRingtones(mPrefs)
+ mCustomRingtones!!.sort()
+ }
+
+ return mCustomRingtones!!
+ }
+
+ /**
+ * This receiver is notified when system settings change. Cached information built on
+ * those system settings must be cleared.
+ */
+ private inner class SystemAlarmAlertChangeObserver
+ : ContentObserver(Handler(Looper.myLooper()!!)) {
+ override fun onChange(selfChange: Boolean) {
+ super.onChange(selfChange)
+
+ // Titles such as "Default ringtone (Oxygen)" are wrong after default ringtone changes.
+ mRingtoneTitles.clear()
+ }
+ }
+
+ /**
+ * Cached information that is locale-sensitive must be cleared in response to locale changes.
+ */
+ private inner class LocaleChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ // Titles such as "Default ringtone (Oxygen)" are wrong after locale changes.
+ mRingtoneTitles.clear()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SettingsDAO.java b/src/com/android/deskclock/data/SettingsDAO.java
deleted file mode 100644
index 78e8e1a..0000000
--- a/src/com/android/deskclock/data/SettingsDAO.java
+++ /dev/null
@@ -1,387 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.provider.Settings;
-import androidx.annotation.NonNull;
-import android.text.format.DateUtils;
-
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
-import com.android.deskclock.data.DataModel.CitySort;
-import com.android.deskclock.data.DataModel.ClockStyle;
-import com.android.deskclock.settings.ScreensaverSettingsActivity;
-import com.android.deskclock.settings.SettingsActivity;
-
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Locale;
-import java.util.TimeZone;
-
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.DISMISS;
-import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.NOTHING;
-import static com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior.SNOOZE;
-import static com.android.deskclock.data.Weekdays.Order.MON_TO_SUN;
-import static com.android.deskclock.data.Weekdays.Order.SAT_TO_FRI;
-import static com.android.deskclock.data.Weekdays.Order.SUN_TO_SAT;
-import static java.util.Calendar.MONDAY;
-import static java.util.Calendar.SATURDAY;
-import static java.util.Calendar.SUNDAY;
-
-/**
- * This class encapsulates the storage of application preferences in {@link SharedPreferences}.
- */
-final class SettingsDAO {
-
- /** Key to a preference that stores the preferred sort order of world cities. */
- private static final String KEY_SORT_PREFERENCE = "sort_preference";
-
- /** Key to a preference that stores the default ringtone for new alarms. */
- private static final String KEY_DEFAULT_ALARM_RINGTONE_URI = "default_alarm_ringtone_uri";
-
- /** Key to a preference that stores the global broadcast id. */
- private static final String KEY_ALARM_GLOBAL_ID = "intent.extra.alarm.global.id";
-
- /** Key to a preference that indicates whether restore (of backup and restore) has completed. */
- private static final String KEY_RESTORE_BACKUP_FINISHED = "restore_finished";
-
- private SettingsDAO() {}
-
- /**
- * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
- */
- static int getGlobalIntentId(SharedPreferences prefs) {
- return prefs.getInt(KEY_ALARM_GLOBAL_ID, -1);
- }
-
- /**
- * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
- */
- static void updateGlobalIntentId(SharedPreferences prefs) {
- final int globalId = prefs.getInt(KEY_ALARM_GLOBAL_ID, -1) + 1;
- prefs.edit().putInt(KEY_ALARM_GLOBAL_ID, globalId).apply();
- }
-
- /**
- * @return an enumerated value indicating the order in which cities are ordered
- */
- static CitySort getCitySort(SharedPreferences prefs) {
- final int defaultSortOrdinal = CitySort.NAME.ordinal();
- final int citySortOrdinal = prefs.getInt(KEY_SORT_PREFERENCE, defaultSortOrdinal);
- return CitySort.values()[citySortOrdinal];
- }
-
- /**
- * Adjust the sort order of cities.
- */
- static void toggleCitySort(SharedPreferences prefs) {
- final CitySort oldSort = getCitySort(prefs);
- final CitySort newSort = oldSort == CitySort.NAME ? CitySort.UTC_OFFSET : CitySort.NAME;
- prefs.edit().putInt(KEY_SORT_PREFERENCE, newSort.ordinal()).apply();
- }
-
- /**
- * @return {@code true} if a clock for the user's home timezone should be automatically
- * displayed when it doesn't match the current timezone
- */
- static boolean getAutoShowHomeClock(SharedPreferences prefs) {
- return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, true);
- }
-
- /**
- * @return the user's home timezone
- */
- static TimeZone getHomeTimeZone(Context context, SharedPreferences prefs, TimeZone defaultTZ) {
- String timeZoneId = prefs.getString(SettingsActivity.KEY_HOME_TZ, null);
-
- // If the recorded home timezone is legal, use it.
- final TimeZones timeZones = getTimeZones(context, System.currentTimeMillis());
- if (timeZones.contains(timeZoneId)) {
- return TimeZone.getTimeZone(timeZoneId);
- }
-
- // No legal home timezone has yet been recorded, attempt to record the default.
- timeZoneId = defaultTZ.getID();
- if (timeZones.contains(timeZoneId)) {
- prefs.edit().putString(SettingsActivity.KEY_HOME_TZ, timeZoneId).apply();
- }
-
- // The timezone returned here may be valid or invalid. When it matches TimeZone.getDefault()
- // the Home city will not show, regardless of its validity.
- return defaultTZ;
- }
-
- /**
- * @return a value indicating whether analog or digital clocks are displayed in the app
- */
- static ClockStyle getClockStyle(Context context, SharedPreferences prefs) {
- return getClockStyle(context, prefs, SettingsActivity.KEY_CLOCK_STYLE);
- }
-
- /**
- * @return a value indicating whether analog or digital clocks are displayed in the app
- */
- static boolean getDisplayClockSeconds(SharedPreferences prefs) {
- return prefs.getBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, false);
- }
-
- /**
- * @param displaySeconds whether or not to display seconds on main clock
- */
- static void setDisplayClockSeconds(SharedPreferences prefs, boolean displaySeconds) {
- prefs.edit().putBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, displaySeconds).apply();
- }
-
- /**
- * Sets the user's display seconds preference based on the currently selected clock if one has
- * not yet been manually chosen.
- */
- static void setDefaultDisplayClockSeconds(Context context, SharedPreferences prefs) {
- if (!prefs.contains(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS)) {
- // If on analog clock style on upgrade, default to true. Otherwise, default to false.
- final boolean isAnalog = getClockStyle(context, prefs) == ClockStyle.ANALOG;
- setDisplayClockSeconds(prefs, isAnalog);
- }
- }
-
- /**
- * @return a value indicating whether analog or digital clocks are displayed on the screensaver
- */
- static ClockStyle getScreensaverClockStyle(Context context, SharedPreferences prefs) {
- return getClockStyle(context, prefs, ScreensaverSettingsActivity.KEY_CLOCK_STYLE);
- }
-
- /**
- * @return {@code true} if the screen saver should be dimmed for lower contrast at night
- */
- static boolean getScreensaverNightModeOn(SharedPreferences prefs) {
- return prefs.getBoolean(ScreensaverSettingsActivity.KEY_NIGHT_MODE, false);
- }
-
- /**
- * @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
- * has yet been made
- */
- static Uri getTimerRingtoneUri(SharedPreferences prefs, Uri defaultUri) {
- final String uriString = prefs.getString(SettingsActivity.KEY_TIMER_RINGTONE, null);
- return uriString == null ? defaultUri : Uri.parse(uriString);
- }
-
- /**
- * @return whether timer vibration is enabled. false by default.
- */
- static boolean getTimerVibrate(SharedPreferences prefs) {
- return prefs.getBoolean(SettingsActivity.KEY_TIMER_VIBRATE, false);
- }
-
- /**
- * @param enabled whether vibration will be turned on for all timers.
- */
- static void setTimerVibrate(SharedPreferences prefs, boolean enabled) {
- prefs.edit().putBoolean(SettingsActivity.KEY_TIMER_VIBRATE, enabled).apply();
- }
-
- /**
- * @param uri the uri of the ringtone to play for all timers
- */
- static void setTimerRingtoneUri(SharedPreferences prefs, Uri uri) {
- prefs.edit().putString(SettingsActivity.KEY_TIMER_RINGTONE, uri.toString()).apply();
- }
-
- /**
- * @return the uri of the selected ringtone or the {@code defaultUri} if no explicit selection
- * has yet been made
- */
- static Uri getDefaultAlarmRingtoneUri(SharedPreferences prefs) {
- final String uriString = prefs.getString(KEY_DEFAULT_ALARM_RINGTONE_URI, null);
- return uriString == null ? Settings.System.DEFAULT_ALARM_ALERT_URI : Uri.parse(uriString);
- }
-
- /**
- * @param uri identifies the default ringtone to play for new alarms
- */
- static void setDefaultAlarmRingtoneUri(SharedPreferences prefs, Uri uri) {
- prefs.edit().putString(KEY_DEFAULT_ALARM_RINGTONE_URI, uri.toString()).apply();
- }
-
- /**
- * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
- * {@code 0} implies no crescendo should be applied
- */
- static long getAlarmCrescendoDuration(SharedPreferences prefs) {
- final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_ALARM_CRESCENDO, "0");
- return Integer.parseInt(crescendoSeconds) * DateUtils.SECOND_IN_MILLIS;
- }
-
- /**
- * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
- * {@code 0} implies no crescendo should be applied
- */
- static long getTimerCrescendoDuration(SharedPreferences prefs) {
- final String crescendoSeconds = prefs.getString(SettingsActivity.KEY_TIMER_CRESCENDO, "0");
- return Integer.parseInt(crescendoSeconds) * DateUtils.SECOND_IN_MILLIS;
- }
-
- /**
- * @return the display order of the weekdays, which can start with {@link Calendar#SATURDAY},
- * {@link Calendar#SUNDAY} or {@link Calendar#MONDAY}
- */
- static Weekdays.Order getWeekdayOrder(SharedPreferences prefs) {
- final String defaultValue = String.valueOf(Calendar.getInstance().getFirstDayOfWeek());
- final String value = prefs.getString(SettingsActivity.KEY_WEEK_START, defaultValue);
- final int firstCalendarDay = Integer.parseInt(value);
- switch (firstCalendarDay) {
- case SATURDAY: return SAT_TO_FRI;
- case SUNDAY: return SUN_TO_SAT;
- case MONDAY: return MON_TO_SUN;
- default:
- throw new IllegalArgumentException("Unknown weekday: " + firstCalendarDay);
- }
- }
-
- /**
- * @return {@code true} if the restore process (of backup and restore) has completed
- */
- static boolean isRestoreBackupFinished(SharedPreferences prefs) {
- return prefs.getBoolean(KEY_RESTORE_BACKUP_FINISHED, false);
- }
-
- /**
- * @param finished {@code true} means the restore process (of backup and restore) has completed
- */
- static void setRestoreBackupFinished(SharedPreferences prefs, boolean finished) {
- if (finished) {
- prefs.edit().putBoolean(KEY_RESTORE_BACKUP_FINISHED, true).apply();
- } else {
- prefs.edit().remove(KEY_RESTORE_BACKUP_FINISHED).apply();
- }
- }
-
- /**
- * @return the behavior to execute when volume buttons are pressed while firing an alarm
- */
- static AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior(SharedPreferences prefs) {
- final String defaultValue = SettingsActivity.DEFAULT_VOLUME_BEHAVIOR;
- final String value = prefs.getString(SettingsActivity.KEY_VOLUME_BUTTONS, defaultValue);
- switch (value) {
- case SettingsActivity.DEFAULT_VOLUME_BEHAVIOR: return NOTHING;
- case SettingsActivity.VOLUME_BEHAVIOR_SNOOZE: return SNOOZE;
- case SettingsActivity.VOLUME_BEHAVIOR_DISMISS: return DISMISS;
- default:
- throw new IllegalArgumentException("Unknown volume button behavior: " + value);
- }
- }
-
- /**
- * @return the number of minutes an alarm may ring before it has timed out and becomes missed
- */
- static int getAlarmTimeout(SharedPreferences prefs) {
- // Default value must match the one in res/xml/settings.xml
- final String string = prefs.getString(SettingsActivity.KEY_AUTO_SILENCE, "10");
- return Integer.parseInt(string);
- }
-
- /**
- * @return the number of minutes an alarm will remain snoozed before it rings again
- */
- static int getSnoozeLength(SharedPreferences prefs) {
- // Default value must match the one in res/xml/settings.xml
- final String string = prefs.getString(SettingsActivity.KEY_ALARM_SNOOZE, "10");
- return Integer.parseInt(string);
- }
-
- /**
- * @param currentTime timezone offsets created relative to this time
- * @return a description of the time zones available for selection
- */
- static TimeZones getTimeZones(Context context, long currentTime) {
- final Locale locale = Locale.getDefault();
- final Resources resources = context.getResources();
- final String[] timeZoneIds = resources.getStringArray(R.array.timezone_values);
- final String[] timeZoneNames = resources.getStringArray(R.array.timezone_labels);
-
- // Verify the data is consistent.
- if (timeZoneIds.length != timeZoneNames.length) {
- final String message = String.format(Locale.US,
- "id count (%d) does not match name count (%d) for locale %s",
- timeZoneIds.length, timeZoneNames.length, locale);
- throw new IllegalStateException(message);
- }
-
- // Create TimeZoneDescriptors for each TimeZone so they can be sorted.
- final TimeZoneDescriptor[] descriptors = new TimeZoneDescriptor[timeZoneIds.length];
- for (int i = 0; i < timeZoneIds.length; i++) {
- final String id = timeZoneIds[i];
- final String name = timeZoneNames[i].replaceAll("\"", "");
- descriptors[i] = new TimeZoneDescriptor(locale, id, name, currentTime);
- }
- Arrays.sort(descriptors);
-
- // Transfer the TimeZoneDescriptors into parallel arrays for easy consumption by the caller.
- final CharSequence[] tzIds = new CharSequence[descriptors.length];
- final CharSequence[] tzNames = new CharSequence[descriptors.length];
- for (int i = 0; i < descriptors.length; i++) {
- final TimeZoneDescriptor descriptor = descriptors[i];
- tzIds[i] = descriptor.mTimeZoneId;
- tzNames[i] = descriptor.mTimeZoneName;
- }
-
- return new TimeZones(tzIds, tzNames);
- }
-
- private static ClockStyle getClockStyle(Context context, SharedPreferences prefs, String key) {
- final String defaultStyle = context.getString(R.string.default_clock_style);
- final String clockStyle = prefs.getString(key, defaultStyle);
- // Use hardcoded locale to perform toUpperCase, because in some languages toUpperCase adds
- // accent to character, which breaks the enum conversion.
- return ClockStyle.valueOf(clockStyle.toUpperCase(Locale.US));
- }
-
- /**
- * These descriptors have a natural order from furthest ahead of GMT to furthest behind GMT.
- */
- private static class TimeZoneDescriptor implements Comparable<TimeZoneDescriptor> {
-
- private final int mOffset;
- private final String mTimeZoneId;
- private final String mTimeZoneName;
-
- private TimeZoneDescriptor(Locale locale, String id, String name, long currentTime) {
- mTimeZoneId = id;
-
- final TimeZone tz = TimeZone.getTimeZone(id);
- mOffset = tz.getOffset(currentTime);
-
- final char sign = mOffset < 0 ? '-' : '+';
- final int absoluteGMTOffset = Math.abs(mOffset);
- final long hour = absoluteGMTOffset / HOUR_IN_MILLIS;
- final long minute = (absoluteGMTOffset / MINUTE_IN_MILLIS) % 60;
- mTimeZoneName = String.format(locale, "(GMT%s%d:%02d) %s", sign, hour, minute, name);
- }
-
- @Override
- public int compareTo(@NonNull TimeZoneDescriptor other) {
- return mOffset - other.mOffset;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SettingsDAO.kt b/src/com/android/deskclock/data/SettingsDAO.kt
new file mode 100644
index 0000000..43e77b5
--- /dev/null
+++ b/src/com/android/deskclock/data/SettingsDAO.kt
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Resources
+import android.net.Uri
+import android.provider.Settings
+import android.text.format.DateUtils
+import android.text.format.DateUtils.HOUR_IN_MILLIS
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior
+import com.android.deskclock.data.DataModel.CitySort
+import com.android.deskclock.data.DataModel.ClockStyle
+import com.android.deskclock.data.Weekdays.Order
+import com.android.deskclock.settings.ScreensaverSettingsActivity
+import com.android.deskclock.settings.SettingsActivity
+
+import java.util.Arrays
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+import kotlin.math.abs
+
+/**
+ * This class encapsulates the storage of application preferences in [SharedPreferences].
+ */
+internal object SettingsDAO {
+ /** Key to a preference that stores the preferred sort order of world cities. */
+ private const val KEY_SORT_PREFERENCE = "sort_preference"
+
+ /** Key to a preference that stores the default ringtone for new alarms. */
+ private const val KEY_DEFAULT_ALARM_RINGTONE_URI = "default_alarm_ringtone_uri"
+
+ /** Key to a preference that stores the global broadcast id. */
+ private const val KEY_ALARM_GLOBAL_ID = "intent.extra.alarm.global.id"
+
+ /** Key to a preference that indicates whether restore (of backup and restore) has completed. */
+ private const val KEY_RESTORE_BACKUP_FINISHED = "restore_finished"
+
+ /**
+ * @return the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ fun getGlobalIntentId(prefs: SharedPreferences): Int {
+ return prefs.getInt(KEY_ALARM_GLOBAL_ID, -1)
+ }
+
+ /**
+ * Update the id used to discriminate relevant AlarmManager callbacks from defunct ones
+ */
+ fun updateGlobalIntentId(prefs: SharedPreferences) {
+ val globalId: Int = prefs.getInt(KEY_ALARM_GLOBAL_ID, -1) + 1
+ prefs.edit().putInt(KEY_ALARM_GLOBAL_ID, globalId).apply()
+ }
+
+ /**
+ * @return an enumerated value indicating the order in which cities are ordered
+ */
+ fun getCitySort(prefs: SharedPreferences): CitySort {
+ val defaultSortOrdinal = CitySort.NAME.ordinal
+ val citySortOrdinal: Int = prefs.getInt(KEY_SORT_PREFERENCE, defaultSortOrdinal)
+ return CitySort.values()[citySortOrdinal]
+ }
+
+ /**
+ * Adjust the sort order of cities.
+ */
+ fun toggleCitySort(prefs: SharedPreferences) {
+ val oldSort = getCitySort(prefs)
+ val newSort = if (oldSort == CitySort.NAME) CitySort.UTC_OFFSET else CitySort.NAME
+ prefs.edit().putInt(KEY_SORT_PREFERENCE, newSort.ordinal).apply()
+ }
+
+ /**
+ * @return `true` if a clock for the user's home timezone should be automatically
+ * displayed when it doesn't match the current timezone
+ */
+ fun getAutoShowHomeClock(prefs: SharedPreferences): Boolean {
+ return prefs.getBoolean(SettingsActivity.KEY_AUTO_HOME_CLOCK, true)
+ }
+
+ /**
+ * @return the user's home timezone
+ */
+ fun getHomeTimeZone(context: Context, prefs: SharedPreferences, defaultTZ: TimeZone): TimeZone {
+ var timeZoneId: String? = prefs.getString(SettingsActivity.KEY_HOME_TZ, null)
+
+ // If the recorded home timezone is legal, use it.
+ val timeZones = getTimeZones(context, System.currentTimeMillis())
+ if (timeZones.contains(timeZoneId)) {
+ return TimeZone.getTimeZone(timeZoneId)
+ }
+
+ // No legal home timezone has yet been recorded, attempt to record the default.
+ timeZoneId = defaultTZ.id
+ if (timeZones.contains(timeZoneId)) {
+ prefs.edit().putString(SettingsActivity.KEY_HOME_TZ, timeZoneId).apply()
+ }
+
+ // The timezone returned here may be valid or invalid. When it matches TimeZone.getDefault()
+ // the Home city will not show, regardless of its validity.
+ return defaultTZ
+ }
+
+ /**
+ * @return a value indicating whether analog or digital clocks are displayed in the app
+ */
+ fun getClockStyle(context: Context, prefs: SharedPreferences): ClockStyle {
+ return getClockStyle(context, prefs, SettingsActivity.KEY_CLOCK_STYLE)
+ }
+
+ /**
+ * @return a value indicating whether analog or digital clocks are displayed in the app
+ */
+ fun getDisplayClockSeconds(prefs: SharedPreferences): Boolean {
+ return prefs.getBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, false)
+ }
+
+ /**
+ * @param displaySeconds whether or not to display seconds on main clock
+ */
+ fun setDisplayClockSeconds(prefs: SharedPreferences, displaySeconds: Boolean) {
+ prefs.edit().putBoolean(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS, displaySeconds).apply()
+ }
+
+ /**
+ * Sets the user's display seconds preference based on the currently selected clock if one has
+ * not yet been manually chosen.
+ */
+ fun setDefaultDisplayClockSeconds(context: Context, prefs: SharedPreferences) {
+ if (!prefs.contains(SettingsActivity.KEY_CLOCK_DISPLAY_SECONDS)) {
+ // If on analog clock style on upgrade, default to true. Otherwise, default to false.
+ val isAnalog = getClockStyle(context, prefs) == ClockStyle.ANALOG
+ setDisplayClockSeconds(prefs, isAnalog)
+ }
+ }
+
+ /**
+ * @return a value indicating whether analog or digital clocks are displayed on the screensaver
+ */
+ fun getScreensaverClockStyle(context: Context, prefs: SharedPreferences): ClockStyle {
+ return getClockStyle(context, prefs, ScreensaverSettingsActivity.KEY_CLOCK_STYLE)
+ }
+
+ /**
+ * @return `true` if the screen saver should be dimmed for lower contrast at night
+ */
+ fun getScreensaverNightModeOn(prefs: SharedPreferences): Boolean {
+ return prefs.getBoolean(ScreensaverSettingsActivity.KEY_NIGHT_MODE, false)
+ }
+
+ /**
+ * @return the uri of the selected ringtone or the `defaultUri` if no explicit selection
+ * has yet been made
+ */
+ fun getTimerRingtoneUri(prefs: SharedPreferences, defaultUri: Uri): Uri {
+ val uriString: String? = prefs.getString(SettingsActivity.KEY_TIMER_RINGTONE, null)
+ return if (uriString == null) defaultUri else Uri.parse(uriString)
+ }
+
+ /**
+ * @return whether timer vibration is enabled. false by default.
+ */
+ fun getTimerVibrate(prefs: SharedPreferences): Boolean {
+ return prefs.getBoolean(SettingsActivity.KEY_TIMER_VIBRATE, false)
+ }
+
+ /**
+ * @param enabled whether vibration will be turned on for all timers.
+ */
+ fun setTimerVibrate(prefs: SharedPreferences, enabled: Boolean) {
+ prefs.edit().putBoolean(SettingsActivity.KEY_TIMER_VIBRATE, enabled).apply()
+ }
+
+ /**
+ * @param uri the uri of the ringtone to play for all timers
+ */
+ fun setTimerRingtoneUri(prefs: SharedPreferences, uri: Uri) {
+ prefs.edit().putString(SettingsActivity.KEY_TIMER_RINGTONE, uri.toString()).apply()
+ }
+
+ /**
+ * @return the uri of the selected ringtone or the `defaultUri` if no explicit selection
+ * has yet been made
+ */
+ fun getDefaultAlarmRingtoneUri(prefs: SharedPreferences): Uri {
+ val uriString: String? = prefs.getString(KEY_DEFAULT_ALARM_RINGTONE_URI, null)
+ return if (uriString == null) {
+ Settings.System.DEFAULT_ALARM_ALERT_URI
+ } else {
+ Uri.parse(uriString)
+ }
+ }
+
+ /**
+ * @param uri identifies the default ringtone to play for new alarms
+ */
+ fun setDefaultAlarmRingtoneUri(prefs: SharedPreferences, uri: Uri) {
+ prefs.edit().putString(KEY_DEFAULT_ALARM_RINGTONE_URI, uri.toString()).apply()
+ }
+
+ /**
+ * @return the duration, in milliseconds, of the crescendo to apply to alarm ringtone playback;
+ * `0` implies no crescendo should be applied
+ */
+ fun getAlarmCrescendoDuration(prefs: SharedPreferences): Long {
+ val crescendoSeconds: String = prefs.getString(SettingsActivity.KEY_ALARM_CRESCENDO, "0")!!
+ return crescendoSeconds.toInt() * DateUtils.SECOND_IN_MILLIS
+ }
+
+ /**
+ * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+ * `0` implies no crescendo should be applied
+ */
+ fun getTimerCrescendoDuration(prefs: SharedPreferences): Long {
+ val crescendoSeconds: String = prefs.getString(SettingsActivity.KEY_TIMER_CRESCENDO, "0")!!
+ return crescendoSeconds.toInt() * DateUtils.SECOND_IN_MILLIS
+ }
+
+ /**
+ * @return the display order of the weekdays, which can start with [Calendar.SATURDAY],
+ * [Calendar.SUNDAY] or [Calendar.MONDAY]
+ */
+ fun getWeekdayOrder(prefs: SharedPreferences): Order {
+ val defaultValue = Calendar.getInstance().firstDayOfWeek.toString()
+ val value: String = prefs.getString(SettingsActivity.KEY_WEEK_START, defaultValue)!!
+ return when (val firstCalendarDay = value.toInt()) {
+ Calendar.SATURDAY -> Order.SAT_TO_FRI
+ Calendar.SUNDAY -> Order.SUN_TO_SAT
+ Calendar.MONDAY -> Order.MON_TO_SUN
+ else -> throw IllegalArgumentException("Unknown weekday: $firstCalendarDay")
+ }
+ }
+
+ /**
+ * @return `true` if the restore process (of backup and restore) has completed
+ */
+ fun isRestoreBackupFinished(prefs: SharedPreferences): Boolean {
+ return prefs.getBoolean(KEY_RESTORE_BACKUP_FINISHED, false)
+ }
+
+ /**
+ * @param finished `true` means the restore process (of backup and restore) has completed
+ */
+ fun setRestoreBackupFinished(prefs: SharedPreferences, finished: Boolean) {
+ if (finished) {
+ prefs.edit().putBoolean(KEY_RESTORE_BACKUP_FINISHED, true).apply()
+ } else {
+ prefs.edit().remove(KEY_RESTORE_BACKUP_FINISHED).apply()
+ }
+ }
+
+ /**
+ * @return the behavior to execute when volume buttons are pressed while firing an alarm
+ */
+ fun getAlarmVolumeButtonBehavior(prefs: SharedPreferences): AlarmVolumeButtonBehavior {
+ val defaultValue = SettingsActivity.DEFAULT_VOLUME_BEHAVIOR
+ val value: String = prefs.getString(SettingsActivity.KEY_VOLUME_BUTTONS, defaultValue)!!
+ return when (value) {
+ SettingsActivity.DEFAULT_VOLUME_BEHAVIOR -> AlarmVolumeButtonBehavior.NOTHING
+ SettingsActivity.VOLUME_BEHAVIOR_SNOOZE -> AlarmVolumeButtonBehavior.SNOOZE
+ SettingsActivity.VOLUME_BEHAVIOR_DISMISS -> AlarmVolumeButtonBehavior.DISMISS
+ else -> throw IllegalArgumentException("Unknown volume button behavior: $value")
+ }
+ }
+
+ /**
+ * @return the number of minutes an alarm may ring before it has timed out and becomes missed
+ */
+ fun getAlarmTimeout(prefs: SharedPreferences): Int {
+ // Default value must match the one in res/xml/settings.xml
+ val string: String = prefs.getString(SettingsActivity.KEY_AUTO_SILENCE, "10")!!
+ return string.toInt()
+ }
+
+ /**
+ * @return the number of minutes an alarm will remain snoozed before it rings again
+ */
+ fun getSnoozeLength(prefs: SharedPreferences): Int {
+ // Default value must match the one in res/xml/settings.xml
+ val string: String = prefs.getString(SettingsActivity.KEY_ALARM_SNOOZE, "10")!!
+ return string.toInt()
+ }
+
+ /**
+ * @param currentTime timezone offsets created relative to this time
+ * @return a description of the time zones available for selection
+ */
+ fun getTimeZones(context: Context, currentTime: Long): TimeZones {
+ val locale = Locale.getDefault()
+ val resources: Resources = context.getResources()
+ val timeZoneIds: Array<String> = resources.getStringArray(R.array.timezone_values)
+ val timeZoneNames: Array<String> = resources.getStringArray(R.array.timezone_labels)
+
+ // Verify the data is consistent.
+ if (timeZoneIds.size != timeZoneNames.size) {
+ val message = String.format(Locale.US,
+ "id count (%d) does not match name count (%d) for locale %s",
+ timeZoneIds.size, timeZoneNames.size, locale)
+ throw IllegalStateException(message)
+ }
+
+ // Create TimeZoneDescriptors for each TimeZone so they can be sorted.
+ val descriptors = arrayOfNulls<TimeZoneDescriptor>(timeZoneIds.size)
+ for (i in timeZoneIds.indices) {
+ val id = timeZoneIds[i]
+ val name = timeZoneNames[i].replace("\"".toRegex(), "")
+ descriptors[i] = TimeZoneDescriptor(locale, id, name, currentTime)
+ }
+ Arrays.sort(descriptors)
+
+ // Transfer the TimeZoneDescriptors into parallel arrays for easy consumption by the caller.
+ val tzIds = arrayOfNulls<CharSequence>(descriptors.size)
+ val tzNames = arrayOfNulls<CharSequence>(descriptors.size)
+ for (i in descriptors.indices) {
+ val descriptor = descriptors[i]
+ tzIds[i] = descriptor!!.mTimeZoneId
+ tzNames[i] = descriptor.mTimeZoneName
+ }
+
+ return TimeZones(tzIds.requireNoNulls(), tzNames.requireNoNulls())
+ }
+
+ private fun getClockStyle(context: Context, prefs: SharedPreferences, key: String): ClockStyle {
+ val defaultStyle: String = context.getString(R.string.default_clock_style)
+ val clockStyle: String = prefs.getString(key, defaultStyle)!!
+ // Use hardcoded locale to perform toUpperCase, because in some languages toUpperCase adds
+ // accent to character, which breaks the enum conversion.
+ return ClockStyle.valueOf(clockStyle.toUpperCase(Locale.US))
+ }
+
+ /**
+ * These descriptors have a natural order from furthest ahead of GMT to furthest behind GMT.
+ */
+ private class TimeZoneDescriptor(
+ locale: Locale,
+ val mTimeZoneId: String,
+ name: String,
+ currentTime: Long
+ ) : Comparable<TimeZoneDescriptor> {
+ private val mOffset: Int
+ val mTimeZoneName: String
+
+ init {
+ val tz = TimeZone.getTimeZone(mTimeZoneId)
+ mOffset = tz.getOffset(currentTime)
+
+ val sign = if (mOffset < 0) '-' else '+'
+ val absoluteGMTOffset = abs(mOffset)
+ val hour: Long = absoluteGMTOffset / HOUR_IN_MILLIS
+ val minute: Long = absoluteGMTOffset / MINUTE_IN_MILLIS % 60
+ mTimeZoneName = String.format(locale, "(GMT%s%d:%02d) %s", sign, hour, minute, name)
+ }
+
+ override fun compareTo(other: TimeZoneDescriptor): Int {
+ return mOffset - other.mOffset
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SettingsModel.java b/src/com/android/deskclock/data/SettingsModel.java
deleted file mode 100644
index 103c210..0000000
--- a/src/com/android/deskclock/data/SettingsModel.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.net.Uri;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel.AlarmVolumeButtonBehavior;
-import com.android.deskclock.data.DataModel.CitySort;
-import com.android.deskclock.data.DataModel.ClockStyle;
-
-import java.util.TimeZone;
-
-/**
- * All settings data is accessed via this model.
- */
-final class SettingsModel {
-
- private final Context mContext;
-
- private final SharedPreferences mPrefs;
-
- /** The model from which time data are fetched. */
- private final TimeModel mTimeModel;
-
- /** The uri of the default ringtone to use for timers until the user explicitly chooses one. */
- private Uri mDefaultTimerRingtoneUri;
-
- SettingsModel(Context context, SharedPreferences prefs, TimeModel timeModel) {
- mContext = context;
- mPrefs = prefs;
- mTimeModel = timeModel;
-
- // Set the user's default display seconds preference if one has not yet been chosen.
- SettingsDAO.setDefaultDisplayClockSeconds(mContext, prefs);
- }
-
- int getGlobalIntentId() {
- return SettingsDAO.getGlobalIntentId(mPrefs);
- }
-
- void updateGlobalIntentId() {
- SettingsDAO.updateGlobalIntentId(mPrefs);
- }
-
- CitySort getCitySort() {
- return SettingsDAO.getCitySort(mPrefs);
- }
-
- void toggleCitySort() {
- SettingsDAO.toggleCitySort(mPrefs);
- }
-
- TimeZone getHomeTimeZone() {
- return SettingsDAO.getHomeTimeZone(mContext, mPrefs, TimeZone.getDefault());
- }
-
- ClockStyle getClockStyle() {
- return SettingsDAO.getClockStyle(mContext, mPrefs);
- }
-
- boolean getDisplayClockSeconds() {
- return SettingsDAO.getDisplayClockSeconds(mPrefs);
- }
-
- void setDisplayClockSeconds(boolean shouldDisplaySeconds) {
- SettingsDAO.setDisplayClockSeconds(mPrefs, shouldDisplaySeconds);
- }
-
- ClockStyle getScreensaverClockStyle() {
- return SettingsDAO.getScreensaverClockStyle(mContext, mPrefs);
- }
-
- boolean getScreensaverNightModeOn() {
- return SettingsDAO.getScreensaverNightModeOn(mPrefs);
- }
-
- boolean getShowHomeClock() {
- if (!SettingsDAO.getAutoShowHomeClock(mPrefs)) {
- return false;
- }
-
- // Show the home clock if the current time and home time differ.
- // (By using UTC offset for this comparison the various DST rules are considered)
- final TimeZone defaultTZ = TimeZone.getDefault();
- final TimeZone homeTimeZone = SettingsDAO.getHomeTimeZone(mContext, mPrefs, defaultTZ);
- final long now = System.currentTimeMillis();
- return homeTimeZone.getOffset(now) != defaultTZ.getOffset(now);
- }
-
- Uri getDefaultTimerRingtoneUri() {
- if (mDefaultTimerRingtoneUri == null) {
- mDefaultTimerRingtoneUri = Utils.getResourceUri(mContext, R.raw.timer_expire);
- }
-
- return mDefaultTimerRingtoneUri;
- }
-
- void setTimerRingtoneUri(Uri uri) {
- SettingsDAO.setTimerRingtoneUri(mPrefs, uri);
- }
-
- Uri getTimerRingtoneUri() {
- return SettingsDAO.getTimerRingtoneUri(mPrefs, getDefaultTimerRingtoneUri());
- }
-
- AlarmVolumeButtonBehavior getAlarmVolumeButtonBehavior() {
- return SettingsDAO.getAlarmVolumeButtonBehavior(mPrefs);
- }
-
- int getAlarmTimeout() {
- return SettingsDAO.getAlarmTimeout(mPrefs);
- }
-
- int getSnoozeLength() {
- return SettingsDAO.getSnoozeLength(mPrefs);
- }
-
- Uri getDefaultAlarmRingtoneUri() {
- return SettingsDAO.getDefaultAlarmRingtoneUri(mPrefs);
- }
-
- void setDefaultAlarmRingtoneUri(Uri uri) {
- SettingsDAO.setDefaultAlarmRingtoneUri(mPrefs, uri);
- }
-
- long getAlarmCrescendoDuration() {
- return SettingsDAO.getAlarmCrescendoDuration(mPrefs);
- }
-
- long getTimerCrescendoDuration() {
- return SettingsDAO.getTimerCrescendoDuration(mPrefs);
- }
-
- Weekdays.Order getWeekdayOrder() {
- return SettingsDAO.getWeekdayOrder(mPrefs);
- }
-
- boolean isRestoreBackupFinished() {
- return SettingsDAO.isRestoreBackupFinished(mPrefs);
- }
-
- void setRestoreBackupFinished(boolean finished) {
- SettingsDAO.setRestoreBackupFinished(mPrefs, finished);
- }
-
- boolean getTimerVibrate() {
- return SettingsDAO.getTimerVibrate(mPrefs);
- }
-
- void setTimerVibrate(boolean enabled) {
- SettingsDAO.setTimerVibrate(mPrefs, enabled);
- }
-
- TimeZones getTimeZones() {
- return SettingsDAO.getTimeZones(mContext, mTimeModel.currentTimeMillis());
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SettingsModel.kt b/src/com/android/deskclock/data/SettingsModel.kt
new file mode 100644
index 0000000..040c791
--- /dev/null
+++ b/src/com/android/deskclock/data/SettingsModel.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.net.Uri
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.util.TimeZone
+
+/**
+ * All settings data is accessed via this model.
+ */
+internal class SettingsModel(
+ private val mContext: Context,
+ private val mPrefs: SharedPreferences,
+ /** The model from which time data are fetched. */
+ private val mTimeModel: TimeModel
+) {
+
+ /** The uri of the default ringtone to use for timers until the user explicitly chooses one. */
+ private var mDefaultTimerRingtoneUri: Uri? = null
+
+ init {
+ // Set the user's default display seconds preference if one has not yet been chosen.
+ SettingsDAO.setDefaultDisplayClockSeconds(mContext, mPrefs)
+ }
+
+ val globalIntentId: Int
+ get() = SettingsDAO.getGlobalIntentId(mPrefs)
+
+ fun updateGlobalIntentId() {
+ SettingsDAO.updateGlobalIntentId(mPrefs)
+ }
+
+ val citySort: DataModel.CitySort
+ get() = SettingsDAO.getCitySort(mPrefs)
+
+ fun toggleCitySort() {
+ SettingsDAO.toggleCitySort(mPrefs)
+ }
+
+ val homeTimeZone: TimeZone
+ get() = SettingsDAO.getHomeTimeZone(mContext, mPrefs, TimeZone.getDefault())
+
+ val clockStyle: DataModel.ClockStyle
+ get() = SettingsDAO.getClockStyle(mContext, mPrefs)
+
+ var displayClockSeconds: Boolean
+ get() = SettingsDAO.getDisplayClockSeconds(mPrefs)
+ set(shouldDisplaySeconds) {
+ SettingsDAO.setDisplayClockSeconds(mPrefs, shouldDisplaySeconds)
+ }
+
+ val screensaverClockStyle: DataModel.ClockStyle
+ get() = SettingsDAO.getScreensaverClockStyle(mContext, mPrefs)
+
+ val screensaverNightModeOn: Boolean
+ get() = SettingsDAO.getScreensaverNightModeOn(mPrefs)
+
+ val showHomeClock: Boolean
+ get() {
+ if (!SettingsDAO.getAutoShowHomeClock(mPrefs)) {
+ return false
+ }
+
+ // Show the home clock if the current time and home time differ.
+ // (By using UTC offset for this comparison the various DST rules are considered)
+ val defaultTZ = TimeZone.getDefault()
+ val homeTimeZone = SettingsDAO.getHomeTimeZone(mContext, mPrefs, defaultTZ)
+ val now = System.currentTimeMillis()
+ return homeTimeZone.getOffset(now) != defaultTZ.getOffset(now)
+ }
+
+ val defaultTimerRingtoneUri: Uri
+ get() {
+ if (mDefaultTimerRingtoneUri == null) {
+ mDefaultTimerRingtoneUri = Utils.getResourceUri(mContext, R.raw.timer_expire)
+ }
+
+ return mDefaultTimerRingtoneUri!!
+ }
+
+ var timerRingtoneUri: Uri
+ get() = SettingsDAO.getTimerRingtoneUri(mPrefs, defaultTimerRingtoneUri)
+ set(uri) {
+ SettingsDAO.setTimerRingtoneUri(mPrefs, uri)
+ }
+
+ val alarmVolumeButtonBehavior: DataModel.AlarmVolumeButtonBehavior
+ get() = SettingsDAO.getAlarmVolumeButtonBehavior(mPrefs)
+
+ val alarmTimeout: Int
+ get() = SettingsDAO.getAlarmTimeout(mPrefs)
+
+ val snoozeLength: Int
+ get() = SettingsDAO.getSnoozeLength(mPrefs)
+
+ var defaultAlarmRingtoneUri: Uri
+ get() = SettingsDAO.getDefaultAlarmRingtoneUri(mPrefs)
+ set(uri) {
+ SettingsDAO.setDefaultAlarmRingtoneUri(mPrefs, uri)
+ }
+
+ val alarmCrescendoDuration: Long
+ get() = SettingsDAO.getAlarmCrescendoDuration(mPrefs)
+
+ val timerCrescendoDuration: Long
+ get() = SettingsDAO.getTimerCrescendoDuration(mPrefs)
+
+ val weekdayOrder: Weekdays.Order
+ get() = SettingsDAO.getWeekdayOrder(mPrefs)
+
+ var isRestoreBackupFinished: Boolean
+ get() = SettingsDAO.isRestoreBackupFinished(mPrefs)
+ set(finished) {
+ SettingsDAO.setRestoreBackupFinished(mPrefs, finished)
+ }
+
+ var timerVibrate: Boolean
+ get() = SettingsDAO.getTimerVibrate(mPrefs)
+ set(enabled) {
+ SettingsDAO.setTimerVibrate(mPrefs, enabled)
+ }
+
+ val timeZones: TimeZones
+ get() = SettingsDAO.getTimeZones(mContext, mTimeModel.currentTimeMillis())
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SilentSettingsModel.java b/src/com/android/deskclock/data/SilentSettingsModel.java
deleted file mode 100644
index b50702a..0000000
--- a/src/com/android/deskclock/data/SilentSettingsModel.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.annotation.TargetApi;
-import android.app.NotificationManager;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.media.AudioManager;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Handler;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel.SilentSetting;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED;
-import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
-import static android.content.Context.AUDIO_SERVICE;
-import static android.content.Context.NOTIFICATION_SERVICE;
-import static android.media.AudioManager.STREAM_ALARM;
-import static android.media.RingtoneManager.TYPE_ALARM;
-import static android.provider.Settings.System.CONTENT_URI;
-import static android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI;
-
-/**
- * This model fetches and stores reasons that alarms may be suppressed or silenced by system
- * settings on the device. This information is displayed passively to notify the user of this
- * condition and set their expectations for future firing alarms.
- */
-final class SilentSettingsModel {
-
- /** The Uri to the settings entry that stores alarm stream volume. */
- private static final Uri VOLUME_URI = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker");
-
- private final Context mContext;
-
- /** Used to query the alarm volume and display the system control to change the alarm volume. */
- private final AudioManager mAudioManager;
-
- /** Used to query the do-not-disturb setting value, also called "interruption filter". */
- private final NotificationManager mNotificationManager;
-
- /** Used to determine if the application is in the foreground. */
- private final NotificationModel mNotificationModel;
-
- /** List of listeners to invoke upon silence state change. */
- private final List<OnSilentSettingsListener> mListeners = new ArrayList<>(1);
-
- /**
- * The last setting known to be blocking alarms; {@code null} indicates no settings are
- * blocking the app or the app is not in the foreground.
- */
- private SilentSetting mSilentSetting;
-
- /** The background task that checks the device system settings that influence alarm firing. */
- private CheckSilenceSettingsTask mCheckSilenceSettingsTask;
-
- SilentSettingsModel(Context context, NotificationModel notificationModel) {
- mContext = context;
- mNotificationModel = notificationModel;
-
- mAudioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
- mNotificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
-
- // Watch for changes to the settings that may silence alarms.
- final ContentResolver cr = context.getContentResolver();
- final ContentObserver contentChangeWatcher = new ContentChangeWatcher();
- cr.registerContentObserver(VOLUME_URI, false, contentChangeWatcher);
- cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, contentChangeWatcher);
- if (Utils.isMOrLater()) {
- final IntentFilter filter = new IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED);
- context.registerReceiver(new DoNotDisturbChangeReceiver(), filter);
- }
- }
-
- void addSilentSettingsListener(OnSilentSettingsListener listener) {
- mListeners.add(listener);
- }
-
- void removeSilentSettingsListener(OnSilentSettingsListener listener) {
- mListeners.remove(listener);
- }
-
- /**
- * If the app is in the foreground, start a task to determine if any device setting will block
- * alarms from firing. If the app is in the background, clear any results from the last time
- * those settings were inspected.
- */
- void updateSilentState() {
- // Cancel any task in flight, the result is no longer relevant.
- if (mCheckSilenceSettingsTask != null) {
- mCheckSilenceSettingsTask.cancel(true);
- mCheckSilenceSettingsTask = null;
- }
-
- if (mNotificationModel.isApplicationInForeground()) {
- mCheckSilenceSettingsTask = new CheckSilenceSettingsTask();
- mCheckSilenceSettingsTask.execute();
- } else {
- setSilentState(null);
- }
- }
-
- /**
- * @param silentSetting the latest notion of which setting is suppressing alarms; {@code null}
- * if no settings are suppressing alarms
- */
- private void setSilentState(SilentSetting silentSetting) {
- if (mSilentSetting != silentSetting) {
- final SilentSetting oldReason = mSilentSetting;
- mSilentSetting = silentSetting;
-
- for (OnSilentSettingsListener listener : mListeners) {
- listener.onSilentSettingsChange(oldReason, silentSetting);
- }
- }
- }
-
- /**
- * This task inspects a variety of system settings that can prevent alarms from firing or the
- * associated ringtone from playing. If any of them would prevent an alarm from firing or
- * making noise, a description of the setting is reported to this model on the main thread.
- */
- private final class CheckSilenceSettingsTask extends AsyncTask<Void, Void, SilentSetting> {
- @Override
- protected SilentSetting doInBackground(Void... parameters) {
- if (!isCancelled() && isDoNotDisturbBlockingAlarms()) {
- return SilentSetting.DO_NOT_DISTURB;
- } else if (!isCancelled() && isAlarmStreamMuted()) {
- return SilentSetting.MUTED_VOLUME;
- } else if (!isCancelled() && isSystemAlarmRingtoneSilent()) {
- return SilentSetting.SILENT_RINGTONE;
- } else if (!isCancelled() && isAppNotificationBlocked()) {
- return SilentSetting.BLOCKED_NOTIFICATIONS;
- }
- return null;
- }
-
- @Override
- protected void onCancelled() {
- super.onCancelled();
- if (mCheckSilenceSettingsTask == this) {
- mCheckSilenceSettingsTask = null;
- }
- }
-
- @Override
- protected void onPostExecute(SilentSetting silentSetting) {
- if (mCheckSilenceSettingsTask == this) {
- mCheckSilenceSettingsTask = null;
- setSilentState(silentSetting);
- }
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- private boolean isDoNotDisturbBlockingAlarms() {
- if (!Utils.isMOrLater()) {
- return false;
- }
-
- try {
- final int interruptionFilter = mNotificationManager.getCurrentInterruptionFilter();
- return interruptionFilter == INTERRUPTION_FILTER_NONE;
- } catch (Exception e) {
- // Since this is purely informational, avoid crashing the app.
- return false;
- }
- }
-
- private boolean isAlarmStreamMuted() {
- try {
- return mAudioManager.getStreamVolume(STREAM_ALARM) <= 0;
- } catch (Exception e) {
- // Since this is purely informational, avoid crashing the app.
- return false;
- }
- }
-
- private boolean isSystemAlarmRingtoneSilent() {
- try {
- return RingtoneManager.getActualDefaultRingtoneUri(mContext, TYPE_ALARM) == null;
- } catch (Exception e) {
- // Since this is purely informational, avoid crashing the app.
- return false;
- }
- }
-
- private boolean isAppNotificationBlocked() {
- try {
- return !NotificationManagerCompat.from(mContext).areNotificationsEnabled();
- } catch (Exception e) {
- // Since this is purely informational, avoid crashing the app.
- return false;
- }
- }
- }
-
- /**
- * Observe changes to specific URI for settings that can silence firing alarms.
- */
- private final class ContentChangeWatcher extends ContentObserver {
- private ContentChangeWatcher() {
- super(new Handler());
- }
-
- @Override
- public void onChange(boolean selfChange) {
- updateSilentState();
- }
- }
-
- /**
- * Observe changes to the do-not-disturb setting.
- */
- private final class DoNotDisturbChangeReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateSilentState();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/SilentSettingsModel.kt b/src/com/android/deskclock/data/SilentSettingsModel.kt
new file mode 100644
index 0000000..a21f1b5
--- /dev/null
+++ b/src/com/android/deskclock/data/SilentSettingsModel.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.annotation.TargetApi
+import android.app.NotificationManager
+import android.app.NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED
+import android.app.NotificationManager.INTERRUPTION_FILTER_NONE
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Context.AUDIO_SERVICE
+import android.content.Context.NOTIFICATION_SERVICE
+import android.content.Intent
+import android.content.IntentFilter
+import android.database.ContentObserver
+import android.media.AudioManager
+import android.media.AudioManager.STREAM_ALARM
+import android.media.RingtoneManager
+import android.media.RingtoneManager.TYPE_ALARM
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings.System.CONTENT_URI
+import android.provider.Settings.System.DEFAULT_ALARM_ALERT_URI
+import androidx.core.app.NotificationManagerCompat
+
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel.SilentSetting
+
+/**
+ * This model fetches and stores reasons that alarms may be suppressed or silenced by system
+ * settings on the device. This information is displayed passively to notify the user of this
+ * condition and set their expectations for future firing alarms.
+ */
+internal class SilentSettingsModel(
+ private val mContext: Context,
+ /** Used to determine if the application is in the foreground. */
+ private val mNotificationModel: NotificationModel
+) {
+
+ /** Used to query the alarm volume and display the system control to change the alarm volume. */
+ private val mAudioManager = mContext.getSystemService(AUDIO_SERVICE) as AudioManager
+
+ /** Used to query the do-not-disturb setting value, also called "interruption filter". */
+ private val mNotificationManager =
+ mContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+ /** List of listeners to invoke upon silence state change. */
+ private val mListeners: MutableList<OnSilentSettingsListener> = ArrayList(1)
+
+ /**
+ * The last setting known to be blocking alarms; `null` indicates no settings are
+ * blocking the app or the app is not in the foreground.
+ */
+ private var mSilentSetting: SilentSetting? = null
+
+ /** The background task that checks the device system settings that influence alarm firing. */
+ private var mCheckSilenceSettingsTask: CheckSilenceSettingsTask? = null
+
+ init {
+ // Watch for changes to the settings that may silence alarms.
+ val cr: ContentResolver = mContext.getContentResolver()
+ val contentChangeWatcher: ContentObserver = ContentChangeWatcher()
+ cr.registerContentObserver(VOLUME_URI, false, contentChangeWatcher)
+ cr.registerContentObserver(DEFAULT_ALARM_ALERT_URI, false, contentChangeWatcher)
+ if (Utils.isMOrLater) {
+ val filter = IntentFilter(ACTION_INTERRUPTION_FILTER_CHANGED)
+ mContext.registerReceiver(DoNotDisturbChangeReceiver(), filter)
+ }
+ }
+
+ fun addSilentSettingsListener(listener: OnSilentSettingsListener) {
+ mListeners.add(listener)
+ }
+
+ fun removeSilentSettingsListener(listener: OnSilentSettingsListener) {
+ mListeners.remove(listener)
+ }
+
+ /**
+ * If the app is in the foreground, start a task to determine if any device setting will block
+ * alarms from firing. If the app is in the background, clear any results from the last time
+ * those settings were inspected.
+ */
+ fun updateSilentState() {
+ // Cancel any task in flight, the result is no longer relevant.
+ if (mCheckSilenceSettingsTask != null) {
+ mCheckSilenceSettingsTask!!.cancel(true)
+ mCheckSilenceSettingsTask = null
+ }
+
+ if (mNotificationModel.isApplicationInForeground) {
+ mCheckSilenceSettingsTask = CheckSilenceSettingsTask()
+ mCheckSilenceSettingsTask!!.execute()
+ } else {
+ setSilentState(null)
+ }
+ }
+
+ /**
+ * @param silentSetting the latest notion of which setting is suppressing alarms; `null`
+ * if no settings are suppressing alarms
+ */
+ private fun setSilentState(silentSetting: SilentSetting?) {
+ if (mSilentSetting != silentSetting) {
+ val oldReason = mSilentSetting
+ mSilentSetting = silentSetting
+ for (listener in mListeners) {
+ listener.onSilentSettingsChange(oldReason, silentSetting)
+ }
+ }
+ }
+
+ /**
+ * This task inspects a variety of system settings that can prevent alarms from firing or the
+ * associated ringtone from playing. If any of them would prevent an alarm from firing or
+ * making noise, a description of the setting is reported to this model on the main thread.
+ */
+ // TODO(b/165664115) Replace deprecated AsyncTask calls
+ private inner class CheckSilenceSettingsTask : AsyncTask<Void?, Void?, SilentSetting?>() {
+ override fun doInBackground(vararg parameters: Void?): SilentSetting? {
+ if (!isCancelled() && isDoNotDisturbBlockingAlarms) {
+ return SilentSetting.DO_NOT_DISTURB
+ } else if (!isCancelled() && isAlarmStreamMuted) {
+ return SilentSetting.MUTED_VOLUME
+ } else if (!isCancelled() && isSystemAlarmRingtoneSilent) {
+ return SilentSetting.SILENT_RINGTONE
+ } else if (!isCancelled() && isAppNotificationBlocked) {
+ return SilentSetting.BLOCKED_NOTIFICATIONS
+ }
+ return null
+ }
+
+ override fun onCancelled() {
+ super.onCancelled()
+ if (mCheckSilenceSettingsTask == this) {
+ mCheckSilenceSettingsTask = null
+ }
+ }
+
+ override fun onPostExecute(silentSetting: SilentSetting?) {
+ if (mCheckSilenceSettingsTask == this) {
+ mCheckSilenceSettingsTask = null
+ setSilentState(silentSetting)
+ }
+ }
+
+ @get:TargetApi(Build.VERSION_CODES.M)
+ private val isDoNotDisturbBlockingAlarms: Boolean
+ get() = if (!Utils.isMOrLater) {
+ false
+ } else try {
+ val interruptionFilter: Int = mNotificationManager.getCurrentInterruptionFilter()
+ interruptionFilter == INTERRUPTION_FILTER_NONE
+ } catch (e: Exception) {
+ // Since this is purely informational, avoid crashing the app.
+ false
+ }
+
+ private val isAlarmStreamMuted: Boolean
+ get() = try {
+ mAudioManager.getStreamVolume(STREAM_ALARM) <= 0
+ } catch (e: Exception) {
+ // Since this is purely informational, avoid crashing the app.
+ false
+ }
+
+ private val isSystemAlarmRingtoneSilent: Boolean
+ get() = try {
+ RingtoneManager.getActualDefaultRingtoneUri(mContext, TYPE_ALARM) == null
+ } catch (e: Exception) {
+ // Since this is purely informational, avoid crashing the app.
+ false
+ }
+
+ private val isAppNotificationBlocked: Boolean
+ get() = try {
+ !NotificationManagerCompat.from(mContext).areNotificationsEnabled()
+ } catch (e: Exception) {
+ // Since this is purely informational, avoid crashing the app.
+ false
+ }
+ }
+
+ /**
+ * Observe changes to specific URI for settings that can silence firing alarms.
+ */
+ private inner class ContentChangeWatcher : ContentObserver(Handler(Looper.myLooper()!!)) {
+ override fun onChange(selfChange: Boolean) {
+ updateSilentState()
+ }
+ }
+
+ /**
+ * Observe changes to the do-not-disturb setting.
+ */
+ private inner class DoNotDisturbChangeReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ updateSilentState()
+ }
+ }
+
+ companion object {
+ /** The Uri to the settings entry that stores alarm stream volume. */
+ private val VOLUME_URI: Uri = Uri.withAppendedPath(CONTENT_URI, "volume_alarm_speaker")
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Stopwatch.java b/src/com/android/deskclock/data/Stopwatch.java
deleted file mode 100644
index f53af38..0000000
--- a/src/com/android/deskclock/data/Stopwatch.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import static com.android.deskclock.Utils.now;
-import static com.android.deskclock.Utils.wallClock;
-import static com.android.deskclock.data.Stopwatch.State.PAUSED;
-import static com.android.deskclock.data.Stopwatch.State.RESET;
-import static com.android.deskclock.data.Stopwatch.State.RUNNING;
-
-/**
- * A read-only domain object representing a stopwatch.
- */
-public final class Stopwatch {
-
- public enum State { RESET, RUNNING, PAUSED }
-
- static final long UNUSED = Long.MIN_VALUE;
-
- /** The single, immutable instance of a reset stopwatch. */
- private static final Stopwatch RESET_STOPWATCH = new Stopwatch(RESET, UNUSED, UNUSED, 0);
-
- /** Current state of this stopwatch. */
- private final State mState;
-
- /** Elapsed time in ms the stopwatch was last started; {@link #UNUSED} if not running. */
- private final long mLastStartTime;
-
- /** The time since epoch at which the stopwatch was last started. */
- private final long mLastStartWallClockTime;
-
- /** Elapsed time in ms this stopwatch has accumulated while running. */
- private final long mAccumulatedTime;
-
- Stopwatch(State state, long lastStartTime, long lastWallClockTime, long accumulatedTime) {
- mState = state;
- mLastStartTime = lastStartTime;
- mLastStartWallClockTime = lastWallClockTime;
- mAccumulatedTime = accumulatedTime;
- }
-
- public State getState() { return mState; }
- public long getLastStartTime() { return mLastStartTime; }
- public long getLastWallClockTime() { return mLastStartWallClockTime; }
- public boolean isReset() { return mState == RESET; }
- public boolean isPaused() { return mState == PAUSED; }
- public boolean isRunning() { return mState == RUNNING; }
-
- /**
- * @return the total amount of time accumulated up to this moment
- */
- public long getTotalTime() {
- if (mState != RUNNING) {
- return mAccumulatedTime;
- }
-
- // In practice, "now" can be any value due to device reboots. When the real-time clock
- // is reset, there is no more guarantee that "now" falls after the last start time. To
- // ensure the stopwatch is monotonically increasing, normalize negative time segments to 0,
- final long timeSinceStart = now() - mLastStartTime;
- return mAccumulatedTime + Math.max(0, timeSinceStart);
- }
-
- /**
- * @return the amount of time accumulated up to the last time the stopwatch was started
- */
- public long getAccumulatedTime() {
- return mAccumulatedTime;
- }
-
- /**
- * @return a copy of this stopwatch that is running
- */
- Stopwatch start() {
- if (mState == RUNNING) {
- return this;
- }
-
- return new Stopwatch(RUNNING, now(), wallClock(), getTotalTime());
- }
-
- /**
- * @return a copy of this stopwatch that is paused
- */
- Stopwatch pause() {
- if (mState != RUNNING) {
- return this;
- }
-
- return new Stopwatch(PAUSED, UNUSED, UNUSED, getTotalTime());
- }
-
- /**
- * @return a copy of this stopwatch that is reset
- */
- Stopwatch reset() {
- return RESET_STOPWATCH;
- }
-
- /**
- * @return this Stopwatch if it is not running or an updated version based on wallclock time.
- * The internals of the stopwatch are updated using the wallclock time which is durable
- * across reboots.
- */
- Stopwatch updateAfterReboot() {
- if (mState != RUNNING) {
- return this;
- }
- final long timeSinceBoot = now();
- final long wallClockTime = wallClock();
- // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
- // update the recorded times and proceed with no change in accumulated time.
- final long delta = Math.max(0, wallClockTime - mLastStartWallClockTime);
- return new Stopwatch(mState, timeSinceBoot, wallClockTime, mAccumulatedTime + delta);
- }
-
- /**
- * @return this Stopwatch if it is not running or an updated version based on the realtime.
- * The internals of the stopwatch are updated using the realtime clock which is accurate
- * across wallclock time adjustments.
- */
- Stopwatch updateAfterTimeSet() {
- if (mState != RUNNING) {
- return this;
- }
- final long timeSinceBoot = now();
- final long wallClockTime = wallClock();
- final long delta = timeSinceBoot - mLastStartTime;
- if (delta < 0) {
- // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
- // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
- // updateAfterReboot() can successfully correct the data at a later time.
- return this;
- }
- return new Stopwatch(mState, timeSinceBoot, wallClockTime, mAccumulatedTime + delta);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Stopwatch.kt b/src/com/android/deskclock/data/Stopwatch.kt
new file mode 100644
index 0000000..2953670
--- /dev/null
+++ b/src/com/android/deskclock/data/Stopwatch.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import com.android.deskclock.Utils
+
+import kotlin.math.max
+
+/**
+ * A read-only domain object representing a stopwatch.
+ */
+class Stopwatch internal constructor(
+ /** Current state of this stopwatch. */
+ val state: State,
+ /** Elapsed time in ms the stopwatch was last started; [.UNUSED] if not running. */
+ val lastStartTime: Long,
+ /** The time since epoch at which the stopwatch was last started. */
+ val lastWallClockTime: Long,
+ /** Elapsed time in ms this stopwatch has accumulated while running. */
+ val accumulatedTime: Long
+) {
+
+ enum class State {
+ RESET, RUNNING, PAUSED
+ }
+
+ val isReset: Boolean
+ get() = state == State.RESET
+
+ val isPaused: Boolean
+ get() = state == State.PAUSED
+
+ val isRunning: Boolean
+ get() = state == State.RUNNING
+
+ /**
+ * @return the total amount of time accumulated up to this moment
+ */
+ val totalTime: Long
+ get() {
+ if (state != State.RUNNING) {
+ return accumulatedTime
+ }
+
+ // In practice, "now" can be any value due to device reboots. When the real-time clock
+ // is reset, there is no more guarantee that "now" falls after the last start time. To
+ // ensure the stopwatch is monotonically increasing, normalize negative time segments to
+ // 0
+ val timeSinceStart = Utils.now() - lastStartTime
+ return accumulatedTime + max(0, timeSinceStart)
+ }
+
+ /**
+ * @return a copy of this stopwatch that is running
+ */
+ fun start(): Stopwatch {
+ return if (state == State.RUNNING) {
+ this
+ } else {
+ Stopwatch(State.RUNNING, Utils.now(), Utils.wallClock(), totalTime)
+ }
+ }
+
+ /**
+ * @return a copy of this stopwatch that is paused
+ */
+ fun pause(): Stopwatch {
+ return if (state != State.RUNNING) {
+ this
+ } else {
+ Stopwatch(State.PAUSED, UNUSED, UNUSED, totalTime)
+ }
+ }
+
+ /**
+ * @return a copy of this stopwatch that is reset
+ */
+ fun reset(): Stopwatch = RESET_STOPWATCH
+
+ /**
+ * @return this Stopwatch if it is not running or an updated version based on wallclock time.
+ * The internals of the stopwatch are updated using the wallclock time which is durable
+ * across reboots.
+ */
+ fun updateAfterReboot(): Stopwatch {
+ if (state != State.RUNNING) {
+ return this
+ }
+ val timeSinceBoot = Utils.now()
+ val wallClockTime = Utils.wallClock()
+ // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
+ // update the recorded times and proceed with no change in accumulated time.
+ val delta = max(0, wallClockTime - lastWallClockTime)
+ return Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta)
+ }
+
+ /**
+ * @return this Stopwatch if it is not running or an updated version based on the realtime.
+ * The internals of the stopwatch are updated using the realtime clock which is accurate
+ * across wallclock time adjustments.
+ */
+ fun updateAfterTimeSet(): Stopwatch {
+ if (state != State.RUNNING) {
+ return this
+ }
+ val timeSinceBoot = Utils.now()
+ val wallClockTime = Utils.wallClock()
+ val delta = timeSinceBoot - lastStartTime
+ return if (delta < 0) {
+ // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
+ // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
+ // updateAfterReboot() can successfully correct the data at a later time.
+ this
+ } else {
+ Stopwatch(state, timeSinceBoot, wallClockTime, accumulatedTime + delta)
+ }
+ }
+
+ companion object {
+ const val UNUSED = Long.MIN_VALUE
+
+ /** The single, immutable instance of a reset stopwatch. */
+ private val RESET_STOPWATCH = Stopwatch(State.RESET, UNUSED, UNUSED, 0)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchDAO.java b/src/com/android/deskclock/data/StopwatchDAO.java
deleted file mode 100644
index 5413901..0000000
--- a/src/com/android/deskclock/data/StopwatchDAO.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.SharedPreferences;
-
-import com.android.deskclock.data.Stopwatch.State;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static com.android.deskclock.data.Stopwatch.State.RESET;
-
-/**
- * This class encapsulates the transfer of data between {@link Stopwatch} and {@link Lap} domain
- * objects and their permanent storage in {@link SharedPreferences}.
- */
-final class StopwatchDAO {
-
- /** Key to a preference that stores the state of the stopwatch. */
- private static final String STATE = "sw_state";
-
- /** Key to a preference that stores the last start time of the stopwatch. */
- private static final String LAST_START_TIME = "sw_start_time";
-
- /** Key to a preference that stores the epoch time when the stopwatch last started. */
- private static final String LAST_WALL_CLOCK_TIME = "sw_wall_clock_time";
-
- /** Key to a preference that stores the accumulated elapsed time of the stopwatch. */
- private static final String ACCUMULATED_TIME = "sw_accum_time";
-
- /** Prefix for a key to a preference that stores the number of recorded laps. */
- private static final String LAP_COUNT = "sw_lap_num";
-
- /** Prefix for a key to a preference that stores accumulated time at the end of a lap. */
- private static final String LAP_ACCUMULATED_TIME = "sw_lap_time_";
-
- private StopwatchDAO() {}
-
- /**
- * @return the stopwatch from permanent storage or a reset stopwatch if none exists
- */
- static Stopwatch getStopwatch(SharedPreferences prefs) {
- final int stateIndex = prefs.getInt(STATE, RESET.ordinal());
- final State state = State.values()[stateIndex];
- final long lastStartTime = prefs.getLong(LAST_START_TIME, Stopwatch.UNUSED);
- final long lastWallClockTime = prefs.getLong(LAST_WALL_CLOCK_TIME, Stopwatch.UNUSED);
- final long accumulatedTime = prefs.getLong(ACCUMULATED_TIME, 0);
- Stopwatch s = new Stopwatch(state, lastStartTime, lastWallClockTime, accumulatedTime);
-
- // If the stopwatch reports an illegal (negative) amount of time, remove the bad data.
- if (s.getTotalTime() < 0) {
- s = s.reset();
- setStopwatch(prefs, s);
- }
- return s;
- }
-
- /**
- * @param stopwatch the last state of the stopwatch
- */
- static void setStopwatch(SharedPreferences prefs, Stopwatch stopwatch) {
- final SharedPreferences.Editor editor = prefs.edit();
-
- if (stopwatch.isReset()) {
- editor.remove(STATE)
- .remove(LAST_START_TIME)
- .remove(LAST_WALL_CLOCK_TIME)
- .remove(ACCUMULATED_TIME);
- } else {
- editor.putInt(STATE, stopwatch.getState().ordinal())
- .putLong(LAST_START_TIME, stopwatch.getLastStartTime())
- .putLong(LAST_WALL_CLOCK_TIME, stopwatch.getLastWallClockTime())
- .putLong(ACCUMULATED_TIME, stopwatch.getAccumulatedTime());
- }
-
- editor.apply();
- }
-
- /**
- * @return a list of recorded laps for the stopwatch
- */
- static List<Lap> getLaps(SharedPreferences prefs) {
- // Prepare the container to be filled with laps.
- final int lapCount = prefs.getInt(LAP_COUNT, 0);
- final List<Lap> laps = new ArrayList<>(lapCount);
-
- long prevAccumulatedTime = 0;
-
- // Lap numbers are 1-based and so the are corresponding shared preference keys.
- for (int lapNumber = 1; lapNumber <= lapCount; lapNumber++) {
- // Look up the accumulated time for the lap.
- final String lapAccumulatedTimeKey = LAP_ACCUMULATED_TIME + lapNumber;
- final long accumulatedTime = prefs.getLong(lapAccumulatedTimeKey, 0);
-
- // Lap time is the delta between accumulated time of this lap and prior lap.
- final long lapTime = accumulatedTime - prevAccumulatedTime;
-
- // Create the lap instance from the data.
- laps.add(new Lap(lapNumber, lapTime, accumulatedTime));
-
- // Update the accumulated time of the previous lap.
- prevAccumulatedTime = accumulatedTime;
- }
-
- // Laps are stored in the order they were recorded; display order is the reverse.
- Collections.reverse(laps);
-
- return laps;
- }
-
- /**
- * @param newLapCount the number of laps including the new lap
- * @param accumulatedTime the amount of time accumulate by the stopwatch at the end of the lap
- */
- static void addLap(SharedPreferences prefs, int newLapCount, long accumulatedTime) {
- prefs.edit()
- .putInt(LAP_COUNT, newLapCount)
- .putLong(LAP_ACCUMULATED_TIME + newLapCount, accumulatedTime)
- .apply();
- }
-
- /**
- * Remove the recorded laps for the stopwatch
- */
- static void clearLaps(SharedPreferences prefs) {
- final SharedPreferences.Editor editor = prefs.edit();
-
- final int lapCount = prefs.getInt(LAP_COUNT, 0);
- for (int lapNumber = 1; lapNumber <= lapCount; lapNumber++) {
- editor.remove(LAP_ACCUMULATED_TIME + lapNumber);
- }
- editor.remove(LAP_COUNT);
-
- editor.apply();
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchDAO.kt b/src/com/android/deskclock/data/StopwatchDAO.kt
new file mode 100644
index 0000000..5e049fe
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchDAO.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.SharedPreferences
+
+/**
+ * This class encapsulates the transfer of data between [Stopwatch] and [Lap] domain
+ * objects and their permanent storage in [SharedPreferences].
+ */
+internal object StopwatchDAO {
+ /** Key to a preference that stores the state of the stopwatch. */
+ private const val STATE = "sw_state"
+
+ /** Key to a preference that stores the last start time of the stopwatch. */
+ private const val LAST_START_TIME = "sw_start_time"
+
+ /** Key to a preference that stores the epoch time when the stopwatch last started. */
+ private const val LAST_WALL_CLOCK_TIME = "sw_wall_clock_time"
+
+ /** Key to a preference that stores the accumulated elapsed time of the stopwatch. */
+ private const val ACCUMULATED_TIME = "sw_accum_time"
+
+ /** Prefix for a key to a preference that stores the number of recorded laps. */
+ private const val LAP_COUNT = "sw_lap_num"
+
+ /** Prefix for a key to a preference that stores accumulated time at the end of a lap. */
+ private const val LAP_ACCUMULATED_TIME = "sw_lap_time_"
+
+ /**
+ * @return the stopwatch from permanent storage or a reset stopwatch if none exists
+ */
+ fun getStopwatch(prefs: SharedPreferences): Stopwatch {
+ val stateIndex: Int = prefs.getInt(STATE, Stopwatch.State.RESET.ordinal)
+ val state = Stopwatch.State.values()[stateIndex]
+ val lastStartTime: Long = prefs.getLong(LAST_START_TIME, Stopwatch.UNUSED)
+ val lastWallClockTime: Long = prefs.getLong(LAST_WALL_CLOCK_TIME, Stopwatch.UNUSED)
+ val accumulatedTime: Long = prefs.getLong(ACCUMULATED_TIME, 0)
+ var s = Stopwatch(state, lastStartTime, lastWallClockTime, accumulatedTime)
+
+ // If the stopwatch reports an illegal (negative) amount of time, remove the bad data.
+ if (s.totalTime < 0) {
+ s = s.reset()
+ setStopwatch(prefs, s)
+ }
+ return s
+ }
+
+ /**
+ * @param stopwatch the last state of the stopwatch
+ */
+ fun setStopwatch(prefs: SharedPreferences, stopwatch: Stopwatch) {
+ val editor: SharedPreferences.Editor = prefs.edit()
+
+ if (stopwatch.isReset) {
+ editor.remove(STATE)
+ .remove(LAST_START_TIME)
+ .remove(LAST_WALL_CLOCK_TIME)
+ .remove(ACCUMULATED_TIME)
+ } else {
+ editor.putInt(STATE, stopwatch.state.ordinal)
+ .putLong(LAST_START_TIME, stopwatch.lastStartTime)
+ .putLong(LAST_WALL_CLOCK_TIME, stopwatch.lastWallClockTime)
+ .putLong(ACCUMULATED_TIME, stopwatch.accumulatedTime)
+ }
+
+ editor.apply()
+ }
+
+ /**
+ * @return a list of recorded laps for the stopwatch
+ */
+ fun getLaps(prefs: SharedPreferences): MutableList<Lap> {
+ // Prepare the container to be filled with laps.
+ val lapCount: Int = prefs.getInt(LAP_COUNT, 0)
+ val laps: MutableList<Lap> = mutableListOf()
+
+ var prevAccumulatedTime: Long = 0
+
+ // Lap numbers are 1-based and so the are corresponding shared preference keys.
+ for (lapNumber in 1..lapCount) {
+ // Look up the accumulated time for the lap.
+ val lapAccumulatedTimeKey = LAP_ACCUMULATED_TIME + lapNumber
+ val accumulatedTime: Long = prefs.getLong(lapAccumulatedTimeKey, 0)
+
+ // Lap time is the delta between accumulated time of this lap and prior lap.
+ val lapTime = accumulatedTime - prevAccumulatedTime
+
+ // Create the lap instance from the data.
+ laps.add(Lap(lapNumber, lapTime, accumulatedTime))
+
+ // Update the accumulated time of the previous lap.
+ prevAccumulatedTime = accumulatedTime
+ }
+
+ // Laps are stored in the order they were recorded; display order is the reverse.
+ laps.reverse()
+
+ return laps
+ }
+
+ /**
+ * @param newLapCount the number of laps including the new lap
+ * @param accumulatedTime the amount of time accumulate by the stopwatch at the end of the lap
+ */
+ fun addLap(prefs: SharedPreferences, newLapCount: Int, accumulatedTime: Long) {
+ prefs.edit()
+ .putInt(LAP_COUNT, newLapCount)
+ .putLong(LAP_ACCUMULATED_TIME + newLapCount, accumulatedTime)
+ .apply()
+ }
+
+ /**
+ * Remove the recorded laps for the stopwatch
+ */
+ fun clearLaps(prefs: SharedPreferences) {
+ val editor: SharedPreferences.Editor = prefs.edit()
+
+ val lapCount: Int = prefs.getInt(LAP_COUNT, 0)
+ for (lapNumber in 1..lapCount) {
+ editor.remove(LAP_ACCUMULATED_TIME + lapNumber)
+ }
+ editor.remove(LAP_COUNT)
+
+ editor.apply()
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchListener.java b/src/com/android/deskclock/data/StopwatchListener.kt
similarity index 79%
rename from src/com/android/deskclock/data/StopwatchListener.java
rename to src/com/android/deskclock/data/StopwatchListener.kt
index 838dfab..334662f 100644
--- a/src/com/android/deskclock/data/StopwatchListener.java
+++ b/src/com/android/deskclock/data/StopwatchListener.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,21 +14,20 @@
* limitations under the License.
*/
-package com.android.deskclock.data;
+package com.android.deskclock.data
/**
* The interface through which interested parties are notified of changes to the stopwatch or laps.
*/
-public interface StopwatchListener {
-
+interface StopwatchListener {
/**
* @param before the stopwatch state before the update
* @param after the stopwatch state after the update
*/
- void stopwatchUpdated(Stopwatch before, Stopwatch after);
+ fun stopwatchUpdated(before: Stopwatch, after: Stopwatch)
/**
* @param lap the lap that was added
*/
- void lapAdded(Lap lap);
+ fun lapAdded(lap: Lap)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchModel.java b/src/com/android/deskclock/data/StopwatchModel.java
deleted file mode 100644
index b5c93f9..0000000
--- a/src/com/android/deskclock/data/StopwatchModel.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import androidx.annotation.VisibleForTesting;
-import androidx.core.app.NotificationManagerCompat;
-
-import com.android.deskclock.R;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * All {@link Stopwatch} data is accessed via this model.
- */
-final class StopwatchModel {
-
- private final Context mContext;
-
- private final SharedPreferences mPrefs;
-
- /** The model from which notification data are fetched. */
- private final NotificationModel mNotificationModel;
-
- /** Used to create and destroy system notifications related to the stopwatch. */
- private final NotificationManagerCompat mNotificationManager;
-
- /** Update stopwatch notification when locale changes. */
- @SuppressWarnings("FieldCanBeLocal")
- private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
- /** The listeners to notify when the stopwatch or its laps change. */
- private final List<StopwatchListener> mStopwatchListeners = new ArrayList<>();
-
- /** Delegate that builds platform-specific stopwatch notifications. */
- private final StopwatchNotificationBuilder mNotificationBuilder =
- new StopwatchNotificationBuilder();
-
- /** The current state of the stopwatch. */
- private Stopwatch mStopwatch;
-
- /** A mutable copy of the recorded stopwatch laps. */
- private List<Lap> mLaps;
-
- StopwatchModel(Context context, SharedPreferences prefs, NotificationModel notificationModel) {
- mContext = context;
- mPrefs = prefs;
- mNotificationModel = notificationModel;
- mNotificationManager = NotificationManagerCompat.from(context);
-
- // Update stopwatch notification when locale changes.
- final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
- mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
- }
-
- /**
- * @param stopwatchListener to be notified when stopwatch changes or laps are added
- */
- void addStopwatchListener(StopwatchListener stopwatchListener) {
- mStopwatchListeners.add(stopwatchListener);
- }
-
- /**
- * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
- */
- void removeStopwatchListener(StopwatchListener stopwatchListener) {
- mStopwatchListeners.remove(stopwatchListener);
- }
-
- /**
- * @return the current state of the stopwatch
- */
- Stopwatch getStopwatch() {
- if (mStopwatch == null) {
- mStopwatch = StopwatchDAO.getStopwatch(mPrefs);
- }
-
- return mStopwatch;
- }
-
- /**
- * @param stopwatch the new state of the stopwatch
- */
- Stopwatch setStopwatch(Stopwatch stopwatch) {
- final Stopwatch before = getStopwatch();
- if (before != stopwatch) {
- StopwatchDAO.setStopwatch(mPrefs, stopwatch);
- mStopwatch = stopwatch;
-
- // Refresh the stopwatch notification to reflect the latest stopwatch state.
- if (!mNotificationModel.isApplicationInForeground()) {
- updateNotification();
- }
-
- // Resetting the stopwatch implicitly clears the recorded laps.
- if (stopwatch.isReset()) {
- clearLaps();
- }
-
- // Notify listeners of the stopwatch change.
- for (StopwatchListener stopwatchListener : mStopwatchListeners) {
- stopwatchListener.stopwatchUpdated(before, stopwatch);
- }
- }
-
- return stopwatch;
- }
-
- /**
- * @return the laps recorded for this stopwatch
- */
- List<Lap> getLaps() {
- return Collections.unmodifiableList(getMutableLaps());
- }
-
- /**
- * @return a newly recorded lap completed now; {@code null} if no more laps can be added
- */
- Lap addLap() {
- if (!mStopwatch.isRunning() || !canAddMoreLaps()) {
- return null;
- }
-
- final long totalTime = getStopwatch().getTotalTime();
- final List<Lap> laps = getMutableLaps();
-
- final int lapNumber = laps.size() + 1;
- StopwatchDAO.addLap(mPrefs, lapNumber, totalTime);
-
- final long prevAccumulatedTime = laps.isEmpty() ? 0 : laps.get(0).getAccumulatedTime();
- final long lapTime = totalTime - prevAccumulatedTime;
-
- final Lap lap = new Lap(lapNumber, lapTime, totalTime);
- laps.add(0, lap);
-
- // Refresh the stopwatch notification to reflect the latest stopwatch state.
- if (!mNotificationModel.isApplicationInForeground()) {
- updateNotification();
- }
-
- // Notify listeners of the new lap.
- for (StopwatchListener stopwatchListener : mStopwatchListeners) {
- stopwatchListener.lapAdded(lap);
- }
-
- return lap;
- }
-
- /**
- * Clears the laps recorded for this stopwatch.
- */
- @VisibleForTesting
- void clearLaps() {
- StopwatchDAO.clearLaps(mPrefs);
- getMutableLaps().clear();
- }
-
- /**
- * @return {@code true} iff more laps can be recorded
- */
- boolean canAddMoreLaps() {
- return getLaps().size() < 98;
- }
-
- /**
- * @return the longest lap time of all recorded laps and the current lap
- */
- long getLongestLapTime() {
- long maxLapTime = 0;
-
- final List<Lap> laps = getLaps();
- if (!laps.isEmpty()) {
- // Compute the maximum lap time across all recorded laps.
- for (Lap lap : getLaps()) {
- maxLapTime = Math.max(maxLapTime, lap.getLapTime());
- }
-
- // Compare with the maximum lap time for the current lap.
- final Stopwatch stopwatch = getStopwatch();
- final long currentLapTime = stopwatch.getTotalTime() - laps.get(0).getAccumulatedTime();
- maxLapTime = Math.max(maxLapTime, currentLapTime);
- }
-
- return maxLapTime;
- }
-
- /**
- * In practice, {@code time} can be any value due to device reboots. When the real-time clock is
- * reset, there is no more guarantee that this time falls after the last recorded lap.
- *
- * @param time a point in time expected, but not required, to be after the end of the prior lap
- * @return the elapsed time between the given {@code time} and the end of the prior lap;
- * negative elapsed times are normalized to {@code 0}
- */
- long getCurrentLapTime(long time) {
- final Lap previousLap = getLaps().get(0);
- final long currentLapTime = time - previousLap.getAccumulatedTime();
- return Math.max(0, currentLapTime);
- }
-
- /**
- * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
- */
- void updateNotification() {
- final Stopwatch stopwatch = getStopwatch();
-
- // Notification should be hidden if the stopwatch has no time or the app is open.
- if (stopwatch.isReset() || mNotificationModel.isApplicationInForeground()) {
- mNotificationManager.cancel(mNotificationModel.getStopwatchNotificationId());
- return;
- }
-
- // Otherwise build and post a notification reflecting the latest stopwatch state.
- final Notification notification =
- mNotificationBuilder.build(mContext, mNotificationModel, stopwatch);
- mNotificationBuilder.buildChannel(mContext, mNotificationManager);
- mNotificationManager.notify(mNotificationModel.getStopwatchNotificationId(), notification);
- }
-
- private List<Lap> getMutableLaps() {
- if (mLaps == null) {
- mLaps = StopwatchDAO.getLaps(mPrefs);
- }
-
- return mLaps;
- }
-
- /**
- * Update the stopwatch notification in response to a locale change.
- */
- private final class LocaleChangedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateNotification();
- }
- }
-}
diff --git a/src/com/android/deskclock/data/StopwatchModel.kt b/src/com/android/deskclock/data/StopwatchModel.kt
new file mode 100644
index 0000000..c3e022f
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchModel.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.app.Notification
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationManagerCompat
+
+import kotlin.math.max
+
+/**
+ * All [Stopwatch] data is accessed via this model.
+ */
+internal class StopwatchModel(
+ private val mContext: Context,
+ private val mPrefs: SharedPreferences,
+ /** The model from which notification data are fetched. */
+ private val mNotificationModel: NotificationModel
+) {
+
+ /** Used to create and destroy system notifications related to the stopwatch. */
+ private val mNotificationManager = NotificationManagerCompat.from(mContext)
+
+ /** Update stopwatch notification when locale changes. */
+ private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+ /** The listeners to notify when the stopwatch or its laps change. */
+ private val mStopwatchListeners: MutableList<StopwatchListener> = mutableListOf()
+
+ /** Delegate that builds platform-specific stopwatch notifications. */
+ private val mNotificationBuilder = StopwatchNotificationBuilder()
+
+ /** The current state of the stopwatch. */
+ private var mStopwatch: Stopwatch? = null
+
+ /** A mutable copy of the recorded stopwatch laps. */
+ private var mLaps: MutableList<Lap>? = null
+
+ init {
+ // Update stopwatch notification when locale changes.
+ val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+ mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+ }
+
+ /**
+ * @param stopwatchListener to be notified when stopwatch changes or laps are added
+ */
+ fun addStopwatchListener(stopwatchListener: StopwatchListener) {
+ mStopwatchListeners.add(stopwatchListener)
+ }
+
+ /**
+ * @param stopwatchListener to no longer be notified when stopwatch changes or laps are added
+ */
+ fun removeStopwatchListener(stopwatchListener: StopwatchListener) {
+ mStopwatchListeners.remove(stopwatchListener)
+ }
+
+ /**
+ * @return the current state of the stopwatch
+ */
+ val stopwatch: Stopwatch
+ get() {
+ if (mStopwatch == null) {
+ mStopwatch = StopwatchDAO.getStopwatch(mPrefs)
+ }
+
+ return mStopwatch!!
+ }
+
+ /**
+ * @param stopwatch the new state of the stopwatch
+ */
+ fun setStopwatch(stopwatch: Stopwatch): Stopwatch {
+ val before = this.stopwatch
+ if (before != stopwatch) {
+ StopwatchDAO.setStopwatch(mPrefs, stopwatch)
+ mStopwatch = stopwatch
+
+ // Refresh the stopwatch notification to reflect the latest stopwatch state.
+ if (!mNotificationModel.isApplicationInForeground) {
+ updateNotification()
+ }
+
+ // Resetting the stopwatch implicitly clears the recorded laps.
+ if (stopwatch.isReset) {
+ clearLaps()
+ }
+
+ // Notify listeners of the stopwatch change.
+ for (stopwatchListener in mStopwatchListeners) {
+ stopwatchListener.stopwatchUpdated(before, stopwatch)
+ }
+ }
+
+ return stopwatch
+ }
+
+ /**
+ * @return the laps recorded for this stopwatch
+ */
+ val laps: List<Lap>
+ get() = mutableLaps
+
+ /**
+ * @return a newly recorded lap completed now; `null` if no more laps can be added
+ */
+ fun addLap(): Lap? {
+ if (!mStopwatch!!.isRunning || !canAddMoreLaps()) {
+ return null
+ }
+
+ val totalTime = stopwatch.totalTime
+ val laps: MutableList<Lap> = mutableLaps
+
+ val lapNumber = laps.size + 1
+ StopwatchDAO.addLap(mPrefs, lapNumber, totalTime)
+
+ val prevAccumulatedTime = if (laps.isEmpty()) 0 else laps[0].accumulatedTime
+ val lapTime = totalTime - prevAccumulatedTime
+
+ val lap = Lap(lapNumber, lapTime, totalTime)
+ laps.add(0, lap)
+
+ // Refresh the stopwatch notification to reflect the latest stopwatch state.
+ if (!mNotificationModel.isApplicationInForeground) {
+ updateNotification()
+ }
+
+ // Notify listeners of the new lap.
+ for (stopwatchListener in mStopwatchListeners) {
+ stopwatchListener.lapAdded(lap)
+ }
+
+ return lap
+ }
+
+ /**
+ * Clears the laps recorded for this stopwatch.
+ */
+ @VisibleForTesting
+ fun clearLaps() {
+ StopwatchDAO.clearLaps(mPrefs)
+ mutableLaps.clear()
+ }
+
+ /**
+ * @return `true` iff more laps can be recorded
+ */
+ fun canAddMoreLaps(): Boolean = laps.size < 98
+
+ /**
+ * @return the longest lap time of all recorded laps and the current lap
+ */
+ val longestLapTime: Long
+ get() {
+ var maxLapTime: Long = 0
+
+ val laps = laps
+ if (laps.isNotEmpty()) {
+ // Compute the maximum lap time across all recorded laps.
+ for (lap in laps) {
+ maxLapTime = max(maxLapTime, lap.lapTime)
+ }
+
+ // Compare with the maximum lap time for the current lap.
+ val stopwatch = stopwatch
+ val currentLapTime = stopwatch.totalTime - laps[0].accumulatedTime
+ maxLapTime = max(maxLapTime, currentLapTime)
+ }
+
+ return maxLapTime
+ }
+
+ /**
+ * In practice, `time` can be any value due to device reboots. When the real-time clock is
+ * reset, there is no more guarantee that this time falls after the last recorded lap.
+ *
+ * @param time a point in time expected, but not required, to be after the end of the prior lap
+ * @return the elapsed time between the given `time` and the end of the prior lap;
+ * negative elapsed times are normalized to `0`
+ */
+ fun getCurrentLapTime(time: Long): Long {
+ val previousLap = laps[0]
+ val currentLapTime = time - previousLap.accumulatedTime
+ return max(0, currentLapTime)
+ }
+
+ /**
+ * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
+ */
+ fun updateNotification() {
+ val stopwatch = stopwatch
+
+ // Notification should be hidden if the stopwatch has no time or the app is open.
+ if (stopwatch.isReset || mNotificationModel.isApplicationInForeground) {
+ mNotificationManager.cancel(mNotificationModel.stopwatchNotificationId)
+ return
+ }
+
+ // Otherwise build and post a notification reflecting the latest stopwatch state.
+ val notification: Notification =
+ mNotificationBuilder.build(mContext, mNotificationModel, stopwatch)
+ mNotificationBuilder.buildChannel(mContext, mNotificationManager)
+ mNotificationManager.notify(mNotificationModel.stopwatchNotificationId, notification)
+ }
+
+ private val mutableLaps: MutableList<Lap>
+ get() {
+ if (mLaps == null) {
+ mLaps = StopwatchDAO.getLaps(mPrefs)
+ }
+
+ return mLaps!!
+ }
+
+ /**
+ * Update the stopwatch notification in response to a locale change.
+ */
+ private inner class LocaleChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ updateNotification()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchNotificationBuilder.java b/src/com/android/deskclock/data/StopwatchNotificationBuilder.java
deleted file mode 100644
index d21fe80..0000000
--- a/src/com/android/deskclock/data/StopwatchNotificationBuilder.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.SystemClock;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.StringRes;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationCompat.Action;
-import androidx.core.app.NotificationCompat.Builder;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-import android.widget.RemoteViews;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.stopwatch.StopwatchService;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-
-/**
- * Builds notification to reflect the latest state of the stopwatch and recorded laps.
- */
-class StopwatchNotificationBuilder {
-
- /**
- * Notification channel containing all stopwatch notifications.
- */
- private static final String STOPWATCH_NOTIFICATION_CHANNEL_ID = "StopwatchNotification";
-
- public void buildChannel(Context context, NotificationManagerCompat notificationManager) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- STOPWATCH_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- notificationManager.createNotificationChannel(channel);
- }
- }
-
- public Notification build(Context context, NotificationModel nm, Stopwatch stopwatch) {
- @StringRes final int eventLabel = R.string.label_notification;
-
- // Intent to load the app when the notification is tapped.
- final Intent showApp = new Intent(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_SHOW_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
- final PendingIntent pendingShowApp = PendingIntent.getService(context, 0, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Compute some values required below.
- final boolean running = stopwatch.isRunning();
- final String pname = context.getPackageName();
- final Resources res = context.getResources();
- final long base = SystemClock.elapsedRealtime() - stopwatch.getTotalTime();
-
- final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content);
- content.setChronometer(R.id.chronometer, base, null, running);
-
- final List<Action> actions = new ArrayList<>(2);
-
- if (running) {
- // Left button: Pause
- final Intent pause = new Intent(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
- @DrawableRes final int icon1 = R.drawable.ic_pause_24dp;
- final CharSequence title1 = res.getText(R.string.sw_pause_button);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, pause);
- actions.add(new Action.Builder(icon1, title1, intent1).build());
-
- // Right button: Add Lap
- if (DataModel.getDataModel().canAddMoreLaps()) {
- final Intent lap = new Intent(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_LAP_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
- @DrawableRes final int icon2 = R.drawable.ic_sw_lap_24dp;
- final CharSequence title2 = res.getText(R.string.sw_lap_button);
- final PendingIntent intent2 = Utils.pendingServiceIntent(context, lap);
- actions.add(new Action.Builder(icon2, title2, intent2).build());
- }
-
- // Show the current lap number if any laps have been recorded.
- final int lapCount = DataModel.getDataModel().getLaps().size();
- if (lapCount > 0) {
- final int lapNumber = lapCount + 1;
- final String lap = res.getString(R.string.sw_notification_lap_number, lapNumber);
- content.setTextViewText(R.id.state, lap);
- content.setViewVisibility(R.id.state, VISIBLE);
- } else {
- content.setViewVisibility(R.id.state, GONE);
- }
- } else {
- // Left button: Start
- final Intent start = new Intent(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_START_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
- @DrawableRes final int icon1 = R.drawable.ic_start_24dp;
- final CharSequence title1 = res.getText(R.string.sw_start_button);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, start);
- actions.add(new Action.Builder(icon1, title1, intent1).build());
-
- // Right button: Reset (dismisses notification and resets stopwatch)
- final Intent reset = new Intent(context, StopwatchService.class)
- .setAction(StopwatchService.ACTION_RESET_STOPWATCH)
- .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel);
-
- @DrawableRes final int icon2 = R.drawable.ic_reset_24dp;
- final CharSequence title2 = res.getText(R.string.sw_reset_button);
- final PendingIntent intent2 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Action.Builder(icon2, title2, intent2).build());
-
- // Indicate the stopwatch is paused.
- content.setTextViewText(R.id.state, res.getString(R.string.swn_paused));
- content.setViewVisibility(R.id.state, VISIBLE);
- }
-
- final Builder notification = new NotificationCompat.Builder(
- context, STOPWATCH_NOTIFICATION_CHANNEL_ID)
- .setLocalOnly(true)
- .setOngoing(running)
- .setCustomContentView(content)
- .setContentIntent(pendingShowApp)
- .setAutoCancel(stopwatch.isPaused())
- .setPriority(Notification.PRIORITY_MAX)
- .setSmallIcon(R.drawable.stat_notify_stopwatch)
- .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
- .setColor(ContextCompat.getColor(context, R.color.default_background));
-
- if (Utils.isNOrLater()) {
- notification.setGroup(nm.getStopwatchNotificationGroupKey());
- }
-
- for (Action action : actions) {
- notification.addAction(action);
- }
-
- return notification.build();
- }
-}
diff --git a/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt b/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt
new file mode 100644
index 0000000..6827da0
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchNotificationBuilder.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.SystemClock
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.RemoteViews
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.Action
+import androidx.core.app.NotificationCompat.Builder
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.events.Events
+import com.android.deskclock.stopwatch.StopwatchService
+
+/**
+ * Builds notification to reflect the latest state of the stopwatch and recorded laps.
+ */
+internal class StopwatchNotificationBuilder {
+ fun buildChannel(context: Context, notificationManager: NotificationManagerCompat) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ STOPWATCH_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_DEFAULT)
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ fun build(context: Context, nm: NotificationModel, stopwatch: Stopwatch?): Notification {
+ @StringRes val eventLabel: Int = R.string.label_notification
+
+ // Intent to load the app when the notification is tapped.
+ val showApp: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_SHOW_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ val pendingShowApp: PendingIntent = PendingIntent.getService(context, 0, showApp,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+ // Compute some values required below.
+ val running = stopwatch!!.isRunning
+ val pname: String = context.getPackageName()
+ val res: Resources = context.getResources()
+ val base: Long = SystemClock.elapsedRealtime() - stopwatch.totalTime
+
+ val content = RemoteViews(pname, R.layout.chronometer_notif_content)
+ content.setChronometer(R.id.chronometer, base, null, running)
+
+ val actions: MutableList<Action> = ArrayList<Action>(2)
+
+ if (running) {
+ // Left button: Pause
+ val pause: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_PAUSE_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_pause_24dp
+ val title1: CharSequence = res.getText(R.string.sw_pause_button)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, pause)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+
+ // Right button: Add Lap
+ if (DataModel.dataModel.canAddMoreLaps()) {
+ val lap: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_LAP_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon2: Int = R.drawable.ic_sw_lap_24dp
+ val title2: CharSequence = res.getText(R.string.sw_lap_button)
+ val intent2: PendingIntent = Utils.pendingServiceIntent(context, lap)
+ actions.add(Action.Builder(icon2, title2, intent2).build())
+ }
+
+ // Show the current lap number if any laps have been recorded.
+ val lapCount = DataModel.dataModel.laps.size
+ if (lapCount > 0) {
+ val lapNumber = lapCount + 1
+ val lap: String = res.getString(R.string.sw_notification_lap_number, lapNumber)
+ content.setTextViewText(R.id.state, lap)
+ content.setViewVisibility(R.id.state, VISIBLE)
+ } else {
+ content.setViewVisibility(R.id.state, GONE)
+ }
+ } else {
+ // Left button: Start
+ val start: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_START_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_start_24dp
+ val title1: CharSequence = res.getText(R.string.sw_start_button)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, start)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+
+ // Right button: Reset (dismisses notification and resets stopwatch)
+ val reset: Intent = Intent(context, StopwatchService::class.java)
+ .setAction(StopwatchService.ACTION_RESET_STOPWATCH)
+ .putExtra(Events.EXTRA_EVENT_LABEL, eventLabel)
+
+ @DrawableRes val icon2: Int = R.drawable.ic_reset_24dp
+ val title2: CharSequence = res.getText(R.string.sw_reset_button)
+ val intent2: PendingIntent = Utils.pendingServiceIntent(context, reset)
+ actions.add(Action.Builder(icon2, title2, intent2).build())
+
+ // Indicate the stopwatch is paused.
+ content.setTextViewText(R.id.state, res.getString(R.string.swn_paused))
+ content.setViewVisibility(R.id.state, VISIBLE)
+ }
+ val notification: Builder = Builder(
+ context, STOPWATCH_NOTIFICATION_CHANNEL_ID)
+ .setLocalOnly(true)
+ .setOngoing(running)
+ .setCustomContentView(content)
+ .setContentIntent(pendingShowApp)
+ .setAutoCancel(stopwatch.isPaused)
+ .setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
+ .setSmallIcon(R.drawable.stat_notify_stopwatch)
+ .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+ if (Utils.isNOrLater) {
+ notification.setGroup(nm.stopwatchNotificationGroupKey)
+ }
+
+ for (action in actions) {
+ notification.addAction(action)
+ }
+
+ return notification.build()
+ }
+
+ companion object {
+ /**
+ * Notification channel containing all stopwatch notifications.
+ */
+ private const val STOPWATCH_NOTIFICATION_CHANNEL_ID = "StopwatchNotification"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimeModel.java b/src/com/android/deskclock/data/TimeModel.java
deleted file mode 100644
index 5f8b9ad..0000000
--- a/src/com/android/deskclock/data/TimeModel.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.Context;
-import android.os.SystemClock;
-import android.text.format.DateFormat;
-
-import java.util.Calendar;
-
-/**
- * All time data is accessed via this model. This model exists so that time can be mocked for
- * testing purposes.
- */
-final class TimeModel {
-
- private final Context mContext;
-
- TimeModel(Context context) {
- mContext = context;
- }
-
- /**
- * @return the current time in milliseconds
- */
- long currentTimeMillis() {
- return System.currentTimeMillis();
- }
-
- /**
- * @return milliseconds since boot, including time spent in sleep
- */
- long elapsedRealtime() {
- return SystemClock.elapsedRealtime();
- }
-
- /**
- * @return {@code true} if 24 hour time format is selected; {@code false} otherwise
- */
- boolean is24HourFormat() {
- return DateFormat.is24HourFormat(mContext);
- }
-
- /**
- * @return a new Calendar with the {@link #currentTimeMillis}
- */
- Calendar getCalendar() {
- final Calendar calendar = Calendar.getInstance();
- calendar.setTimeInMillis(currentTimeMillis());
- return calendar;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimeModel.kt b/src/com/android/deskclock/data/TimeModel.kt
new file mode 100644
index 0000000..28be8a3
--- /dev/null
+++ b/src/com/android/deskclock/data/TimeModel.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.os.SystemClock
+import android.text.format.DateFormat
+
+import java.util.Calendar
+
+/**
+ * All time data is accessed via this model. This model exists so that time can be mocked for
+ * testing purposes.
+ */
+internal class TimeModel(private val mContext: Context) {
+ /**
+ * @return the current time in milliseconds
+ */
+ fun currentTimeMillis(): Long = System.currentTimeMillis()
+
+ /**
+ * @return milliseconds since boot, including time spent in sleep
+ */
+ fun elapsedRealtime(): Long = SystemClock.elapsedRealtime()
+
+ /**
+ * @return `true` if 24 hour time format is selected; `false` otherwise
+ */
+ fun is24HourFormat(): Boolean = DateFormat.is24HourFormat(mContext)
+
+ /**
+ * @return a new Calendar with the [.currentTimeMillis]
+ */
+ val calendar: Calendar
+ get() {
+ val calendar = Calendar.getInstance()
+ calendar.timeInMillis = currentTimeMillis()
+ return calendar
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimeZones.java b/src/com/android/deskclock/data/TimeZones.java
deleted file mode 100644
index 60153c7..0000000
--- a/src/com/android/deskclock/data/TimeZones.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.text.TextUtils;
-
-/**
- * A read-only domain object representing the timezones from which to choose a "home" timezone.
- */
-public final class TimeZones {
-
- private final CharSequence[] mTimeZoneIds;
- private final CharSequence[] mTimeZoneNames;
-
- TimeZones(CharSequence[] timeZoneIds, CharSequence[] timeZoneNames) {
- mTimeZoneIds = timeZoneIds;
- mTimeZoneNames = timeZoneNames;
- }
-
- public CharSequence[] getTimeZoneIds() {
- return mTimeZoneIds;
- }
-
- public CharSequence[] getTimeZoneNames() {
- return mTimeZoneNames;
- }
-
- /**
- * @param timeZoneId identifies the timezone to locate
- * @return the timezone name with the {@code timeZoneId}; {@code null} if it does not exist
- */
- CharSequence getTimeZoneName(CharSequence timeZoneId) {
- for (int i = 0; i < mTimeZoneIds.length; i++) {
- if (TextUtils.equals(timeZoneId, mTimeZoneIds[i])) {
- return mTimeZoneNames[i];
- }
- }
-
- return null;
- }
-
- /**
- * @param timeZoneId identifies the timezone to locate
- * @return {@code true} iff the timezone with the given id is present
- */
- boolean contains(String timeZoneId) {
- return getTimeZoneName(timeZoneId) != null;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimeZones.kt b/src/com/android/deskclock/data/TimeZones.kt
new file mode 100644
index 0000000..8a5d73e
--- /dev/null
+++ b/src/com/android/deskclock/data/TimeZones.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.text.TextUtils
+
+/**
+ * A read-only domain object representing the timezones from which to choose a "home" timezone.
+ */
+class TimeZones internal constructor(
+ val timeZoneIds: Array<CharSequence>,
+ val timeZoneNames: Array<CharSequence>
+) {
+
+ /**
+ * @param timeZoneId identifies the timezone to locate
+ * @return the timezone name with the `timeZoneId`; `null` if it does not exist
+ */
+ fun getTimeZoneName(timeZoneId: CharSequence?): CharSequence? {
+ for (i in timeZoneIds.indices) {
+ if (TextUtils.equals(timeZoneId, timeZoneIds[i])) {
+ return timeZoneNames[i]
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * @param timeZoneId identifies the timezone to locate
+ * @return `true` iff the timezone with the given id is present
+ */
+ operator fun contains(timeZoneId: String?): Boolean {
+ return getTimeZoneName(timeZoneId) != null
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Timer.java b/src/com/android/deskclock/data/Timer.java
deleted file mode 100644
index 71716a3..0000000
--- a/src/com/android/deskclock/data/Timer.java
+++ /dev/null
@@ -1,433 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.text.TextUtils;
-
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
-
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-import static com.android.deskclock.Utils.now;
-import static com.android.deskclock.Utils.wallClock;
-import static com.android.deskclock.data.Timer.State.EXPIRED;
-import static com.android.deskclock.data.Timer.State.MISSED;
-import static com.android.deskclock.data.Timer.State.PAUSED;
-import static com.android.deskclock.data.Timer.State.RESET;
-import static com.android.deskclock.data.Timer.State.RUNNING;
-
-/**
- * A read-only domain object representing a countdown timer.
- */
-public final class Timer {
-
- public enum State {
- RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4), MISSED(5);
-
- /** The value assigned to this State in prior releases. */
- private final int mValue;
-
- State(int value) {
- mValue = value;
- }
-
- /**
- * @return the numeric value assigned to this state
- */
- public int getValue() {
- return mValue;
- }
-
- /**
- * @return the state corresponding to the given {@code value}
- */
- public static State fromValue(int value) {
- for (State state : values()) {
- if (state.getValue() == value) {
- return state;
- }
- }
-
- return null;
- }
- }
-
- /** The minimum duration of a timer. */
- public static final long MIN_LENGTH = SECOND_IN_MILLIS;
-
- /** The maximum duration of a new timer created via the user interface. */
- static final long MAX_LENGTH =
- 99 * HOUR_IN_MILLIS + 99 * MINUTE_IN_MILLIS + 99 * SECOND_IN_MILLIS;
-
- static final long UNUSED = Long.MIN_VALUE;
-
- /** A unique identifier for the timer. */
- private final int mId;
-
- /** The current state of the timer. */
- private final State mState;
-
- /** The original length of the timer in milliseconds when it was created. */
- private final long mLength;
-
- /** The length of the timer in milliseconds including additional time added by the user. */
- private final long mTotalLength;
-
- /** The time at which the timer was last started; {@link #UNUSED} when not running. */
- private final long mLastStartTime;
-
- /** The time since epoch at which the timer was last started. */
- private final long mLastStartWallClockTime;
-
- /** The time at which the timer is scheduled to expire; negative if it is already expired. */
- private final long mRemainingTime;
-
- /** A message describing the meaning of the timer. */
- private final String mLabel;
-
- /** A flag indicating the timer should be deleted when it is reset. */
- private final boolean mDeleteAfterUse;
-
- Timer(int id, State state, long length, long totalLength, long lastStartTime,
- long lastWallClockTime, long remainingTime, String label, boolean deleteAfterUse) {
- mId = id;
- mState = state;
- mLength = length;
- mTotalLength = totalLength;
- mLastStartTime = lastStartTime;
- mLastStartWallClockTime = lastWallClockTime;
- mRemainingTime = remainingTime;
- mLabel = label;
- mDeleteAfterUse = deleteAfterUse;
- }
-
- public int getId() { return mId; }
- public State getState() { return mState; }
- public String getLabel() { return mLabel; }
- public long getLength() { return mLength; }
- public long getTotalLength() { return mTotalLength; }
- public boolean getDeleteAfterUse() { return mDeleteAfterUse; }
- public boolean isReset() { return mState == RESET; }
- public boolean isRunning() { return mState == RUNNING; }
- public boolean isPaused() { return mState == PAUSED; }
- public boolean isExpired() { return mState == EXPIRED; }
- public boolean isMissed() { return mState == MISSED; }
-
- /**
- * @return the amount of remaining time when the timer was last started or paused.
- */
- public long getLastRemainingTime() {
- return mRemainingTime;
- }
-
- /**
- * @return the total amount of time remaining up to this moment; expired and missed timers will
- * return a negative amount
- */
- public long getRemainingTime() {
- if (mState == PAUSED || mState == RESET) {
- return mRemainingTime;
- }
-
- // In practice, "now" can be any value due to device reboots. When the real-time clock
- // is reset, there is no more guarantee that "now" falls after the last start time. To
- // ensure the timer is monotonically decreasing, normalize negative time segments to 0,
- final long timeSinceStart = now() - mLastStartTime;
- return mRemainingTime - Math.max(0, timeSinceStart);
- }
-
- /**
- * @return the elapsed realtime at which this timer will or did expire
- */
- public long getExpirationTime() {
- if (mState != RUNNING && mState != EXPIRED && mState != MISSED) {
- throw new IllegalStateException("cannot compute expiration time in state " + mState);
- }
-
- return mLastStartTime + mRemainingTime;
- }
-
- /**
- * @return the wall clock time at which this timer will or did expire
- */
- public long getWallClockExpirationTime() {
- if (mState != RUNNING && mState != EXPIRED && mState != MISSED) {
- throw new IllegalStateException("cannot compute expiration time in state " + mState);
- }
-
- return mLastStartWallClockTime + mRemainingTime;
- }
-
- /**
- *
- * @return the total amount of time elapsed up to this moment; expired timers will report more
- * than the {@link #getTotalLength() total length}
- */
- public long getElapsedTime() {
- return getTotalLength() - getRemainingTime();
- }
-
- long getLastStartTime() { return mLastStartTime; }
- long getLastWallClockTime() { return mLastStartWallClockTime; }
-
- /**
- * @return a copy of this timer that is running, expired or missed
- */
- Timer start() {
- if (mState == RUNNING || mState == EXPIRED || mState == MISSED) {
- return this;
- }
-
- return new Timer(mId, RUNNING, mLength, mTotalLength, now(), wallClock(), mRemainingTime,
- mLabel, mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer that is paused or reset
- */
- Timer pause() {
- if (mState == PAUSED || mState == RESET) {
- return this;
- } else if (mState == EXPIRED || mState == MISSED) {
- return reset();
- }
-
- final long remainingTime = getRemainingTime();
- return new Timer(mId, PAUSED, mLength, mTotalLength, UNUSED, UNUSED, remainingTime, mLabel,
- mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer that is expired, missed or reset
- */
- Timer expire() {
- if (mState == EXPIRED || mState == RESET || mState == MISSED) {
- return this;
- }
-
- final long remainingTime = Math.min(0L, getRemainingTime());
- return new Timer(mId, EXPIRED, mLength, 0L, now(), wallClock(), remainingTime, mLabel,
- mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer that is missed or reset
- */
- Timer miss() {
- if (mState == RESET || mState == MISSED) {
- return this;
- }
-
- final long remainingTime = Math.min(0L, getRemainingTime());
- return new Timer(mId, MISSED, mLength, 0L, now(), wallClock(), remainingTime, mLabel,
- mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer that is reset
- */
- Timer reset() {
- if (mState == RESET) {
- return this;
- }
-
- return new Timer(mId, RESET, mLength, mLength, UNUSED, UNUSED, mLength, mLabel,
- mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer that has its times adjusted after a reboot
- */
- Timer updateAfterReboot() {
- if (mState == RESET || mState == PAUSED) {
- return this;
- }
-
- final long timeSinceBoot = now();
- final long wallClockTime = wallClock();
- // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
- // update the recorded times and proceed with no change in accumulated time.
- final long delta = Math.max(0, wallClockTime - mLastStartWallClockTime);
- final long remainingTime = mRemainingTime - delta;
- return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime,
- remainingTime, mLabel, mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer that has its times adjusted after time has been set
- */
- Timer updateAfterTimeSet() {
- if (mState == RESET || mState == PAUSED) {
- return this;
- }
-
- final long timeSinceBoot = now();
- final long wallClockTime = wallClock();
- final long delta = timeSinceBoot - mLastStartTime;
- final long remainingTime = mRemainingTime - delta;
- if (delta < 0) {
- // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
- // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
- // updateAfterReboot() can successfully correct the data at a later time.
- return this;
- }
- return new Timer(mId, mState, mLength, mTotalLength, timeSinceBoot, wallClockTime,
- remainingTime, mLabel, mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer with the given {@code label}
- */
- Timer setLabel(String label) {
- if (TextUtils.equals(mLabel, label)) {
- return this;
- }
-
- return new Timer(mId, mState, mLength, mTotalLength, mLastStartTime,
- mLastStartWallClockTime, mRemainingTime, label, mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer with the given {@code length} or this timer if the length could
- * not be legally adjusted
- */
- Timer setLength(long length) {
- if (mLength == length || length <= Timer.MIN_LENGTH) {
- return this;
- }
-
- final long totalLength;
- final long remainingTime;
- if (mState == RESET) {
- totalLength = length;
- remainingTime = length;
- } else {
- totalLength = mTotalLength;
- remainingTime = mRemainingTime;
- }
-
- return new Timer(mId, mState, length, totalLength, mLastStartTime,
- mLastStartWallClockTime, remainingTime, mLabel, mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer with the given {@code remainingTime} or this timer if the
- * remaining time could not be legally adjusted
- */
- Timer setRemainingTime(long remainingTime) {
- // Do not change the remaining time of a reset timer.
- if (mRemainingTime == remainingTime || mState == RESET) {
- return this;
- }
-
- final long delta = remainingTime - mRemainingTime;
- final long totalLength = mTotalLength + delta;
-
- final long lastStartTime;
- final long lastWallClockTime;
- final State state;
- if (remainingTime > 0 && (mState == EXPIRED || mState == MISSED)) {
- state = RUNNING;
- lastStartTime = now();
- lastWallClockTime = wallClock();
- } else {
- state = mState;
- lastStartTime = mLastStartTime;
- lastWallClockTime = mLastStartWallClockTime;
- }
-
- return new Timer(mId, state, mLength, totalLength, lastStartTime,
- lastWallClockTime, remainingTime, mLabel, mDeleteAfterUse);
- }
-
- /**
- * @return a copy of this timer with an additional minute added to the remaining time and total
- * length, or this Timer if the minute could not be added
- */
- Timer addMinute() {
- // Expired and missed timers restart with 60 seconds of remaining time.
- if (mState == EXPIRED || mState == MISSED) {
- return setRemainingTime(MINUTE_IN_MILLIS);
- }
-
- // Otherwise try to add a minute to the remaining time.
- return setRemainingTime(mRemainingTime + MINUTE_IN_MILLIS);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- final Timer timer = (Timer) o;
-
- return mId == timer.mId;
- }
-
- @Override
- public int hashCode() {
- return mId;
- }
-
- /**
- * Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top.
- */
- static Comparator<Timer> ID_COMPARATOR = new Comparator<Timer>() {
- @Override
- public int compare(Timer timer1, Timer timer2) {
- return Integer.compare(timer2.getId(), timer1.getId());
- }
- };
-
- /**
- * Orders timers by their expected/actual expiration time. The general order is:
- *
- * <ol>
- * <li>{@link State#MISSED MISSED} timers; ties broken by {@link #getRemainingTime()}</li>
- * <li>{@link State#EXPIRED EXPIRED} timers; ties broken by {@link #getRemainingTime()}</li>
- * <li>{@link State#RUNNING RUNNING} timers; ties broken by {@link #getRemainingTime()}</li>
- * <li>{@link State#PAUSED PAUSED} timers; ties broken by {@link #getRemainingTime()}</li>
- * <li>{@link State#RESET RESET} timers; ties broken by {@link #getLength()}</li>
- * </ol>
- */
- static Comparator<Timer> EXPIRY_COMPARATOR = new Comparator<Timer>() {
-
- private final List<State> stateExpiryOrder = Arrays.asList(MISSED, EXPIRED, RUNNING, PAUSED,
- RESET);
-
- @Override
- public int compare(Timer timer1, Timer timer2) {
- final int stateIndex1 = stateExpiryOrder.indexOf(timer1.getState());
- final int stateIndex2 = stateExpiryOrder.indexOf(timer2.getState());
-
- int order = Integer.compare(stateIndex1, stateIndex2);
- if (order == 0) {
- final State state = timer1.getState();
- if (state == RESET) {
- order = Long.compare(timer1.getLength(), timer2.getLength());
- } else {
- order = Long.compare(timer1.getRemainingTime(), timer2.getRemainingTime());
- }
- }
-
- return order;
- }
- };
-}
diff --git a/src/com/android/deskclock/data/Timer.kt b/src/com/android/deskclock/data/Timer.kt
new file mode 100644
index 0000000..f19b75a
--- /dev/null
+++ b/src/com/android/deskclock/data/Timer.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.text.TextUtils
+import android.text.format.DateUtils.HOUR_IN_MILLIS
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.SECOND_IN_MILLIS
+
+import com.android.deskclock.Utils
+
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * A read-only domain object representing a countdown timer.
+ */
+class Timer internal constructor(
+ /** A unique identifier for the timer. */
+ val id: Int,
+ /** The current state of the timer. */
+ val state: State,
+ /** The original length of the timer in milliseconds when it was created. */
+ val length: Long,
+ /** The length of the timer in milliseconds including additional time added by the user. */
+ val totalLength: Long,
+ /** The time at which the timer was last started; [.UNUSED] when not running. */
+ val lastStartTime: Long,
+ /** The time since epoch at which the timer was last started. */
+ val lastWallClockTime: Long,
+ /** The time at which the timer is scheduled to expire; negative if it is already expired. */
+ val lastRemainingTime: Long,
+ /** A message describing the meaning of the timer. */
+ val label: String?,
+ /** A flag indicating the timer should be deleted when it is reset. */
+ val deleteAfterUse: Boolean
+) {
+
+ enum class State(
+ /** The value assigned to this State in prior releases. */
+ val value: Int
+ ) {
+ RUNNING(1), PAUSED(2), EXPIRED(3), RESET(4), MISSED(5);
+
+ companion object {
+ /**
+ * @return the state corresponding to the given `value`
+ */
+ fun fromValue(value: Int): State? {
+ for (state in values()) {
+ if (state.value == value) {
+ return state
+ }
+ }
+ return null
+ }
+ }
+ }
+
+ val isReset: Boolean
+ get() = state == State.RESET
+
+ val isRunning: Boolean
+ get() = state == State.RUNNING
+
+ val isPaused: Boolean
+ get() = state == State.PAUSED
+
+ val isExpired: Boolean
+ get() = state == State.EXPIRED
+
+ val isMissed: Boolean
+ get() = state == State.MISSED
+
+ /**
+ * @return the total amount of time remaining up to this moment; expired and missed timers will
+ * return a negative amount
+ */
+ val remainingTime: Long
+ get() {
+ if (state == State.PAUSED || state == State.RESET) {
+ return lastRemainingTime
+ }
+
+ // In practice, "now" can be any value due to device reboots. When the real-time clock
+ // is reset, there is no more guarantee that "now" falls after the last start time. To
+ // ensure the timer is monotonically decreasing, normalize negative time segments to 0,
+ val timeSinceStart = Utils.now() - lastStartTime
+ return lastRemainingTime - max(0, timeSinceStart)
+ }
+
+ /**
+ * @return the elapsed realtime at which this timer will or did expire
+ */
+ val expirationTime: Long
+ get() {
+ check(!(state != State.RUNNING && state != State.EXPIRED && state != State.MISSED)) {
+ "cannot compute expiration time in state $state"
+ }
+ return lastStartTime + lastRemainingTime
+ }
+
+ /**
+ * @return the wall clock time at which this timer will or did expire
+ */
+ val wallClockExpirationTime: Long
+ get() {
+ check(!(state != State.RUNNING && state != State.EXPIRED && state != State.MISSED)) {
+ "cannot compute expiration time in state $state"
+ }
+ return lastWallClockTime + lastRemainingTime
+ }
+
+ /**
+ *
+ * @return the total amount of time elapsed up to this moment; expired timers will report more
+ * than the [total length][.getTotalLength]
+ */
+ val elapsedTime: Long
+ get() = totalLength - remainingTime
+
+ /**
+ * @return a copy of this timer that is running, expired or missed
+ */
+ fun start(): Timer {
+ return if (state == State.RUNNING || state == State.EXPIRED || state == State.MISSED) {
+ this
+ } else {
+ Timer(id, State.RUNNING, length, totalLength,
+ Utils.now(), Utils.wallClock(), lastRemainingTime, label, deleteAfterUse)
+ }
+ }
+
+ /**
+ * @return a copy of this timer that is paused or reset
+ */
+ fun pause(): Timer {
+ if (state == State.PAUSED || state == State.RESET) {
+ return this
+ } else if (state == State.EXPIRED || state == State.MISSED) {
+ return reset()
+ }
+
+ val remainingTime = this.remainingTime
+ return Timer(id, State.PAUSED, length, totalLength, UNUSED, UNUSED, remainingTime, label,
+ deleteAfterUse)
+ }
+
+ /**
+ * @return a copy of this timer that is expired, missed or reset
+ */
+ fun expire(): Timer {
+ if (state == State.EXPIRED || state == State.RESET || state == State.MISSED) {
+ return this
+ }
+
+ val remainingTime = min(0L, lastRemainingTime)
+ return Timer(id, State.EXPIRED, length, 0L, Utils.now(),
+ Utils.wallClock(), remainingTime, label, deleteAfterUse)
+ }
+
+ /**
+ * @return a copy of this timer that is missed or reset
+ */
+ fun miss(): Timer {
+ if (state == State.RESET || state == State.MISSED) {
+ return this
+ }
+
+ val remainingTime = min(0L, lastRemainingTime)
+ return Timer(id, State.MISSED, length, 0L, Utils.now(),
+ Utils.wallClock(), remainingTime, label, deleteAfterUse)
+ }
+
+ /**
+ * @return a copy of this timer that is reset
+ */
+ fun reset(): Timer {
+ return if (state == State.RESET) {
+ this
+ } else {
+ Timer(id, State.RESET, length, length, UNUSED, UNUSED, length, label,
+ deleteAfterUse)
+ }
+ }
+
+ /**
+ * @return a copy of this timer that has its times adjusted after a reboot
+ */
+ fun updateAfterReboot(): Timer {
+ if (state == State.RESET || state == State.PAUSED) {
+ return this
+ }
+ val timeSinceBoot = Utils.now()
+ val wallClockTime = Utils.wallClock()
+ // Avoid negative time deltas. They can happen in practice, but they can't be used. Simply
+ // update the recorded times and proceed with no change in accumulated time.
+ val delta = max(0, wallClockTime - lastWallClockTime)
+ val remainingTime = lastRemainingTime - delta
+ return Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
+ remainingTime, label, deleteAfterUse)
+ }
+
+ /**
+ * @return a copy of this timer that has its times adjusted after time has been set
+ */
+ fun updateAfterTimeSet(): Timer {
+ if (state == State.RESET || state == State.PAUSED) {
+ return this
+ }
+ val timeSinceBoot = Utils.now()
+ val wallClockTime = Utils.wallClock()
+ val delta = timeSinceBoot - lastStartTime
+ val remainingTime = lastRemainingTime - delta
+ return if (delta < 0) {
+ // Avoid negative time deltas. They typically happen following reboots when TIME_SET is
+ // broadcast before BOOT_COMPLETED. Simply ignore the time update and hope
+ // updateAfterReboot() can successfully correct the data at a later time.
+ this
+ } else {
+ Timer(id, state, length, totalLength, timeSinceBoot, wallClockTime,
+ remainingTime, label, deleteAfterUse)
+ }
+ }
+
+ /**
+ * @return a copy of this timer with the given `label`
+ */
+ fun setLabel(label: String?): Timer {
+ return if (TextUtils.equals(this.label, label)) {
+ this
+ } else {
+ Timer(id, state, length, totalLength, lastStartTime,
+ lastWallClockTime, lastRemainingTime, label, deleteAfterUse)
+ }
+ }
+
+ /**
+ * @return a copy of this timer with the given `length` or this timer if the length could
+ * not be legally adjusted
+ */
+ fun setLength(length: Long): Timer {
+ if (this.length == length || length <= MIN_LENGTH) {
+ return this
+ }
+
+ val totalLength: Long
+ val remainingTime: Long
+ if (state == State.RESET) {
+ totalLength = length
+ remainingTime = length
+ } else {
+ totalLength = this.totalLength
+ remainingTime = lastRemainingTime
+ }
+
+ return Timer(id, state, length, totalLength, lastStartTime,
+ lastWallClockTime, remainingTime, label, deleteAfterUse)
+ }
+
+ /**
+ * @return a copy of this timer with the given `remainingTime` or this timer if the
+ * remaining time could not be legally adjusted
+ */
+ fun setRemainingTime(remainingTime: Long): Timer {
+ // Do not change the remaining time of a reset timer.
+ if (lastRemainingTime == remainingTime || state == State.RESET) {
+ return this
+ }
+
+ val delta = remainingTime - lastRemainingTime
+ val totalLength = totalLength + delta
+
+ val lastStartTime: Long
+ val lastWallClockTime: Long
+ val state: State?
+ if (remainingTime > 0 && (this.state == State.EXPIRED || this.state == State.MISSED)) {
+ state = State.RUNNING
+ lastStartTime = Utils.now()
+ lastWallClockTime = Utils.wallClock()
+ } else {
+ state = this.state
+ lastStartTime = this.lastStartTime
+ lastWallClockTime = this.lastWallClockTime
+ }
+
+ return Timer(id, state, length, totalLength, lastStartTime,
+ lastWallClockTime, remainingTime, label, deleteAfterUse)
+ }
+
+ /**
+ * @return a copy of this timer with an additional minute added to the remaining time and total
+ * length, or this Timer if the minute could not be added
+ */
+ fun addMinute(): Timer {
+ return if (state == State.EXPIRED || state == State.MISSED) {
+ // Expired and missed timers restart with 60 seconds of remaining time.
+ setRemainingTime(MINUTE_IN_MILLIS)
+ } else {
+ // Otherwise try to add a minute to the remaining time.
+ setRemainingTime(lastRemainingTime + MINUTE_IN_MILLIS)
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+
+ val timer = other as Timer
+
+ return id == timer.id
+ }
+
+ override fun hashCode(): Int {
+ return id
+ }
+
+ companion object {
+ /** The minimum duration of a timer. */
+ @JvmField
+ val MIN_LENGTH: Long = SECOND_IN_MILLIS
+
+ /** The maximum duration of a new timer created via the user interface. */
+ val MAX_LENGTH: Long = 99 * HOUR_IN_MILLIS + 99 * MINUTE_IN_MILLIS + 99 * SECOND_IN_MILLIS
+
+ const val UNUSED = Long.MIN_VALUE
+
+ /**
+ * Orders timers by their IDs. Oldest timers are at the bottom. Newest timers are at the top
+ */
+ @JvmField
+ var ID_COMPARATOR = Comparator<Timer> { timer1, timer2 -> timer2.id.compareTo(timer1.id) }
+
+ /**
+ * Orders timers by their expected/actual expiration time. The general order is:
+ *
+ * 1. [MISSED][State.MISSED] timers; ties broken by [.getRemainingTime]
+ * 2. [EXPIRED][State.EXPIRED] timers; ties broken by [.getRemainingTime]
+ * 3. [RUNNING][State.RUNNING] timers; ties broken by [.getRemainingTime]
+ * 4. [PAUSED][State.PAUSED] timers; ties broken by [.getRemainingTime]
+ * 5. [RESET][State.RESET] timers; ties broken by [.getLength]
+ *
+ */
+ @JvmField
+ var EXPIRY_COMPARATOR: Comparator<Timer> = object : Comparator<Timer> {
+ private val stateExpiryOrder =
+ listOf(State.MISSED, State.EXPIRED, State.RUNNING, State.PAUSED, State.RESET)
+
+ override fun compare(timer1: Timer, timer2: Timer): Int {
+ val stateIndex1 = stateExpiryOrder.indexOf(timer1.state)
+ val stateIndex2 = stateExpiryOrder.indexOf(timer2.state)
+
+ var order = stateIndex1.compareTo(stateIndex2)
+ if (order == 0) {
+ val state = timer1.state
+ order = if (state == State.RESET) {
+ timer1.length.compareTo(timer2.length)
+ } else {
+ timer1.lastRemainingTime.compareTo(timer2.lastRemainingTime)
+ }
+ }
+
+ return order
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerDAO.java b/src/com/android/deskclock/data/TimerDAO.java
deleted file mode 100644
index 32d36d9..0000000
--- a/src/com/android/deskclock/data/TimerDAO.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.SharedPreferences;
-
-import com.android.deskclock.data.Timer.State;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import static com.android.deskclock.data.Timer.State.RESET;
-
-/**
- * This class encapsulates the transfer of data between {@link Timer} domain objects and their
- * permanent storage in {@link SharedPreferences}.
- */
-final class TimerDAO {
-
- /** Key to a preference that stores the set of timer ids. */
- private static final String TIMER_IDS = "timers_list";
-
- /** Key to a preference that stores the id to assign to the next timer. */
- private static final String NEXT_TIMER_ID = "next_timer_id";
-
- /** Prefix for a key to a preference that stores the state of the timer. */
- private static final String STATE = "timer_state_";
-
- /** Prefix for a key to a preference that stores the original timer length at creation. */
- private static final String LENGTH = "timer_setup_timet_";
-
- /** Prefix for a key to a preference that stores the total timer length with additions. */
- private static final String TOTAL_LENGTH = "timer_original_timet_";
-
- /** Prefix for a key to a preference that stores the last start time of the timer. */
- private static final String LAST_START_TIME = "timer_start_time_";
-
- /** Prefix for a key to a preference that stores the epoch time when the timer last started. */
- private static final String LAST_WALL_CLOCK_TIME = "timer_wall_clock_time_";
-
- /** Prefix for a key to a preference that stores the remaining time before expiry. */
- private static final String REMAINING_TIME = "timer_time_left_";
-
- /** Prefix for a key to a preference that stores the label of the timer. */
- private static final String LABEL = "timer_label_";
-
- /** Prefix for a key to a preference that signals the timer should be deleted on first reset. */
- private static final String DELETE_AFTER_USE = "delete_after_use_";
-
- private TimerDAO() {}
-
- /**
- * @return the timers from permanent storage
- */
- static List<Timer> getTimers(SharedPreferences prefs) {
- // Read the set of timer ids.
- final Set<String> timerIds = prefs.getStringSet(TIMER_IDS, Collections.<String>emptySet());
- final List<Timer> timers = new ArrayList<>(timerIds.size());
-
- // Build a timer using the data associated with each timer id.
- for (String timerId : timerIds) {
- final int id = Integer.parseInt(timerId);
- final int stateValue = prefs.getInt(STATE + id, RESET.getValue());
- final State state = State.fromValue(stateValue);
-
- // Timer state may be null when migrating timers from prior releases which defined a
- // "deleted" state. Such a state is no longer required.
- if (state != null) {
- final long length = prefs.getLong(LENGTH + id, Long.MIN_VALUE);
- final long totalLength = prefs.getLong(TOTAL_LENGTH + id, Long.MIN_VALUE);
- final long lastStartTime = prefs.getLong(LAST_START_TIME + id, Timer.UNUSED);
- final long lastWallClockTime = prefs.getLong(LAST_WALL_CLOCK_TIME + id,
- Timer.UNUSED);
- final long remainingTime = prefs.getLong(REMAINING_TIME + id, totalLength);
- final String label = prefs.getString(LABEL + id, null);
- final boolean deleteAfterUse = prefs.getBoolean(DELETE_AFTER_USE + id, false);
- timers.add(new Timer(id, state, length, totalLength, lastStartTime,
- lastWallClockTime, remainingTime, label, deleteAfterUse));
- }
- }
-
- return timers;
- }
-
- /**
- * @param timer the timer to be added
- */
- static Timer addTimer(SharedPreferences prefs, Timer timer) {
- final SharedPreferences.Editor editor = prefs.edit();
-
- // Fetch the next timer id.
- final int id = prefs.getInt(NEXT_TIMER_ID, 0);
- editor.putInt(NEXT_TIMER_ID, id + 1);
-
- // Add the new timer id to the set of all timer ids.
- final Set<String> timerIds = new HashSet<>(getTimerIds(prefs));
- timerIds.add(String.valueOf(id));
- editor.putStringSet(TIMER_IDS, timerIds);
-
- // Record the fields of the timer.
- editor.putInt(STATE + id, timer.getState().getValue());
- editor.putLong(LENGTH + id, timer.getLength());
- editor.putLong(TOTAL_LENGTH + id, timer.getTotalLength());
- editor.putLong(LAST_START_TIME + id, timer.getLastStartTime());
- editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.getLastWallClockTime());
- editor.putLong(REMAINING_TIME + id, timer.getRemainingTime());
- editor.putString(LABEL + id, timer.getLabel());
- editor.putBoolean(DELETE_AFTER_USE + id, timer.getDeleteAfterUse());
-
- editor.apply();
-
- // Return a new timer with the generated timer id present.
- return new Timer(id, timer.getState(), timer.getLength(), timer.getTotalLength(),
- timer.getLastStartTime(), timer.getLastWallClockTime(), timer.getRemainingTime(),
- timer.getLabel(), timer.getDeleteAfterUse());
- }
-
- /**
- * @param timer the timer to be updated
- */
- static void updateTimer(SharedPreferences prefs, Timer timer) {
- final SharedPreferences.Editor editor = prefs.edit();
-
- // Record the fields of the timer.
- final int id = timer.getId();
- editor.putInt(STATE + id, timer.getState().getValue());
- editor.putLong(LENGTH + id, timer.getLength());
- editor.putLong(TOTAL_LENGTH + id, timer.getTotalLength());
- editor.putLong(LAST_START_TIME + id, timer.getLastStartTime());
- editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.getLastWallClockTime());
- editor.putLong(REMAINING_TIME + id, timer.getRemainingTime());
- editor.putString(LABEL + id, timer.getLabel());
- editor.putBoolean(DELETE_AFTER_USE + id, timer.getDeleteAfterUse());
-
- editor.apply();
- }
-
- /**
- * @param timer the timer to be removed
- */
- static void removeTimer(SharedPreferences prefs, Timer timer) {
- final SharedPreferences.Editor editor = prefs.edit();
-
- final int id = timer.getId();
-
- // Remove the timer id from the set of all timer ids.
- final Set<String> timerIds = new HashSet<>(getTimerIds(prefs));
- timerIds.remove(String.valueOf(id));
- if (timerIds.isEmpty()) {
- editor.remove(TIMER_IDS);
- editor.remove(NEXT_TIMER_ID);
- } else {
- editor.putStringSet(TIMER_IDS, timerIds);
- }
-
- // Record the fields of the timer.
- editor.remove(STATE + id);
- editor.remove(LENGTH + id);
- editor.remove(TOTAL_LENGTH + id);
- editor.remove(LAST_START_TIME + id);
- editor.remove(LAST_WALL_CLOCK_TIME + id);
- editor.remove(REMAINING_TIME + id);
- editor.remove(LABEL + id);
- editor.remove(DELETE_AFTER_USE + id);
-
- editor.apply();
- }
-
- private static Set<String> getTimerIds(SharedPreferences prefs) {
- return prefs.getStringSet(TIMER_IDS, Collections.<String>emptySet());
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerDAO.kt b/src/com/android/deskclock/data/TimerDAO.kt
new file mode 100644
index 0000000..137c450
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerDAO.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.SharedPreferences
+
+/**
+ * This class encapsulates the transfer of data between [Timer] domain objects and their
+ * permanent storage in [SharedPreferences].
+ */
+internal object TimerDAO {
+ /** Key to a preference that stores the set of timer ids. */
+ private const val TIMER_IDS = "timers_list"
+
+ /** Key to a preference that stores the id to assign to the next timer. */
+ private const val NEXT_TIMER_ID = "next_timer_id"
+
+ /** Prefix for a key to a preference that stores the state of the timer. */
+ private const val STATE = "timer_state_"
+
+ /** Prefix for a key to a preference that stores the original timer length at creation. */
+ private const val LENGTH = "timer_setup_timet_"
+
+ /** Prefix for a key to a preference that stores the total timer length with additions. */
+ private const val TOTAL_LENGTH = "timer_original_timet_"
+
+ /** Prefix for a key to a preference that stores the last start time of the timer. */
+ private const val LAST_START_TIME = "timer_start_time_"
+
+ /** Prefix for a key to a preference that stores the epoch time when the timer last started. */
+ private const val LAST_WALL_CLOCK_TIME = "timer_wall_clock_time_"
+
+ /** Prefix for a key to a preference that stores the remaining time before expiry. */
+ private const val REMAINING_TIME = "timer_time_left_"
+
+ /** Prefix for a key to a preference that stores the label of the timer. */
+ private const val LABEL = "timer_label_"
+
+ /** Prefix for a key to a preference that signals the timer should be deleted on first reset. */
+ private const val DELETE_AFTER_USE = "delete_after_use_"
+
+ /**
+ * @return the timers from permanent storage
+ */
+ @JvmStatic
+ fun getTimers(prefs: SharedPreferences): MutableList<Timer> {
+ // Read the set of timer ids.
+ val timerIds: Set<String> = prefs.getStringSet(TIMER_IDS, emptySet<String>())!!
+ val timers: MutableList<Timer> = ArrayList(timerIds.size)
+
+ // Build a timer using the data associated with each timer id.
+ for (timerId in timerIds) {
+ val id = timerId.toInt()
+ val stateValue: Int = prefs.getInt(STATE + id, Timer.State.RESET.value)
+ val state: Timer.State? = Timer.State.fromValue(stateValue)
+
+ // Timer state may be null when migrating timers from prior releases which defined a
+ // "deleted" state. Such a state is no longer required.
+ state?.let {
+ val length: Long = prefs.getLong(LENGTH + id, Long.MIN_VALUE)
+ val totalLength: Long = prefs.getLong(TOTAL_LENGTH + id, Long.MIN_VALUE)
+ val lastStartTime: Long = prefs.getLong(LAST_START_TIME + id, Timer.UNUSED)
+ val lastWallClockTime: Long = prefs.getLong(LAST_WALL_CLOCK_TIME + id, Timer.UNUSED)
+ val remainingTime: Long = prefs.getLong(REMAINING_TIME + id, totalLength)
+ val label: String? = prefs.getString(LABEL + id, null)
+ val deleteAfterUse: Boolean = prefs.getBoolean(DELETE_AFTER_USE + id, false)
+ timers.add(Timer(id, it, length, totalLength, lastStartTime,
+ lastWallClockTime, remainingTime, label, deleteAfterUse))
+ }
+ }
+
+ return timers
+ }
+
+ /**
+ * @param timer the timer to be added
+ */
+ @JvmStatic
+ fun addTimer(prefs: SharedPreferences, timer: Timer): Timer {
+ val editor: SharedPreferences.Editor = prefs.edit()
+
+ // Fetch the next timer id.
+ val id: Int = prefs.getInt(NEXT_TIMER_ID, 0)
+ editor.putInt(NEXT_TIMER_ID, id + 1)
+
+ // Add the new timer id to the set of all timer ids.
+ val timerIds: MutableSet<String> = HashSet(getTimerIds(prefs))
+ timerIds.add(id.toString())
+ editor.putStringSet(TIMER_IDS, timerIds)
+
+ // Record the fields of the timer.
+ editor.putInt(STATE + id, timer.state.value)
+ editor.putLong(LENGTH + id, timer.length)
+ editor.putLong(TOTAL_LENGTH + id, timer.totalLength)
+ editor.putLong(LAST_START_TIME + id, timer.lastStartTime)
+ editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.lastWallClockTime)
+ editor.putLong(REMAINING_TIME + id, timer.remainingTime)
+ editor.putString(LABEL + id, timer.label)
+ editor.putBoolean(DELETE_AFTER_USE + id, timer.deleteAfterUse)
+
+ editor.apply()
+
+ // Return a new timer with the generated timer id present.
+ return Timer(id, timer.state, timer.length, timer.totalLength,
+ timer.lastStartTime, timer.lastWallClockTime, timer.remainingTime,
+ timer.label, timer.deleteAfterUse)
+ }
+
+ /**
+ * @param timer the timer to be updated
+ */
+ @JvmStatic
+ fun updateTimer(prefs: SharedPreferences, timer: Timer) {
+ val editor: SharedPreferences.Editor = prefs.edit()
+
+ // Record the fields of the timer.
+ val id = timer.id
+ editor.putInt(STATE + id, timer.state.value)
+ editor.putLong(LENGTH + id, timer.length)
+ editor.putLong(TOTAL_LENGTH + id, timer.totalLength)
+ editor.putLong(LAST_START_TIME + id, timer.lastStartTime)
+ editor.putLong(LAST_WALL_CLOCK_TIME + id, timer.lastWallClockTime)
+ editor.putLong(REMAINING_TIME + id, timer.remainingTime)
+ editor.putString(LABEL + id, timer.label)
+ editor.putBoolean(DELETE_AFTER_USE + id, timer.deleteAfterUse)
+
+ editor.apply()
+ }
+
+ /**
+ * @param timer the timer to be removed
+ */
+ @JvmStatic
+ fun removeTimer(prefs: SharedPreferences, timer: Timer) {
+ val editor: SharedPreferences.Editor = prefs.edit()
+ val id = timer.id
+
+ // Remove the timer id from the set of all timer ids.
+ val timerIds: MutableSet<String> = HashSet(getTimerIds(prefs))
+ timerIds.remove(id.toString())
+ if (timerIds.isEmpty()) {
+ editor.remove(TIMER_IDS)
+ editor.remove(NEXT_TIMER_ID)
+ } else {
+ editor.putStringSet(TIMER_IDS, timerIds)
+ }
+
+ // Record the fields of the timer.
+ editor.remove(STATE + id)
+ editor.remove(LENGTH + id)
+ editor.remove(TOTAL_LENGTH + id)
+ editor.remove(LAST_START_TIME + id)
+ editor.remove(LAST_WALL_CLOCK_TIME + id)
+ editor.remove(REMAINING_TIME + id)
+ editor.remove(LABEL + id)
+ editor.remove(DELETE_AFTER_USE + id)
+
+ editor.apply()
+ }
+
+ private fun getTimerIds(prefs: SharedPreferences): Set<String> {
+ return prefs.getStringSet(TIMER_IDS, emptySet<String>())!!
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerListener.java b/src/com/android/deskclock/data/TimerListener.kt
similarity index 78%
rename from src/com/android/deskclock/data/TimerListener.java
rename to src/com/android/deskclock/data/TimerListener.kt
index a2f1d80..9a3d8b0 100644
--- a/src/com/android/deskclock/data/TimerListener.java
+++ b/src/com/android/deskclock/data/TimerListener.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,26 +14,25 @@
* limitations under the License.
*/
-package com.android.deskclock.data;
+package com.android.deskclock.data
/**
* The interface through which interested parties are notified of changes to one of the timers.
*/
-public interface TimerListener {
-
+interface TimerListener {
/**
* @param timer the timer that was added
*/
- void timerAdded(Timer timer);
+ fun timerAdded(timer: Timer)
/**
* @param before the timer state before the update
* @param after the timer state after the update
*/
- void timerUpdated(Timer before, Timer after);
+ fun timerUpdated(before: Timer, after: Timer)
/**
* @param timer the timer that was removed
*/
- void timerRemoved(Timer timer);
+ fun timerRemoved(timer: Timer)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerModel.java b/src/com/android/deskclock/data/TimerModel.java
deleted file mode 100644
index 54dfeab..0000000
--- a/src/com/android/deskclock/data/TimerModel.java
+++ /dev/null
@@ -1,846 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.annotation.SuppressLint;
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.net.Uri;
-import androidx.annotation.StringRes;
-import androidx.core.app.NotificationManagerCompat;
-import android.util.ArraySet;
-
-import com.android.deskclock.AlarmAlertWakeLock;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.settings.SettingsActivity;
-import com.android.deskclock.timer.TimerKlaxon;
-import com.android.deskclock.timer.TimerService;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static com.android.deskclock.data.Timer.State.EXPIRED;
-import static com.android.deskclock.data.Timer.State.RESET;
-
-/**
- * All {@link Timer} data is accessed via this model.
- */
-final class TimerModel {
-
- /**
- * Running timers less than this threshold are left running/expired; greater than this
- * threshold are considered missed.
- */
- private static final long MISSED_THRESHOLD = -MINUTE_IN_MILLIS;
-
- private final Context mContext;
-
- private final SharedPreferences mPrefs;
-
- /** The alarm manager system service that calls back when timers expire. */
- private final AlarmManager mAlarmManager;
-
- /** The model from which settings are fetched. */
- private final SettingsModel mSettingsModel;
-
- /** The model from which notification data are fetched. */
- private final NotificationModel mNotificationModel;
-
- /** The model from which ringtone data are fetched. */
- private final RingtoneModel mRingtoneModel;
-
- /** Used to create and destroy system notifications related to timers. */
- private final NotificationManagerCompat mNotificationManager;
-
- /** Update timer notification when locale changes. */
- @SuppressWarnings("FieldCanBeLocal")
- private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
- /**
- * Retain a hard reference to the shared preference observer to prevent it from being garbage
- * collected. See {@link SharedPreferences#registerOnSharedPreferenceChangeListener} for detail.
- */
- @SuppressWarnings("FieldCanBeLocal")
- private final OnSharedPreferenceChangeListener mPreferenceListener = new PreferenceListener();
-
- /** The listeners to notify when a timer is added, updated or removed. */
- private final List<TimerListener> mTimerListeners = new ArrayList<>();
-
- /** Delegate that builds platform-specific timer notifications. */
- private final TimerNotificationBuilder mNotificationBuilder = new TimerNotificationBuilder();
-
- /**
- * The ids of expired timers for which the ringer is ringing. Not all expired timers have their
- * ids in this collection. If a timer was already expired when the app was started its id will
- * be absent from this collection.
- */
- @SuppressLint("NewApi")
- private final Set<Integer> mRingingIds = new ArraySet<>();
-
- /** The uri of the ringtone to play for timers. */
- private Uri mTimerRingtoneUri;
-
- /** The title of the ringtone to play for timers. */
- private String mTimerRingtoneTitle;
-
- /** A mutable copy of the timers. */
- private List<Timer> mTimers;
-
- /** A mutable copy of the expired timers. */
- private List<Timer> mExpiredTimers;
-
- /** A mutable copy of the missed timers. */
- private List<Timer> mMissedTimers;
-
- /**
- * The service that keeps this application in the foreground while a heads-up timer
- * notification is displayed. Marking the service as foreground prevents the operating system
- * from killing this application while expired timers are actively firing.
- */
- private Service mService;
-
- TimerModel(Context context, SharedPreferences prefs, SettingsModel settingsModel,
- RingtoneModel ringtoneModel, NotificationModel notificationModel) {
- mContext = context;
- mPrefs = prefs;
- mSettingsModel = settingsModel;
- mRingtoneModel = ringtoneModel;
- mNotificationModel = notificationModel;
- mNotificationManager = NotificationManagerCompat.from(context);
-
- mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
-
- // Clear caches affected by preferences when preferences change.
- prefs.registerOnSharedPreferenceChangeListener(mPreferenceListener);
-
- // Update timer notification when locale changes.
- final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
- mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
- }
-
- /**
- * @param timerListener to be notified when timers are added, updated and removed
- */
- void addTimerListener(TimerListener timerListener) {
- mTimerListeners.add(timerListener);
- }
-
- /**
- * @param timerListener to no longer be notified when timers are added, updated and removed
- */
- void removeTimerListener(TimerListener timerListener) {
- mTimerListeners.remove(timerListener);
- }
-
- /**
- * @return all defined timers in their creation order
- */
- List<Timer> getTimers() {
- return Collections.unmodifiableList(getMutableTimers());
- }
-
- /**
- * @return all expired timers in their expiration order
- */
- List<Timer> getExpiredTimers() {
- return Collections.unmodifiableList(getMutableExpiredTimers());
- }
-
- /**
- * @return all missed timers in their expiration order
- */
- private List<Timer> getMissedTimers() {
- return Collections.unmodifiableList(getMutableMissedTimers());
- }
-
- /**
- * @param timerId identifies the timer to return
- * @return the timer with the given {@code timerId}
- */
- Timer getTimer(int timerId) {
- for (Timer timer : getMutableTimers()) {
- if (timer.getId() == timerId) {
- return timer;
- }
- }
-
- return null;
- }
-
- /**
- * @return the timer that last expired and is still expired now; {@code null} if no timers are
- * expired
- */
- Timer getMostRecentExpiredTimer() {
- final List<Timer> timers = getMutableExpiredTimers();
- return timers.isEmpty() ? null : timers.get(timers.size() - 1);
- }
-
- /**
- * @param length the length of the timer in milliseconds
- * @param label describes the purpose of the timer
- * @param deleteAfterUse {@code true} indicates the timer should be deleted when it is reset
- * @return the newly added timer
- */
- Timer addTimer(long length, String label, boolean deleteAfterUse) {
- // Create the timer instance.
- Timer timer = new Timer(-1, RESET, length, length, Timer.UNUSED, Timer.UNUSED, length,
- label, deleteAfterUse);
-
- // Add the timer to permanent storage.
- timer = TimerDAO.addTimer(mPrefs, timer);
-
- // Add the timer to the cache.
- getMutableTimers().add(0, timer);
-
- // Update the timer notification.
- updateNotification();
- // Heads-Up notification is unaffected by this change
-
- // Notify listeners of the change.
- for (TimerListener timerListener : mTimerListeners) {
- timerListener.timerAdded(timer);
- }
-
- return timer;
- }
-
- /**
- * @param service used to start foreground notifications related to expired timers
- * @param timer the timer to be expired
- */
- void expireTimer(Service service, Timer timer) {
- if (mService == null) {
- // If this is the first expired timer, retain the service that will be used to start
- // the heads-up notification in the foreground.
- mService = service;
- } else if (mService != service) {
- // If this is not the first expired timer, the service should match the one given when
- // the first timer expired.
- LogUtils.wtf("Expected TimerServices to be identical");
- }
-
- updateTimer(timer.expire());
- }
-
- /**
- * @param timer an updated timer to store
- */
- void updateTimer(Timer timer) {
- final Timer before = doUpdateTimer(timer);
-
- // Update the notification after updating the timer data.
- updateNotification();
-
- // If the timer started or stopped being expired, update the heads-up notification.
- if (before.getState() != timer.getState()) {
- if (before.isExpired() || timer.isExpired()) {
- updateHeadsUpNotification();
- }
- }
- }
-
- /**
- * @param timer an existing timer to be removed
- */
- void removeTimer(Timer timer) {
- doRemoveTimer(timer);
-
- // Update the timer notifications after removing the timer data.
- if (timer.isExpired()) {
- updateHeadsUpNotification();
- } else {
- updateNotification();
- }
- }
-
- /**
- * If the given {@code timer} is expired and marked for deletion after use then this method
- * removes the the timer. The timer is otherwise transitioned to the reset state and continues
- * to exist.
- *
- * @param timer the timer to be reset
- * @param allowDelete {@code true} if the timer is allowed to be deleted instead of reset
- * (e.g. one use timers)
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- * @return the reset {@code timer} or {@code null} if the timer was deleted
- */
- Timer resetTimer(Timer timer, boolean allowDelete, @StringRes int eventLabelId) {
- final Timer result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId);
-
- // Update the notification after updating the timer data.
- if (timer.isMissed()) {
- updateMissedNotification();
- } else if (timer.isExpired()) {
- updateHeadsUpNotification();
- } else {
- updateNotification();
- }
-
- return result;
- }
-
- /**
- * Update timers after system reboot.
- */
- void updateTimersAfterReboot() {
- final List<Timer> timers = new ArrayList<>(getTimers());
- for (Timer timer : timers) {
- doUpdateAfterRebootTimer(timer);
- }
-
- // Update the notifications once after all timers are updated.
- updateNotification();
- updateMissedNotification();
- updateHeadsUpNotification();
- }
-
- /**
- * Update timers after time set.
- */
- void updateTimersAfterTimeSet() {
- final List<Timer> timers = new ArrayList<>(getTimers());
- for (Timer timer : timers) {
- doUpdateAfterTimeSetTimer(timer);
- }
-
- // Update the notifications once after all timers are updated.
- updateNotification();
- updateMissedNotification();
- updateHeadsUpNotification();
- }
-
- /**
- * Reset all expired timers. Exactly one parameter should be filled, with preference given to
- * eventLabelId.
- *
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- */
- void resetOrDeleteExpiredTimers(@StringRes int eventLabelId) {
- final List<Timer> timers = new ArrayList<>(getTimers());
- for (Timer timer : timers) {
- if (timer.isExpired()) {
- doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
- }
- }
-
- // Update the notifications once after all timers are updated.
- updateHeadsUpNotification();
- }
-
- /**
- * Reset all missed timers.
- *
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- */
- void resetMissedTimers(@StringRes int eventLabelId) {
- final List<Timer> timers = new ArrayList<>(getTimers());
- for (Timer timer : timers) {
- if (timer.isMissed()) {
- doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
- }
- }
-
- // Update the notifications once after all timers are updated.
- updateMissedNotification();
- }
-
- /**
- * Reset all unexpired timers.
- *
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- */
- void resetUnexpiredTimers(@StringRes int eventLabelId) {
- final List<Timer> timers = new ArrayList<>(getTimers());
- for (Timer timer : timers) {
- if (timer.isRunning() || timer.isPaused()) {
- doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId);
- }
- }
-
- // Update the notification once after all timers are updated.
- updateNotification();
- // Heads-Up notification is unaffected by this change
- }
-
- /**
- * @return the uri of the default ringtone to play for all timers when no user selection exists
- */
- Uri getDefaultTimerRingtoneUri() {
- return mSettingsModel.getDefaultTimerRingtoneUri();
- }
-
- /**
- * @return {@code true} iff the ringtone to play for all timers is the silent ringtone
- */
- boolean isTimerRingtoneSilent() {
- return Uri.EMPTY.equals(getTimerRingtoneUri());
- }
-
- /**
- * @return the uri of the ringtone to play for all timers
- */
- Uri getTimerRingtoneUri() {
- if (mTimerRingtoneUri == null) {
- mTimerRingtoneUri = mSettingsModel.getTimerRingtoneUri();
- }
-
- return mTimerRingtoneUri;
- }
-
- /**
- * @param uri the uri of the ringtone to play for all timers
- */
- void setTimerRingtoneUri(Uri uri) {
- mSettingsModel.setTimerRingtoneUri(uri);
- }
-
- /**
- * @return the title of the ringtone that is played for all timers
- */
- String getTimerRingtoneTitle() {
- if (mTimerRingtoneTitle == null) {
- if (isTimerRingtoneSilent()) {
- // Special case: no ringtone has a title of "Silent".
- mTimerRingtoneTitle = mContext.getString(R.string.silent_ringtone_title);
- } else {
- final Uri defaultUri = getDefaultTimerRingtoneUri();
- final Uri uri = getTimerRingtoneUri();
-
- if (defaultUri.equals(uri)) {
- // Special case: default ringtone has a title of "Timer Expired".
- mTimerRingtoneTitle = mContext.getString(R.string.default_timer_ringtone_title);
- } else {
- mTimerRingtoneTitle = mRingtoneModel.getRingtoneTitle(uri);
- }
- }
- }
-
- return mTimerRingtoneTitle;
- }
-
- /**
- * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
- * {@code 0} implies no crescendo should be applied
- */
- long getTimerCrescendoDuration() {
- return mSettingsModel.getTimerCrescendoDuration();
- }
-
- /**
- * @return {@code true} if the device vibrates when timers expire
- */
- boolean getTimerVibrate() {
- return mSettingsModel.getTimerVibrate();
- }
-
- /**
- * @param enabled {@code true} if the device should vibrate when timers expire
- */
- void setTimerVibrate(boolean enabled) {
- mSettingsModel.setTimerVibrate(enabled);
- }
-
- private List<Timer> getMutableTimers() {
- if (mTimers == null) {
- mTimers = TimerDAO.getTimers(mPrefs);
- Collections.sort(mTimers, Timer.ID_COMPARATOR);
- }
-
- return mTimers;
- }
-
- private List<Timer> getMutableExpiredTimers() {
- if (mExpiredTimers == null) {
- mExpiredTimers = new ArrayList<>();
-
- for (Timer timer : getMutableTimers()) {
- if (timer.isExpired()) {
- mExpiredTimers.add(timer);
- }
- }
- Collections.sort(mExpiredTimers, Timer.EXPIRY_COMPARATOR);
- }
-
- return mExpiredTimers;
- }
-
- private List<Timer> getMutableMissedTimers() {
- if (mMissedTimers == null) {
- mMissedTimers = new ArrayList<>();
-
- for (Timer timer : getMutableTimers()) {
- if (timer.isMissed()) {
- mMissedTimers.add(timer);
- }
- }
- Collections.sort(mMissedTimers, Timer.EXPIRY_COMPARATOR);
- }
-
- return mMissedTimers;
- }
-
- /**
- * This method updates timer data without updating notifications. This is useful in bulk-update
- * scenarios so the notifications are only rebuilt once.
- *
- * @param timer an updated timer to store
- * @return the state of the timer prior to the update
- */
- private Timer doUpdateTimer(Timer timer) {
- // Retrieve the cached form of the timer.
- final List<Timer> timers = getMutableTimers();
- final int index = timers.indexOf(timer);
- final Timer before = timers.get(index);
-
- // If no change occurred, ignore this update.
- if (timer == before) {
- return timer;
- }
-
- // Update the timer in permanent storage.
- TimerDAO.updateTimer(mPrefs, timer);
-
- // Update the timer in the cache.
- final Timer oldTimer = timers.set(index, timer);
-
- // Clear the cache of expired timers if the timer changed to/from expired.
- if (before.isExpired() || timer.isExpired()) {
- mExpiredTimers = null;
- }
- // Clear the cache of missed timers if the timer changed to/from missed.
- if (before.isMissed() || timer.isMissed()) {
- mMissedTimers = null;
- }
-
- // Update the timer expiration callback.
- updateAlarmManager();
-
- // Update the timer ringer.
- updateRinger(before, timer);
-
- // Notify listeners of the change.
- for (TimerListener timerListener : mTimerListeners) {
- timerListener.timerUpdated(before, timer);
- }
-
- return oldTimer;
- }
-
- /**
- * This method removes timer data without updating notifications. This is useful in bulk-remove
- * scenarios so the notifications are only rebuilt once.
- *
- * @param timer an existing timer to be removed
- */
- private void doRemoveTimer(Timer timer) {
- // Remove the timer from permanent storage.
- TimerDAO.removeTimer(mPrefs, timer);
-
- // Remove the timer from the cache.
- final List<Timer> timers = getMutableTimers();
- final int index = timers.indexOf(timer);
-
- // If the timer cannot be located there is nothing to remove.
- if (index == -1) {
- return;
- }
-
- timer = timers.remove(index);
-
- // Clear the cache of expired timers if a new expired timer was added.
- if (timer.isExpired()) {
- mExpiredTimers = null;
- }
-
- // Clear the cache of missed timers if a new missed timer was added.
- if (timer.isMissed()) {
- mMissedTimers = null;
- }
-
- // Update the timer expiration callback.
- updateAlarmManager();
-
- // Update the timer ringer.
- updateRinger(timer, null);
-
- // Notify listeners of the change.
- for (TimerListener timerListener : mTimerListeners) {
- timerListener.timerRemoved(timer);
- }
- }
-
- /**
- * This method updates/removes timer data without updating notifications. This is useful in
- * bulk-update scenarios so the notifications are only rebuilt once.
- *
- * If the given {@code timer} is expired and marked for deletion after use then this method
- * removes the the timer. The timer is otherwise transitioned to the reset state and continues
- * to exist.
- *
- * @param timer the timer to be reset
- * @param allowDelete {@code true} if the timer is allowed to be deleted instead of reset
- * (e.g. one use timers)
- * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
- * @return the reset {@code timer} or {@code null} if the timer was deleted
- */
- private Timer doResetOrDeleteTimer(Timer timer, boolean allowDelete,
- @StringRes int eventLabelId) {
- if (allowDelete
- && (timer.isExpired() || timer.isMissed())
- && timer.getDeleteAfterUse()) {
- doRemoveTimer(timer);
- if (eventLabelId != 0) {
- Events.sendTimerEvent(R.string.action_delete, eventLabelId);
- }
- return null;
- } else if (!timer.isReset()) {
- final Timer reset = timer.reset();
- doUpdateTimer(reset);
- if (eventLabelId != 0) {
- Events.sendTimerEvent(R.string.action_reset, eventLabelId);
- }
- return reset;
- }
-
- return timer;
- }
-
- /**
- * This method updates/removes timer data after a reboot without updating notifications.
- *
- * @param timer the timer to be updated
- */
- private void doUpdateAfterRebootTimer(Timer timer) {
- Timer updated = timer.updateAfterReboot();
- if (updated.getRemainingTime() < MISSED_THRESHOLD && updated.isRunning()) {
- updated = updated.miss();
- }
- doUpdateTimer(updated);
- }
-
- private void doUpdateAfterTimeSetTimer(Timer timer) {
- final Timer updated = timer.updateAfterTimeSet();
- doUpdateTimer(updated);
- }
-
-
- /**
- * Updates the callback given to this application from the {@link AlarmManager} that signals the
- * expiration of the next timer. If no timers are currently set to expire (i.e. no running
- * timers exist) then this method clears the expiration callback from AlarmManager.
- */
- private void updateAlarmManager() {
- // Locate the next firing timer if one exists.
- Timer nextExpiringTimer = null;
- for (Timer timer : getMutableTimers()) {
- if (timer.isRunning()) {
- if (nextExpiringTimer == null) {
- nextExpiringTimer = timer;
- } else if (timer.getExpirationTime() < nextExpiringTimer.getExpirationTime()) {
- nextExpiringTimer = timer;
- }
- }
- }
-
- // Build the intent that signals the timer expiration.
- final Intent intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer);
-
- if (nextExpiringTimer == null) {
- // Cancel the existing timer expiration callback.
- final PendingIntent pi = PendingIntent.getService(mContext,
- 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
- if (pi != null) {
- mAlarmManager.cancel(pi);
- pi.cancel();
- }
- } else {
- // Update the existing timer expiration callback.
- final PendingIntent pi = PendingIntent.getService(mContext,
- 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
- schedulePendingIntent(mAlarmManager, nextExpiringTimer.getExpirationTime(), pi);
- }
- }
-
- /**
- * Starts and stops the ringer for timers if the change to the timer demands it.
- *
- * @param before the state of the timer before the change; {@code null} indicates added
- * @param after the state of the timer after the change; {@code null} indicates delete
- */
- private void updateRinger(Timer before, Timer after) {
- // Retrieve the states before and after the change.
- final Timer.State beforeState = before == null ? null : before.getState();
- final Timer.State afterState = after == null ? null : after.getState();
-
- // If the timer state did not change, the ringer state is unchanged.
- if (beforeState == afterState) {
- return;
- }
-
- // If the timer is the first to expire, start ringing.
- if (afterState == EXPIRED && mRingingIds.add(after.getId()) && mRingingIds.size() == 1) {
- AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext);
- TimerKlaxon.start(mContext);
- }
-
- // If the expired timer was the last to reset, stop ringing.
- if (beforeState == EXPIRED && mRingingIds.remove(before.getId()) && mRingingIds.isEmpty()) {
- TimerKlaxon.stop(mContext);
- AlarmAlertWakeLock.releaseCpuLock();
- }
- }
-
- /**
- * Updates the notification controlling unexpired timers. This notification is only displayed
- * when the application is not open.
- */
- void updateNotification() {
- // Notifications should be hidden if the app is open.
- if (mNotificationModel.isApplicationInForeground()) {
- mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
- return;
- }
-
- // Filter the timers to just include unexpired ones.
- final List<Timer> unexpired = new ArrayList<>();
- for (Timer timer : getMutableTimers()) {
- if (timer.isRunning() || timer.isPaused()) {
- unexpired.add(timer);
- }
- }
-
- // If no unexpired timers exist, cancel the notification.
- if (unexpired.isEmpty()) {
- mNotificationManager.cancel(mNotificationModel.getUnexpiredTimerNotificationId());
- return;
- }
-
- // Sort the unexpired timers to locate the next one scheduled to expire.
- Collections.sort(unexpired, Timer.EXPIRY_COMPARATOR);
-
- // Otherwise build and post a notification reflecting the latest unexpired timers.
- final Notification notification =
- mNotificationBuilder.build(mContext, mNotificationModel, unexpired);
- final int notificationId = mNotificationModel.getUnexpiredTimerNotificationId();
- mNotificationBuilder.buildChannel(mContext, mNotificationManager);
- mNotificationManager.notify(notificationId, notification);
- }
-
- /**
- * Updates the notification controlling missed timers. This notification is only displayed when
- * the application is not open.
- */
- void updateMissedNotification() {
- // Notifications should be hidden if the app is open.
- if (mNotificationModel.isApplicationInForeground()) {
- mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
- return;
- }
-
- final List<Timer> missed = getMissedTimers();
-
- if (missed.isEmpty()) {
- mNotificationManager.cancel(mNotificationModel.getMissedTimerNotificationId());
- return;
- }
-
- final Notification notification = mNotificationBuilder.buildMissed(mContext,
- mNotificationModel, missed);
- final int notificationId = mNotificationModel.getMissedTimerNotificationId();
- mNotificationManager.notify(notificationId, notification);
- }
-
- /**
- * Updates the heads-up notification controlling expired timers. This heads-up notification is
- * displayed whether the application is open or not.
- */
- private void updateHeadsUpNotification() {
- // Nothing can be done with the heads-up notification without a valid service reference.
- if (mService == null) {
- return;
- }
-
- final List<Timer> expired = getExpiredTimers();
-
- // If no expired timers exist, stop the service (which cancels the foreground notification).
- if (expired.isEmpty()) {
- mService.stopSelf();
- mService = null;
- return;
- }
-
- // Otherwise build and post a foreground notification reflecting the latest expired timers.
- final Notification notification = mNotificationBuilder.buildHeadsUp(mContext, expired);
- final int notificationId = mNotificationModel.getExpiredTimerNotificationId();
- mService.startForeground(notificationId, notification);
- }
-
- /**
- * Update the timer notification in response to a locale change.
- */
- private final class LocaleChangedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- mTimerRingtoneTitle = null;
- updateNotification();
- updateMissedNotification();
- updateHeadsUpNotification();
- }
- }
-
- /**
- * This receiver is notified when shared preferences change. Cached information built on
- * preferences must be cleared.
- */
- private final class PreferenceListener implements OnSharedPreferenceChangeListener {
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- switch (key) {
- case SettingsActivity.KEY_TIMER_RINGTONE:
- mTimerRingtoneUri = null;
- mTimerRingtoneTitle = null;
- break;
- }
- }
- }
-
- static void schedulePendingIntent(AlarmManager am, long triggerTime, PendingIntent pi) {
- if (Utils.isMOrLater()) {
- // Ensure the timer fires even if the device is dozing.
- am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
- } else {
- am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi);
- }
- }
-}
diff --git a/src/com/android/deskclock/data/TimerModel.kt b/src/com/android/deskclock/data/TimerModel.kt
new file mode 100644
index 0000000..08f6fb1
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerModel.kt
@@ -0,0 +1,809 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.annotation.SuppressLint
+import android.app.AlarmManager
+import android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.SharedPreferences
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.net.Uri
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import androidx.annotation.StringRes
+import androidx.core.app.NotificationManagerCompat
+
+import com.android.deskclock.AlarmAlertWakeLock
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.events.Events
+import com.android.deskclock.settings.SettingsActivity
+import com.android.deskclock.timer.TimerKlaxon
+import com.android.deskclock.timer.TimerService
+
+/**
+ * All [Timer] data is accessed via this model.
+ */
+internal class TimerModel(
+ private val mContext: Context,
+ private val mPrefs: SharedPreferences,
+ /** The model from which settings are fetched. */
+ private val mSettingsModel: SettingsModel,
+ /** The model from which ringtone data are fetched. */
+ private val mRingtoneModel: RingtoneModel,
+ /** The model from which notification data are fetched. */
+ private val mNotificationModel: NotificationModel
+) {
+ /** The alarm manager system service that calls back when timers expire. */
+ private val mAlarmManager = mContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+
+ /** Used to create and destroy system notifications related to timers. */
+ private val mNotificationManager = NotificationManagerCompat.from(mContext)
+
+ /** Update timer notification when locale changes. */
+ private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+ /**
+ * Retain a hard reference to the shared preference observer to prevent it from being garbage
+ * collected. See [SharedPreferences.registerOnSharedPreferenceChangeListener] for detail.
+ */
+ private val mPreferenceListener: OnSharedPreferenceChangeListener = PreferenceListener()
+
+ /** The listeners to notify when a timer is added, updated or removed. */
+ private val mTimerListeners: MutableList<TimerListener> = mutableListOf()
+
+ /** Delegate that builds platform-specific timer notifications. */
+ private val mNotificationBuilder = TimerNotificationBuilder()
+
+ /**
+ * The ids of expired timers for which the ringer is ringing. Not all expired timers have their
+ * ids in this collection. If a timer was already expired when the app was started its id will
+ * be absent from this collection.
+ */
+ @SuppressLint("NewApi")
+ private val mRingingIds: MutableSet<Int> = mutableSetOf()
+
+ /** The uri of the ringtone to play for timers. */
+ private var mTimerRingtoneUri: Uri? = null
+
+ /** The title of the ringtone to play for timers. */
+ private var mTimerRingtoneTitle: String? = null
+
+ /** A mutable copy of the timers. */
+ private var mTimers: MutableList<Timer>? = null
+
+ /** A mutable copy of the expired timers. */
+ private var mExpiredTimers: MutableList<Timer>? = null
+
+ /** A mutable copy of the missed timers. */
+ private var mMissedTimers: MutableList<Timer>? = null
+
+ /**
+ * The service that keeps this application in the foreground while a heads-up timer
+ * notification is displayed. Marking the service as foreground prevents the operating system
+ * from killing this application while expired timers are actively firing.
+ */
+ private var mService: Service? = null
+
+ init {
+ // Clear caches affected by preferences when preferences change.
+ mPrefs.registerOnSharedPreferenceChangeListener(mPreferenceListener)
+
+ // Update timer notification when locale changes.
+ val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+ mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+ }
+
+ /**
+ * @param timerListener to be notified when timers are added, updated and removed
+ */
+ fun addTimerListener(timerListener: TimerListener) {
+ mTimerListeners.add(timerListener)
+ }
+
+ /**
+ * @param timerListener to no longer be notified when timers are added, updated and removed
+ */
+ fun removeTimerListener(timerListener: TimerListener) {
+ mTimerListeners.remove(timerListener)
+ }
+
+ /**
+ * @return all defined timers in their creation order
+ */
+ val timers: List<Timer>
+ get() = mutableTimers
+
+ /**
+ * @return all expired timers in their expiration order
+ */
+ val expiredTimers: List<Timer>
+ get() = mutableExpiredTimers
+
+ /**
+ * @return all missed timers in their expiration order
+ */
+ private val missedTimers: List<Timer>
+ get() = mutableMissedTimers
+
+ /**
+ * @param timerId identifies the timer to return
+ * @return the timer with the given `timerId`
+ */
+ fun getTimer(timerId: Int): Timer? {
+ for (timer in mutableTimers) {
+ if (timer.id == timerId) {
+ return timer
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * @return the timer that last expired and is still expired now; `null` if no timers are
+ * expired
+ */
+ val mostRecentExpiredTimer: Timer?
+ get() {
+ val timers = mutableExpiredTimers
+ return if (timers.isEmpty()) null else timers[timers.size - 1]
+ }
+
+ /**
+ * @param length the length of the timer in milliseconds
+ * @param label describes the purpose of the timer
+ * @param deleteAfterUse `true` indicates the timer should be deleted when it is reset
+ * @return the newly added timer
+ */
+ fun addTimer(length: Long, label: String?, deleteAfterUse: Boolean): Timer {
+ // Create the timer instance.
+ var timer =
+ Timer(-1, Timer.State.RESET, length, length, Timer.UNUSED, Timer.UNUSED, length,
+ label, deleteAfterUse)
+
+ // Add the timer to permanent storage.
+ timer = TimerDAO.addTimer(mPrefs, timer)
+
+ // Add the timer to the cache.
+ mutableTimers.add(0, timer)
+
+ // Update the timer notification.
+ updateNotification()
+ // Heads-Up notification is unaffected by this change
+
+ // Notify listeners of the change.
+ for (timerListener in mTimerListeners) {
+ timerListener.timerAdded(timer)
+ }
+
+ return timer
+ }
+
+ /**
+ * @param service used to start foreground notifications related to expired timers
+ * @param timer the timer to be expired
+ */
+ fun expireTimer(service: Service?, timer: Timer) {
+ if (mService == null) {
+ // If this is the first expired timer, retain the service that will be used to start
+ // the heads-up notification in the foreground.
+ mService = service
+ } else if (mService != service) {
+ // If this is not the first expired timer, the service should match the one given when
+ // the first timer expired.
+ LogUtils.wtf("Expected TimerServices to be identical")
+ }
+
+ updateTimer(timer.expire())
+ }
+
+ /**
+ * @param timer an updated timer to store
+ */
+ fun updateTimer(timer: Timer) {
+ val before = doUpdateTimer(timer)
+
+ // Update the notification after updating the timer data.
+ updateNotification()
+
+ // If the timer started or stopped being expired, update the heads-up notification.
+ if (before.state != timer.state) {
+ if (before.isExpired || timer.isExpired) {
+ updateHeadsUpNotification()
+ }
+ }
+ }
+
+ /**
+ * @param timer an existing timer to be removed
+ */
+ fun removeTimer(timer: Timer) {
+ doRemoveTimer(timer)
+
+ // Update the timer notifications after removing the timer data.
+ if (timer.isExpired) {
+ updateHeadsUpNotification()
+ } else {
+ updateNotification()
+ }
+ }
+
+ /**
+ * If the given `timer` is expired and marked for deletion after use then this method
+ * removes the timer. The timer is otherwise transitioned to the reset state and continues
+ * to exist.
+ *
+ * @param timer the timer to be reset
+ * @param allowDelete `true` if the timer is allowed to be deleted instead of reset
+ * (e.g. one use timers)
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ * @return the reset `timer` or `null` if the timer was deleted
+ */
+ fun resetTimer(timer: Timer, allowDelete: Boolean, @StringRes eventLabelId: Int): Timer? {
+ val result = doResetOrDeleteTimer(timer, allowDelete, eventLabelId)
+
+ // Update the notification after updating the timer data.
+ when {
+ timer.isMissed -> updateMissedNotification()
+ timer.isExpired -> updateHeadsUpNotification()
+ else -> updateNotification()
+ }
+
+ return result
+ }
+
+ /**
+ * Update timers after system reboot.
+ */
+ fun updateTimersAfterReboot() {
+ for (timer in timers) {
+ doUpdateAfterRebootTimer(timer)
+ }
+
+ // Update the notifications once after all timers are updated.
+ updateNotification()
+ updateMissedNotification()
+ updateHeadsUpNotification()
+ }
+
+ /**
+ * Update timers after time set.
+ */
+ fun updateTimersAfterTimeSet() {
+ for (timer in timers) {
+ doUpdateAfterTimeSetTimer(timer)
+ }
+
+ // Update the notifications once after all timers are updated.
+ updateNotification()
+ updateMissedNotification()
+ updateHeadsUpNotification()
+ }
+
+ /**
+ * Reset all expired timers. Exactly one parameter should be filled, with preference given to
+ * eventLabelId.
+ *
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ */
+ fun resetOrDeleteExpiredTimers(@StringRes eventLabelId: Int) {
+ for (timer in timers) {
+ if (timer.isExpired) {
+ doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId)
+ }
+ }
+
+ // Update the notifications once after all timers are updated.
+ updateHeadsUpNotification()
+ }
+
+ /**
+ * Reset all missed timers.
+ *
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ */
+ fun resetMissedTimers(@StringRes eventLabelId: Int) {
+ for (timer in timers) {
+ if (timer.isMissed) {
+ doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId)
+ }
+ }
+
+ // Update the notifications once after all timers are updated.
+ updateMissedNotification()
+ }
+
+ /**
+ * Reset all unexpired timers.
+ *
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ */
+ fun resetUnexpiredTimers(@StringRes eventLabelId: Int) {
+ for (timer in timers) {
+ if (timer.isRunning || timer.isPaused) {
+ doResetOrDeleteTimer(timer, true /* allowDelete */, eventLabelId)
+ }
+ }
+
+ // Update the notification once after all timers are updated.
+ updateNotification()
+ // Heads-Up notification is unaffected by this change
+ }
+
+ /**
+ * @return the uri of the default ringtone to play for all timers when no user selection exists
+ */
+ val defaultTimerRingtoneUri: Uri
+ get() = mSettingsModel.defaultTimerRingtoneUri
+
+ /**
+ * @return `true` iff the ringtone to play for all timers is the silent ringtone
+ */
+ val isTimerRingtoneSilent: Boolean
+ get() = Uri.EMPTY.equals(timerRingtoneUri)
+
+ var timerRingtoneUri: Uri
+ /**
+ * @return the uri of the ringtone to play for all timers
+ */
+ get() {
+ if (mTimerRingtoneUri == null) {
+ mTimerRingtoneUri = mSettingsModel.timerRingtoneUri
+ }
+
+ return mTimerRingtoneUri!!
+ }
+ /**
+ * @param uri the uri of the ringtone to play for all timers
+ */
+ set(uri) {
+ mSettingsModel.timerRingtoneUri = uri
+ }
+
+ /**
+ * @return the title of the ringtone that is played for all timers
+ */
+ val timerRingtoneTitle: String
+ get() {
+ if (mTimerRingtoneTitle == null) {
+ mTimerRingtoneTitle = if (isTimerRingtoneSilent) {
+ // Special case: no ringtone has a title of "Silent".
+ mContext.getString(R.string.silent_ringtone_title)
+ } else {
+ val defaultUri: Uri = defaultTimerRingtoneUri
+ val uri: Uri = timerRingtoneUri
+ if (defaultUri.equals(uri)) {
+ // Special case: default ringtone has a title of "Timer Expired".
+ mContext.getString(R.string.default_timer_ringtone_title)
+ } else {
+ mRingtoneModel.getRingtoneTitle(uri)
+ }
+ }
+ }
+
+ return mTimerRingtoneTitle!!
+ }
+
+ /**
+ * @return the duration, in milliseconds, of the crescendo to apply to timer ringtone playback;
+ * `0` implies no crescendo should be applied
+ */
+ val timerCrescendoDuration: Long
+ get() = mSettingsModel.timerCrescendoDuration
+
+ var timerVibrate: Boolean
+ /**
+ * @return `true` if the device vibrates when timers expire
+ */
+ get() = mSettingsModel.timerVibrate
+ /**
+ * @param enabled `true` if the device should vibrate when timers expire
+ */
+ set(enabled) {
+ mSettingsModel.timerVibrate = enabled
+ }
+
+ private val mutableTimers: MutableList<Timer>
+ get() {
+ if (mTimers == null) {
+ mTimers = TimerDAO.getTimers(mPrefs)
+ mTimers!!.sortWith(Timer.ID_COMPARATOR)
+ }
+
+ return mTimers!!
+ }
+
+ private val mutableExpiredTimers: List<Timer>
+ get() {
+ if (mExpiredTimers == null) {
+ mExpiredTimers = mutableListOf()
+ for (timer in mutableTimers) {
+ if (timer.isExpired) {
+ mExpiredTimers!!.add(timer)
+ }
+ }
+ mExpiredTimers!!.sortWith(Timer.EXPIRY_COMPARATOR)
+ }
+
+ return mExpiredTimers!!
+ }
+
+ private val mutableMissedTimers: List<Timer>
+ get() {
+ if (mMissedTimers == null) {
+ mMissedTimers = mutableListOf()
+ for (timer in mutableTimers) {
+ if (timer.isMissed) {
+ mMissedTimers!!.add(timer)
+ }
+ }
+ mMissedTimers!!.sortWith(Timer.EXPIRY_COMPARATOR)
+ }
+
+ return mMissedTimers!!
+ }
+
+ /**
+ * This method updates timer data without updating notifications. This is useful in bulk-update
+ * scenarios so the notifications are only rebuilt once.
+ *
+ * @param timer an updated timer to store
+ * @return the state of the timer prior to the update
+ */
+ private fun doUpdateTimer(timer: Timer): Timer {
+ // Retrieve the cached form of the timer.
+ val timers = mutableTimers
+ val index = timers.indexOf(timer)
+ val before = timers[index]
+
+ // If no change occurred, ignore this update.
+ if (timer === before) {
+ return timer
+ }
+
+ // Update the timer in permanent storage.
+ TimerDAO.updateTimer(mPrefs, timer)
+
+ // Update the timer in the cache.
+ val oldTimer = timers.set(index, timer)
+
+ // Clear the cache of expired timers if the timer changed to/from expired.
+ if (before.isExpired || timer.isExpired) {
+ mExpiredTimers = null
+ }
+ // Clear the cache of missed timers if the timer changed to/from missed.
+ if (before.isMissed || timer.isMissed) {
+ mMissedTimers = null
+ }
+
+ // Update the timer expiration callback.
+ updateAlarmManager()
+
+ // Update the timer ringer.
+ updateRinger(before, timer)
+
+ // Notify listeners of the change.
+ for (timerListener in mTimerListeners) {
+ timerListener.timerUpdated(before, timer)
+ }
+
+ return oldTimer
+ }
+
+ /**
+ * This method removes timer data without updating notifications. This is useful in bulk-remove
+ * scenarios so the notifications are only rebuilt once.
+ *
+ * @param timer an existing timer to be removed
+ */
+ private fun doRemoveTimer(timer: Timer) {
+ // Remove the timer from permanent storage.
+ var timerVar = timer
+ TimerDAO.removeTimer(mPrefs, timerVar)
+
+ // Remove the timer from the cache.
+ val timers: MutableList<Timer> = mutableTimers
+ val index = timers.indexOf(timerVar)
+
+ // If the timer cannot be located there is nothing to remove.
+ if (index == -1) {
+ return
+ }
+ timerVar = timers.removeAt(index)
+
+ // Clear the cache of expired timers if a new expired timer was added.
+ if (timerVar.isExpired) {
+ mExpiredTimers = null
+ }
+
+ // Clear the cache of missed timers if a new missed timer was added.
+ if (timerVar.isMissed) {
+ mMissedTimers = null
+ }
+
+ // Update the timer expiration callback.
+ updateAlarmManager()
+
+ // Update the timer ringer.
+ updateRinger(timerVar, null)
+
+ // Notify listeners of the change.
+ for (timerListener in mTimerListeners) {
+ timerListener.timerRemoved(timerVar)
+ }
+ }
+
+ /**
+ * This method updates/removes timer data without updating notifications. This is useful in
+ * bulk-update scenarios so the notifications are only rebuilt once.
+ *
+ * If the given `timer` is expired and marked for deletion after use then this method
+ * removes the timer. The timer is otherwise transitioned to the reset state and continues
+ * to exist.
+ *
+ * @param timer the timer to be reset
+ * @param allowDelete `true` if the timer is allowed to be deleted instead of reset
+ * (e.g. one use timers)
+ * @param eventLabelId the label of the timer event to send; 0 if no event should be sent
+ * @return the reset `timer` or `null` if the timer was deleted
+ */
+ private fun doResetOrDeleteTimer(
+ timer: Timer,
+ allowDelete: Boolean,
+ @StringRes eventLabelId: Int
+ ): Timer? {
+ if (allowDelete &&
+ (timer.isExpired || timer.isMissed) &&
+ timer.deleteAfterUse) {
+ doRemoveTimer(timer)
+ if (eventLabelId != 0) {
+ Events.sendTimerEvent(R.string.action_delete, eventLabelId)
+ }
+ return null
+ } else if (!timer.isReset) {
+ val reset = timer.reset()
+ doUpdateTimer(reset)
+ if (eventLabelId != 0) {
+ Events.sendTimerEvent(R.string.action_reset, eventLabelId)
+ }
+ return reset
+ }
+ return timer
+ }
+
+ /**
+ * This method updates/removes timer data after a reboot without updating notifications.
+ *
+ * @param timer the timer to be updated
+ */
+ private fun doUpdateAfterRebootTimer(timer: Timer) {
+ var updated = timer.updateAfterReboot()
+ if (updated.remainingTime < MISSED_THRESHOLD && updated.isRunning) {
+ updated = updated.miss()
+ }
+ doUpdateTimer(updated)
+ }
+
+ private fun doUpdateAfterTimeSetTimer(timer: Timer) {
+ val updated = timer.updateAfterTimeSet()
+ doUpdateTimer(updated)
+ }
+
+ /**
+ * Updates the callback given to this application from the [AlarmManager] that signals the
+ * expiration of the next timer. If no timers are currently set to expire (i.e. no running
+ * timers exist) then this method clears the expiration callback from AlarmManager.
+ */
+ private fun updateAlarmManager() {
+ // Locate the next firing timer if one exists.
+ var nextExpiringTimer: Timer? = null
+ for (timer in mutableTimers) {
+ if (timer.isRunning) {
+ if (nextExpiringTimer == null) {
+ nextExpiringTimer = timer
+ } else if (timer.expirationTime < nextExpiringTimer.expirationTime) {
+ nextExpiringTimer = timer
+ }
+ }
+ }
+
+ // Build the intent that signals the timer expiration.
+ val intent: Intent = TimerService.createTimerExpiredIntent(mContext, nextExpiringTimer)
+ if (nextExpiringTimer == null) {
+ // Cancel the existing timer expiration callback.
+ val pi: PendingIntent? = PendingIntent.getService(mContext,
+ 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE)
+ if (pi != null) {
+ mAlarmManager.cancel(pi)
+ pi.cancel()
+ }
+ } else {
+ // Update the existing timer expiration callback.
+ val pi: PendingIntent = PendingIntent.getService(mContext,
+ 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+ schedulePendingIntent(mAlarmManager, nextExpiringTimer.expirationTime, pi)
+ }
+ }
+
+ /**
+ * Starts and stops the ringer for timers if the change to the timer demands it.
+ *
+ * @param before the state of the timer before the change; `null` indicates added
+ * @param after the state of the timer after the change; `null` indicates delete
+ */
+ private fun updateRinger(before: Timer?, after: Timer?) {
+ // Retrieve the states before and after the change.
+ val beforeState = before?.state
+ val afterState = after?.state
+
+ // If the timer state did not change, the ringer state is unchanged.
+ if (beforeState == afterState) {
+ return
+ }
+
+ // If the timer is the first to expire, start ringing.
+ if (afterState == Timer.State.EXPIRED && mRingingIds.add(after.id) &&
+ mRingingIds.size == 1) {
+ AlarmAlertWakeLock.acquireScreenCpuWakeLock(mContext)
+ TimerKlaxon.start(mContext)
+ }
+
+ // If the expired timer was the last to reset, stop ringing.
+ if (beforeState == Timer.State.EXPIRED && mRingingIds.remove(before.id) &&
+ mRingingIds.isEmpty()) {
+ TimerKlaxon.stop(mContext)
+ AlarmAlertWakeLock.releaseCpuLock()
+ }
+ }
+
+ /**
+ * Updates the notification controlling unexpired timers. This notification is only displayed
+ * when the application is not open.
+ */
+ fun updateNotification() {
+ // Notifications should be hidden if the app is open.
+ if (mNotificationModel.isApplicationInForeground) {
+ mNotificationManager.cancel(mNotificationModel.unexpiredTimerNotificationId)
+ return
+ }
+
+ // Filter the timers to just include unexpired ones.
+ val unexpired: MutableList<Timer> = mutableListOf()
+ for (timer in mutableTimers) {
+ if (timer.isRunning || timer.isPaused) {
+ unexpired.add(timer)
+ }
+ }
+
+ // If no unexpired timers exist, cancel the notification.
+ if (unexpired.isEmpty()) {
+ mNotificationManager.cancel(mNotificationModel.unexpiredTimerNotificationId)
+ return
+ }
+
+ // Sort the unexpired timers to locate the next one scheduled to expire.
+ unexpired.sortWith(Timer.EXPIRY_COMPARATOR)
+
+ // Otherwise build and post a notification reflecting the latest unexpired timers.
+ val notification: Notification =
+ mNotificationBuilder.build(mContext, mNotificationModel, unexpired)
+ val notificationId = mNotificationModel.unexpiredTimerNotificationId
+ mNotificationBuilder.buildChannel(mContext, mNotificationManager)
+ mNotificationManager.notify(notificationId, notification)
+ }
+
+ /**
+ * Updates the notification controlling missed timers. This notification is only displayed when
+ * the application is not open.
+ */
+ fun updateMissedNotification() {
+ // Notifications should be hidden if the app is open.
+ if (mNotificationModel.isApplicationInForeground) {
+ mNotificationManager.cancel(mNotificationModel.missedTimerNotificationId)
+ return
+ }
+
+ val missed = missedTimers
+
+ if (missed.isEmpty()) {
+ mNotificationManager.cancel(mNotificationModel.missedTimerNotificationId)
+ return
+ }
+
+ val notification: Notification = mNotificationBuilder.buildMissed(mContext,
+ mNotificationModel, missed)
+ val notificationId = mNotificationModel.missedTimerNotificationId
+ mNotificationManager.notify(notificationId, notification)
+ }
+
+ /**
+ * Updates the heads-up notification controlling expired timers. This heads-up notification is
+ * displayed whether the application is open or not.
+ */
+ private fun updateHeadsUpNotification() {
+ // Nothing can be done with the heads-up notification without a valid service reference.
+ if (mService == null) {
+ return
+ }
+
+ val expired = expiredTimers
+
+ // If no expired timers exist, stop the service (which cancels the foreground notification).
+ if (expired.isEmpty()) {
+ mService!!.stopSelf()
+ mService = null
+ return
+ }
+
+ // Otherwise build and post a foreground notification reflecting the latest expired timers.
+ val notification: Notification = mNotificationBuilder.buildHeadsUp(mContext, expired)
+ val notificationId = mNotificationModel.expiredTimerNotificationId
+ mService!!.startForeground(notificationId, notification)
+ }
+
+ /**
+ * Update the timer notification in response to a locale change.
+ */
+ private inner class LocaleChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ mTimerRingtoneTitle = null
+ updateNotification()
+ updateMissedNotification()
+ updateHeadsUpNotification()
+ }
+ }
+
+ /**
+ * This receiver is notified when shared preferences change. Cached information built on
+ * preferences must be cleared.
+ */
+ private inner class PreferenceListener : OnSharedPreferenceChangeListener {
+ override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
+ when (key) {
+ SettingsActivity.KEY_TIMER_RINGTONE -> {
+ mTimerRingtoneUri = null
+ mTimerRingtoneTitle = null
+ }
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Running timers less than this threshold are left running/expired; greater than this
+ * threshold are considered missed.
+ */
+ private val MISSED_THRESHOLD: Long = -MINUTE_IN_MILLIS
+
+ fun schedulePendingIntent(am: AlarmManager, triggerTime: Long, pi: PendingIntent?) {
+ if (Utils.isMOrLater) {
+ // Ensure the timer fires even if the device is dozing.
+ am.setExactAndAllowWhileIdle(ELAPSED_REALTIME_WAKEUP, triggerTime, pi)
+ } else {
+ am.setExact(ELAPSED_REALTIME_WAKEUP, triggerTime, pi)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerNotificationBuilder.java b/src/com/android/deskclock/data/TimerNotificationBuilder.java
deleted file mode 100644
index efd4297..0000000
--- a/src/com/android/deskclock/data/TimerNotificationBuilder.java
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.annotation.TargetApi;
-import android.app.AlarmManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Build;
-import android.os.SystemClock;
-import androidx.annotation.DrawableRes;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-import android.text.TextUtils;
-import android.widget.RemoteViews;
-
-import com.android.deskclock.AlarmUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.timer.ExpiredTimersActivity;
-import com.android.deskclock.timer.TimerService;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static androidx.core.app.NotificationCompat.Action;
-import static androidx.core.app.NotificationCompat.Builder;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-
-/**
- * Builds notifications to reflect the latest state of the timers.
- */
-class TimerNotificationBuilder {
-
- /**
- * Notification channel containing all TimerModel notifications.
- */
- private static final String TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "TimerModelNotification";
-
- private static final int REQUEST_CODE_UPCOMING = 0;
- private static final int REQUEST_CODE_MISSING = 1;
-
- public void buildChannel(Context context, NotificationManagerCompat notificationManager) {
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(
- TIMER_MODEL_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.default_label),
- NotificationManagerCompat.IMPORTANCE_DEFAULT);
- notificationManager.createNotificationChannel(channel);
- }
- }
-
- public Notification build(Context context, NotificationModel nm, List<Timer> unexpired) {
- final Timer timer = unexpired.get(0);
- final int count = unexpired.size();
-
- // Compute some values required below.
- final boolean running = timer.isRunning();
- final Resources res = context.getResources();
-
- final long base = getChronometerBase(timer);
- final String pname = context.getPackageName();
-
- final List<Action> actions = new ArrayList<>(2);
-
- final CharSequence stateText;
- if (count == 1) {
- if (running) {
- // Single timer is running.
- if (TextUtils.isEmpty(timer.getLabel())) {
- stateText = res.getString(R.string.timer_notification_label);
- } else {
- stateText = timer.getLabel();
- }
-
- // Left button: Pause
- final Intent pause = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_PAUSE_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
- @DrawableRes final int icon1 = R.drawable.ic_pause_24dp;
- final CharSequence title1 = res.getText(R.string.timer_pause);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, pause);
- actions.add(new Action.Builder(icon1, title1, intent1).build());
-
- // Right Button: +1 Minute
- final Intent addMinute = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_ADD_MINUTE_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
- @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
- final CharSequence title2 = res.getText(R.string.timer_plus_1_min);
- final PendingIntent intent2 = Utils.pendingServiceIntent(context, addMinute);
- actions.add(new Action.Builder(icon2, title2, intent2).build());
-
- } else {
- // Single timer is paused.
- stateText = res.getString(R.string.timer_paused);
-
- // Left button: Start
- final Intent start = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_START_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
- @DrawableRes final int icon1 = R.drawable.ic_start_24dp;
- final CharSequence title1 = res.getText(R.string.sw_resume_button);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, start);
- actions.add(new Action.Builder(icon1, title1, intent1).build());
-
- // Right Button: Reset
- final Intent reset = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_RESET_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
- @DrawableRes final int icon2 = R.drawable.ic_reset_24dp;
- final CharSequence title2 = res.getText(R.string.sw_reset_button);
- final PendingIntent intent2 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Action.Builder(icon2, title2, intent2).build());
- }
- } else {
- if (running) {
- // At least one timer is running.
- stateText = res.getString(R.string.timers_in_use, count);
- } else {
- // All timers are paused.
- stateText = res.getString(R.string.timers_stopped, count);
- }
-
- final Intent reset = TimerService.createResetUnexpiredTimersIntent(context);
-
- @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
- final CharSequence title1 = res.getText(R.string.timer_reset_all);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
- actions.add(new Action.Builder(icon1, title1, intent1).build());
- }
-
- // Intent to load the app and show the timer when the notification is tapped.
- final Intent showApp = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_SHOW_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId())
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification);
-
- final PendingIntent pendingShowApp =
- PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- final Builder notification = new NotificationCompat.Builder(
- context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
- .setOngoing(true)
- .setLocalOnly(true)
- .setShowWhen(false)
- .setAutoCancel(false)
- .setContentIntent(pendingShowApp)
- .setPriority(Notification.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setSmallIcon(R.drawable.stat_notify_timer)
- .setSortKey(nm.getTimerNotificationSortKey())
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
- .setColor(ContextCompat.getColor(context, R.color.default_background));
-
- for (Action action : actions) {
- notification.addAction(action);
- }
-
- if (Utils.isNOrLater()) {
- notification.setCustomContentView(buildChronometer(pname, base, running, stateText))
- .setGroup(nm.getTimerNotificationGroupKey());
- } else {
- final CharSequence contentTextPreN;
- if (count == 1) {
- contentTextPreN = TimerStringFormatter.formatTimeRemaining(context,
- timer.getRemainingTime(), false);
- } else if (running) {
- final String timeRemaining = TimerStringFormatter.formatTimeRemaining(context,
- timer.getRemainingTime(), false);
- contentTextPreN = context.getString(R.string.next_timer_notif, timeRemaining);
- } else {
- contentTextPreN = context.getString(R.string.all_timers_stopped_notif);
- }
-
- notification.setContentTitle(stateText).setContentText(contentTextPreN);
-
- final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- final Intent updateNotification = TimerService.createUpdateNotificationIntent(context);
- final long remainingTime = timer.getRemainingTime();
- if (timer.isRunning() && remainingTime > MINUTE_IN_MILLIS) {
- // Schedule a callback to update the time-sensitive information of the running timer
- final PendingIntent pi =
- PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- final long nextMinuteChange = remainingTime % MINUTE_IN_MILLIS;
- final long triggerTime = SystemClock.elapsedRealtime() + nextMinuteChange;
- TimerModel.schedulePendingIntent(am, triggerTime, pi);
- } else {
- // Cancel the update notification callback.
- final PendingIntent pi = PendingIntent.getService(context, 0, updateNotification,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_NO_CREATE);
- if (pi != null) {
- am.cancel(pi);
- pi.cancel();
- }
- }
- }
-
- return notification.build();
- }
-
- Notification buildHeadsUp(Context context, List<Timer> expired) {
- final Timer timer = expired.get(0);
-
- // First action intent is to reset all timers.
- @DrawableRes final int icon1 = R.drawable.ic_stop_24dp;
- final Intent reset = TimerService.createResetExpiredTimersIntent(context);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
-
- // Generate some descriptive text, a title, and an action name based on the timer count.
- final CharSequence stateText;
- final int count = expired.size();
- final List<Action> actions = new ArrayList<>(2);
- if (count == 1) {
- final String label = timer.getLabel();
- if (TextUtils.isEmpty(label)) {
- stateText = context.getString(R.string.timer_times_up);
- } else {
- stateText = label;
- }
-
- // Left button: Reset single timer
- final CharSequence title1 = context.getString(R.string.timer_stop);
- actions.add(new Action.Builder(icon1, title1, intent1).build());
-
- // Right button: Add minute
- final Intent addTime = TimerService.createAddMinuteTimerIntent(context, timer.getId());
- final PendingIntent intent2 = Utils.pendingServiceIntent(context, addTime);
- @DrawableRes final int icon2 = R.drawable.ic_add_24dp;
- final CharSequence title2 = context.getString(R.string.timer_plus_1_min);
- actions.add(new Action.Builder(icon2, title2, intent2).build());
- } else {
- stateText = context.getString(R.string.timer_multi_times_up, count);
-
- // Left button: Reset all timers
- final CharSequence title1 = context.getString(R.string.timer_stop_all);
- actions.add(new Action.Builder(icon1, title1, intent1).build());
- }
-
- final long base = getChronometerBase(timer);
-
- final String pname = context.getPackageName();
-
- // Content intent shows the timer full screen when clicked.
- final Intent content = new Intent(context, ExpiredTimersActivity.class);
- final PendingIntent contentIntent = Utils.pendingActivityIntent(context, content);
-
- // Full screen intent has flags so it is different than the content intent.
- final Intent fullScreen = new Intent(context, ExpiredTimersActivity.class)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
- final PendingIntent pendingFullScreen = Utils.pendingActivityIntent(context, fullScreen);
-
- final Builder notification = new NotificationCompat.Builder(
- context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
- .setOngoing(true)
- .setLocalOnly(true)
- .setShowWhen(false)
- .setAutoCancel(false)
- .setContentIntent(contentIntent)
- .setPriority(Notification.PRIORITY_MAX)
- .setDefaults(Notification.DEFAULT_LIGHTS)
- .setSmallIcon(R.drawable.stat_notify_timer)
- .setFullScreenIntent(pendingFullScreen, true)
- .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
- .setColor(ContextCompat.getColor(context, R.color.default_background));
-
- for (Action action : actions) {
- notification.addAction(action);
- }
-
- if (Utils.isNOrLater()) {
- notification.setCustomContentView(buildChronometer(pname, base, true, stateText));
- } else {
- final CharSequence contentTextPreN = count == 1
- ? context.getString(R.string.timer_times_up)
- : context.getString(R.string.timer_multi_times_up, count);
-
- notification.setContentTitle(stateText).setContentText(contentTextPreN);
- }
-
- return notification.build();
- }
-
- Notification buildMissed(Context context, NotificationModel nm,
- List<Timer> missedTimers) {
- final Timer timer = missedTimers.get(0);
- final int count = missedTimers.size();
-
- // Compute some values required below.
- final long base = getChronometerBase(timer);
- final String pname = context.getPackageName();
- final Resources res = context.getResources();
-
- final Action action;
-
- final CharSequence stateText;
- if (count == 1) {
- // Single timer is missed.
- if (TextUtils.isEmpty(timer.getLabel())) {
- stateText = res.getString(R.string.missed_timer_notification_label);
- } else {
- stateText = res.getString(R.string.missed_named_timer_notification_label,
- timer.getLabel());
- }
-
- // Reset button
- final Intent reset = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_RESET_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId());
-
- @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
- final CharSequence title1 = res.getText(R.string.timer_reset);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
- action = new Action.Builder(icon1, title1, intent1).build();
- } else {
- // Multiple missed timers.
- stateText = res.getString(R.string.timer_multi_missed, count);
-
- final Intent reset = TimerService.createResetMissedTimersIntent(context);
-
- @DrawableRes final int icon1 = R.drawable.ic_reset_24dp;
- final CharSequence title1 = res.getText(R.string.timer_reset_all);
- final PendingIntent intent1 = Utils.pendingServiceIntent(context, reset);
- action = new Action.Builder(icon1, title1, intent1).build();
- }
-
- // Intent to load the app and show the timer when the notification is tapped.
- final Intent showApp = new Intent(context, TimerService.class)
- .setAction(TimerService.ACTION_SHOW_TIMER)
- .putExtra(TimerService.EXTRA_TIMER_ID, timer.getId())
- .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification);
-
- final PendingIntent pendingShowApp =
- PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- final Builder notification = new NotificationCompat.Builder(
- context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
- .setLocalOnly(true)
- .setShowWhen(false)
- .setAutoCancel(false)
- .setContentIntent(pendingShowApp)
- .setPriority(Notification.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_ALARM)
- .setSmallIcon(R.drawable.stat_notify_timer)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setSortKey(nm.getTimerNotificationMissedSortKey())
- .setStyle(new NotificationCompat.DecoratedCustomViewStyle())
- .addAction(action)
- .setColor(ContextCompat.getColor(context, R.color.default_background));
-
- if (Utils.isNOrLater()) {
- notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
- .setGroup(nm.getTimerNotificationGroupKey());
- } else {
- final CharSequence contentText = AlarmUtils.getFormattedTime(context,
- timer.getWallClockExpirationTime());
- notification.setContentText(contentText).setContentTitle(stateText);
- }
-
- return notification.build();
- }
-
- /**
- * @param timer the timer on which to base the chronometer display
- * @return the time at which the chronometer will/did reach 0:00 in realtime
- */
- private static long getChronometerBase(Timer timer) {
- // The in-app timer display rounds *up* to the next second for positive timer values. Mirror
- // that behavior in the notification's Chronometer by padding in an extra second as needed.
- final long remaining = timer.getRemainingTime();
- final long adjustedRemaining = remaining < 0 ? remaining : remaining + SECOND_IN_MILLIS;
-
- // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now.
- return SystemClock.elapsedRealtime() + adjustedRemaining;
- }
-
- @TargetApi(Build.VERSION_CODES.N)
- private RemoteViews buildChronometer(String pname, long base, boolean running,
- CharSequence stateText) {
- final RemoteViews content = new RemoteViews(pname, R.layout.chronometer_notif_content);
- content.setChronometerCountDown(R.id.chronometer, true);
- content.setChronometer(R.id.chronometer, base, null, running);
- content.setTextViewText(R.id.state, stateText);
- return content;
- }
-}
diff --git a/src/com/android/deskclock/data/TimerNotificationBuilder.kt b/src/com/android/deskclock/data/TimerNotificationBuilder.kt
new file mode 100644
index 0000000..98e2a92
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerNotificationBuilder.kt
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.annotation.TargetApi
+import android.app.AlarmManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.os.SystemClock
+import android.text.TextUtils
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.SECOND_IN_MILLIS
+import android.widget.RemoteViews
+import androidx.annotation.DrawableRes
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.Action
+import androidx.core.app.NotificationCompat.Builder
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.AlarmUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.events.Events
+import com.android.deskclock.timer.ExpiredTimersActivity
+import com.android.deskclock.timer.TimerService
+
+/**
+ * Builds notifications to reflect the latest state of the timers.
+ */
+internal class TimerNotificationBuilder {
+
+ fun buildChannel(context: Context, notificationManager: NotificationManagerCompat) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ TIMER_MODEL_NOTIFICATION_CHANNEL_ID,
+ context.getString(R.string.default_label),
+ NotificationManagerCompat.IMPORTANCE_DEFAULT)
+ notificationManager.createNotificationChannel(channel)
+ }
+ }
+
+ fun build(context: Context, nm: NotificationModel, unexpired: List<Timer>): Notification {
+ val timer = unexpired[0]
+ val count = unexpired.size
+
+ // Compute some values required below.
+ val running = timer.isRunning
+ val res: Resources = context.getResources()
+
+ val base = getChronometerBase(timer)
+ val pname: String = context.getPackageName()
+
+ val actions: MutableList<Action> = ArrayList<Action>(2)
+
+ val stateText: CharSequence
+ if (count == 1) {
+ if (running) {
+ // Single timer is running.
+ stateText = if (timer.label.isNullOrEmpty()) {
+ res.getString(R.string.timer_notification_label)
+ } else {
+ timer.label
+ }
+
+ // Left button: Pause
+ val pause: Intent = Intent(context, TimerService::class.java)
+ .setAction(TimerService.ACTION_PAUSE_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_pause_24dp
+ val title1: CharSequence = res.getText(R.string.timer_pause)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, pause)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+
+ // Right Button: +1 Minute
+ val addMinute: Intent = Intent(context, TimerService::class.java)
+ .setAction(TimerService.ACTION_ADD_MINUTE_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+ @DrawableRes val icon2: Int = R.drawable.ic_add_24dp
+ val title2: CharSequence = res.getText(R.string.timer_plus_1_min)
+ val intent2: PendingIntent = Utils.pendingServiceIntent(context, addMinute)
+ actions.add(Action.Builder(icon2, title2, intent2).build())
+ } else {
+ // Single timer is paused.
+ stateText = res.getString(R.string.timer_paused)
+
+ // Left button: Start
+ val start: Intent = Intent(context, TimerService::class.java)
+ .setAction(TimerService.ACTION_START_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_start_24dp
+ val title1: CharSequence = res.getText(R.string.sw_resume_button)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, start)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+
+ // Right Button: Reset
+ val reset: Intent = Intent(context, TimerService::class.java)
+ .setAction(TimerService.ACTION_RESET_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+ @DrawableRes val icon2: Int = R.drawable.ic_reset_24dp
+ val title2: CharSequence = res.getText(R.string.sw_reset_button)
+ val intent2: PendingIntent = Utils.pendingServiceIntent(context, reset)
+ actions.add(Action.Builder(icon2, title2, intent2).build())
+ }
+ } else {
+ stateText = if (running) {
+ // At least one timer is running.
+ res.getString(R.string.timers_in_use, count)
+ } else {
+ // All timers are paused.
+ res.getString(R.string.timers_stopped, count)
+ }
+
+ val reset: Intent = TimerService.createResetUnexpiredTimersIntent(context)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp
+ val title1: CharSequence = res.getText(R.string.timer_reset_all)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+ }
+
+ // Intent to load the app and show the timer when the notification is tapped.
+ val showApp: Intent = Intent(context, TimerService::class.java)
+ .setAction(TimerService.ACTION_SHOW_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification)
+
+ val pendingShowApp: PendingIntent =
+ PendingIntent.getService(context, REQUEST_CODE_UPCOMING, showApp,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val notification: Builder = Builder(
+ context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
+ .setOngoing(true)
+ .setLocalOnly(true)
+ .setShowWhen(false)
+ .setAutoCancel(false)
+ .setContentIntent(pendingShowApp)
+ .setPriority(NotificationManager.IMPORTANCE_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setSmallIcon(R.drawable.stat_notify_timer)
+ .setSortKey(nm.timerNotificationSortKey)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+ for (action in actions) {
+ notification.addAction(action)
+ }
+
+ if (Utils.isNOrLater) {
+ notification.setCustomContentView(buildChronometer(pname, base, running, stateText))
+ .setGroup(nm.timerNotificationGroupKey)
+ } else {
+ val contentTextPreN: CharSequence?
+ contentTextPreN = when {
+ count == 1 -> {
+ TimerStringFormatter.formatTimeRemaining(context, timer.remainingTime, false)
+ }
+ running -> {
+ val timeRemaining = TimerStringFormatter.formatTimeRemaining(context,
+ timer.remainingTime, false)
+ context.getString(R.string.next_timer_notif, timeRemaining)
+ }
+ else -> context.getString(R.string.all_timers_stopped_notif)
+ }
+
+ notification.setContentTitle(stateText).setContentText(contentTextPreN)
+
+ val am: AlarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val updateNotification: Intent = TimerService.createUpdateNotificationIntent(context)
+ val remainingTime = timer.remainingTime
+ if (timer.isRunning && remainingTime > MINUTE_IN_MILLIS) {
+ // Schedule a callback to update the time-sensitive information of the running timer
+ val pi: PendingIntent =
+ PendingIntent.getService(context, REQUEST_CODE_UPCOMING, updateNotification,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val nextMinuteChange: Long = remainingTime % MINUTE_IN_MILLIS
+ val triggerTime: Long = SystemClock.elapsedRealtime() + nextMinuteChange
+ TimerModel.schedulePendingIntent(am, triggerTime, pi)
+ } else {
+ // Cancel the update notification callback.
+ val pi: PendingIntent? = PendingIntent.getService(context, 0, updateNotification,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE)
+ if (pi != null) {
+ am.cancel(pi)
+ pi.cancel()
+ }
+ }
+ }
+ return notification.build()
+ }
+
+ fun buildHeadsUp(context: Context, expired: List<Timer>): Notification {
+ val timer = expired[0]
+
+ // First action intent is to reset all timers.
+ @DrawableRes val icon1: Int = R.drawable.ic_stop_24dp
+ val reset: Intent = TimerService.createResetExpiredTimersIntent(context)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+
+ // Generate some descriptive text, a title, and an action name based on the timer count.
+ val stateText: CharSequence
+ val count = expired.size
+ val actions: MutableList<Action> = ArrayList<Action>(2)
+ if (count == 1) {
+ val label = timer.label
+ stateText = if (label.isNullOrEmpty()) {
+ context.getString(R.string.timer_times_up)
+ } else {
+ label
+ }
+
+ // Left button: Reset single timer
+ val title1: CharSequence = context.getString(R.string.timer_stop)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+
+ // Right button: Add minute
+ val addTime: Intent = TimerService.createAddMinuteTimerIntent(context, timer.id)
+ val intent2: PendingIntent = Utils.pendingServiceIntent(context, addTime)
+ @DrawableRes val icon2: Int = R.drawable.ic_add_24dp
+ val title2: CharSequence = context.getString(R.string.timer_plus_1_min)
+ actions.add(Action.Builder(icon2, title2, intent2).build())
+ } else {
+ stateText = context.getString(R.string.timer_multi_times_up, count)
+
+ // Left button: Reset all timers
+ val title1: CharSequence = context.getString(R.string.timer_stop_all)
+ actions.add(Action.Builder(icon1, title1, intent1).build())
+ }
+
+ val base = getChronometerBase(timer)
+
+ val pname: String = context.getPackageName()
+
+ // Content intent shows the timer full screen when clicked.
+ val content = Intent(context, ExpiredTimersActivity::class.java)
+ val contentIntent: PendingIntent = Utils.pendingActivityIntent(context, content)
+
+ // Full screen intent has flags so it is different than the content intent.
+ val fullScreen: Intent = Intent(context, ExpiredTimersActivity::class.java)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
+ val pendingFullScreen: PendingIntent = Utils.pendingActivityIntent(context, fullScreen)
+
+ val notification: Builder = Builder(
+ context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
+ .setOngoing(true)
+ .setLocalOnly(true)
+ .setShowWhen(false)
+ .setAutoCancel(false)
+ .setContentIntent(contentIntent)
+ .setPriority(NotificationManager.IMPORTANCE_HIGH)
+ .setDefaults(Notification.DEFAULT_LIGHTS)
+ .setSmallIcon(R.drawable.stat_notify_timer)
+ .setFullScreenIntent(pendingFullScreen, true)
+ .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+ for (action in actions) {
+ notification.addAction(action)
+ }
+
+ if (Utils.isNOrLater) {
+ notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
+ } else {
+ val contentTextPreN: CharSequence = if (count == 1) {
+ context.getString(R.string.timer_times_up)
+ } else {
+ context.getString(R.string.timer_multi_times_up, count)
+ }
+ notification.setContentTitle(stateText).setContentText(contentTextPreN)
+ }
+
+ return notification.build()
+ }
+
+ fun buildMissed(
+ context: Context,
+ nm: NotificationModel,
+ missedTimers: List<Timer>
+ ): Notification {
+ val timer = missedTimers[0]
+ val count = missedTimers.size
+
+ // Compute some values required below.
+ val base = getChronometerBase(timer)
+ val pname: String = context.getPackageName()
+ val res: Resources = context.getResources()
+
+ val action: Action
+
+ val stateText: CharSequence
+ if (count == 1) {
+ // Single timer is missed.
+ stateText = if (TextUtils.isEmpty(timer.label)) {
+ res.getString(R.string.missed_timer_notification_label)
+ } else {
+ res.getString(R.string.missed_named_timer_notification_label,
+ timer.label)
+ }
+
+ // Reset button
+ val reset: Intent = Intent(context, TimerService::class.java)
+ .setAction(TimerService.ACTION_RESET_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp
+ val title1: CharSequence = res.getText(R.string.timer_reset)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+ action = Action.Builder(icon1, title1, intent1).build()
+ } else {
+ // Multiple missed timers.
+ stateText = res.getString(R.string.timer_multi_missed, count)
+
+ val reset: Intent = TimerService.createResetMissedTimersIntent(context)
+
+ @DrawableRes val icon1: Int = R.drawable.ic_reset_24dp
+ val title1: CharSequence = res.getText(R.string.timer_reset_all)
+ val intent1: PendingIntent = Utils.pendingServiceIntent(context, reset)
+ action = Action.Builder(icon1, title1, intent1).build()
+ }
+
+ // Intent to load the app and show the timer when the notification is tapped.
+ val showApp: Intent = Intent(context, TimerService::class.java)
+ .setAction(TimerService.ACTION_SHOW_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, timer.id)
+ .putExtra(Events.EXTRA_EVENT_LABEL, R.string.label_notification)
+
+ val pendingShowApp: PendingIntent =
+ PendingIntent.getService(context, REQUEST_CODE_MISSING, showApp,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
+
+ val notification: Builder = Builder(
+ context, TIMER_MODEL_NOTIFICATION_CHANNEL_ID)
+ .setLocalOnly(true)
+ .setShowWhen(false)
+ .setAutoCancel(false)
+ .setContentIntent(pendingShowApp)
+ .setPriority(NotificationManager.IMPORTANCE_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_ALARM)
+ .setSmallIcon(R.drawable.stat_notify_timer)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setSortKey(nm.timerNotificationMissedSortKey)
+ .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+ .addAction(action)
+ .setColor(ContextCompat.getColor(context, R.color.default_background))
+
+ if (Utils.isNOrLater) {
+ notification.setCustomContentView(buildChronometer(pname, base, true, stateText))
+ .setGroup(nm.timerNotificationGroupKey)
+ } else {
+ val contentText: CharSequence = AlarmUtils.getFormattedTime(context,
+ timer.wallClockExpirationTime)
+ notification.setContentText(contentText).setContentTitle(stateText)
+ }
+
+ return notification.build()
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private fun buildChronometer(
+ pname: String,
+ base: Long,
+ running: Boolean,
+ stateText: CharSequence
+ ): RemoteViews {
+ val content = RemoteViews(pname, R.layout.chronometer_notif_content)
+ content.setChronometerCountDown(R.id.chronometer, true)
+ content.setChronometer(R.id.chronometer, base, null, running)
+ content.setTextViewText(R.id.state, stateText)
+ return content
+ }
+
+ companion object {
+ /**
+ * Notification channel containing all TimerModel notifications.
+ */
+ private const val TIMER_MODEL_NOTIFICATION_CHANNEL_ID = "TimerModelNotification"
+
+ private const val REQUEST_CODE_UPCOMING = 0
+ private const val REQUEST_CODE_MISSING = 1
+
+ /**
+ * @param timer the timer on which to base the chronometer display
+ * @return the time at which the chronometer will/did reach 0:00 in realtime
+ */
+ private fun getChronometerBase(timer: Timer): Long {
+ // The in-app timer display rounds *up* to the next second for positive timer values.
+ // Mirror that behavior in the notification's Chronometer by padding in an extra second
+ // as needed.
+ val remaining = timer.remainingTime
+ val adjustedRemaining = if (remaining < 0) remaining else remaining + SECOND_IN_MILLIS
+
+ // Chronometer will/did reach 0:00 adjustedRemaining milliseconds from now.
+ return SystemClock.elapsedRealtime() + adjustedRemaining
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/TimerStringFormatter.java b/src/com/android/deskclock/data/TimerStringFormatter.java
deleted file mode 100644
index 6ce6881..0000000
--- a/src/com/android/deskclock/data/TimerStringFormatter.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.Context;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static android.text.format.DateUtils.SECOND_IN_MILLIS;
-
-public class TimerStringFormatter {
-
- /**
- * Format "7 hours 52 minutes 14 seconds remaining"
- */
- public static String formatTimeRemaining(Context context, long remainingTime,
- boolean shouldShowSeconds) {
- int roundedHours = (int) (remainingTime / HOUR_IN_MILLIS);
- int roundedMinutes = (int) (remainingTime / MINUTE_IN_MILLIS % 60);
- int roundedSeconds = (int) (remainingTime / SECOND_IN_MILLIS % 60);
-
- final int seconds;
- final int minutes;
- final int hours;
- if ((remainingTime % SECOND_IN_MILLIS != 0) && shouldShowSeconds) {
- // Add 1 because there's a partial second.
- roundedSeconds += 1;
- if (roundedSeconds == 60) {
- // Wind back and fix the hours and minutes as needed.
- seconds = 0;
- roundedMinutes += 1;
- if (roundedMinutes == 60) {
- minutes = 0;
- roundedHours += 1;
- hours = roundedHours;
- } else {
- minutes = roundedMinutes;
- hours = roundedHours;
- }
- } else {
- seconds = roundedSeconds;
- minutes = roundedMinutes;
- hours = roundedHours;
- }
- } else {
- // Already perfect precision, or we don't want to consider seconds at all.
- seconds = roundedSeconds;
- minutes = roundedMinutes;
- hours = roundedHours;
- }
-
- final String minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes,
- minutes);
- final String hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours,
- hours);
- final String secSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.seconds,
- seconds);
-
- // The verb "remaining" may have to change tense for singular subjects in some languages.
- final String remainingSuffix = context.getString((minutes > 1 || hours > 1 || seconds > 1)
- ? R.string.timer_remaining_multiple
- : R.string.timer_remaining_single);
-
- final boolean showHours = hours > 0;
- final boolean showMinutes = minutes > 0;
- final boolean showSeconds = (seconds > 0) && shouldShowSeconds;
-
- int formatStringId = -1;
- if (showHours) {
- if (showMinutes) {
- if (showSeconds) {
- formatStringId = R.string.timer_notifications_hours_minutes_seconds;
- } else {
- formatStringId = R.string.timer_notifications_hours_minutes;
- }
- } else if (showSeconds) {
- formatStringId = R.string.timer_notifications_hours_seconds;
- } else {
- formatStringId = R.string.timer_notifications_hours;
- }
- } else if (showMinutes) {
- if (showSeconds) {
- formatStringId = R.string.timer_notifications_minutes_seconds;
- } else {
- formatStringId = R.string.timer_notifications_minutes;
- }
- } else if (showSeconds) {
- formatStringId = R.string.timer_notifications_seconds;
- } else if (!shouldShowSeconds) {
- formatStringId = R.string.timer_notifications_less_min;
- }
-
- if (formatStringId == -1) {
- return null;
- }
- return String.format(context.getString(formatStringId), hourSeq, minSeq, remainingSuffix,
- secSeq);
- }
-
- public static String formatString(Context context, @StringRes int stringResId, long currentTime,
- boolean shouldShowSeconds) {
- return String.format(context.getString(stringResId),
- formatTimeRemaining(context, currentTime, shouldShowSeconds));
- }
-}
diff --git a/src/com/android/deskclock/data/TimerStringFormatter.kt b/src/com/android/deskclock/data/TimerStringFormatter.kt
new file mode 100644
index 0000000..be5a983
--- /dev/null
+++ b/src/com/android/deskclock/data/TimerStringFormatter.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import android.text.format.DateUtils.HOUR_IN_MILLIS
+import android.text.format.DateUtils.MINUTE_IN_MILLIS
+import android.text.format.DateUtils.SECOND_IN_MILLIS
+import androidx.annotation.StringRes
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+object TimerStringFormatter {
+ /**
+ * Format "7 hours 52 minutes 14 seconds remaining"
+ */
+ @JvmStatic
+ fun formatTimeRemaining(
+ context: Context,
+ remainingTime: Long,
+ shouldShowSeconds: Boolean
+ ): String? {
+ var roundedHours = (remainingTime / HOUR_IN_MILLIS).toInt()
+ var roundedMinutes = (remainingTime / MINUTE_IN_MILLIS % 60).toInt()
+ var roundedSeconds = (remainingTime / SECOND_IN_MILLIS % 60).toInt()
+
+ val seconds: Int
+ val minutes: Int
+ val hours: Int
+ if (remainingTime % SECOND_IN_MILLIS != 0L && shouldShowSeconds) {
+ // Add 1 because there's a partial second.
+ roundedSeconds += 1
+ if (roundedSeconds == 60) {
+ // Wind back and fix the hours and minutes as needed.
+ seconds = 0
+ roundedMinutes += 1
+ if (roundedMinutes == 60) {
+ minutes = 0
+ roundedHours += 1
+ hours = roundedHours
+ } else {
+ minutes = roundedMinutes
+ hours = roundedHours
+ }
+ } else {
+ seconds = roundedSeconds
+ minutes = roundedMinutes
+ hours = roundedHours
+ }
+ } else {
+ // Already perfect precision, or we don't want to consider seconds at all.
+ seconds = roundedSeconds
+ minutes = roundedMinutes
+ hours = roundedHours
+ }
+
+ val minSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.minutes, minutes)
+ val hourSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.hours, hours)
+ val secSeq = Utils.getNumberFormattedQuantityString(context, R.plurals.seconds, seconds)
+
+ // The verb "remaining" may have to change tense for singular subjects in some languages.
+ val remainingSuffix: String =
+ context.getString(if (minutes > 1 || hours > 1 || seconds > 1) {
+ R.string.timer_remaining_multiple
+ } else {
+ R.string.timer_remaining_single
+ })
+
+ val showHours = hours > 0
+ val showMinutes = minutes > 0
+ val showSeconds = seconds > 0 && shouldShowSeconds
+
+ var formatStringId = -1
+ if (showHours) {
+ formatStringId = if (showMinutes) {
+ if (showSeconds) {
+ R.string.timer_notifications_hours_minutes_seconds
+ } else {
+ R.string.timer_notifications_hours_minutes
+ }
+ } else if (showSeconds) {
+ R.string.timer_notifications_hours_seconds
+ } else {
+ R.string.timer_notifications_hours
+ }
+ } else if (showMinutes) {
+ formatStringId = if (showSeconds) {
+ R.string.timer_notifications_minutes_seconds
+ } else {
+ R.string.timer_notifications_minutes
+ }
+ } else if (showSeconds) {
+ formatStringId = R.string.timer_notifications_seconds
+ } else if (!shouldShowSeconds) {
+ formatStringId = R.string.timer_notifications_less_min
+ }
+
+ return if (formatStringId == -1) {
+ null
+ } else {
+ String.format(context.getString(formatStringId), hourSeq, minSeq,
+ remainingSuffix, secSeq)
+ }
+ }
+
+ @JvmStatic
+ fun formatString(
+ context: Context,
+ @StringRes stringResId: Int,
+ currentTime: Long,
+ shouldShowSeconds: Boolean
+ ): String {
+ return String.format(context.getString(stringResId),
+ formatTimeRemaining(context, currentTime, shouldShowSeconds))
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Weekdays.java b/src/com/android/deskclock/data/Weekdays.java
deleted file mode 100644
index b5ba73b..0000000
--- a/src/com/android/deskclock/data/Weekdays.java
+++ /dev/null
@@ -1,336 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.Context;
-import androidx.annotation.VisibleForTesting;
-import android.util.ArrayMap;
-
-import com.android.deskclock.R;
-
-import java.text.DateFormatSymbols;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import static java.util.Calendar.DAY_OF_WEEK;
-import static java.util.Calendar.FRIDAY;
-import static java.util.Calendar.MONDAY;
-import static java.util.Calendar.SATURDAY;
-import static java.util.Calendar.SUNDAY;
-import static java.util.Calendar.THURSDAY;
-import static java.util.Calendar.TUESDAY;
-import static java.util.Calendar.WEDNESDAY;
-
-/**
- * This class is responsible for encoding a weekly repeat cycle in a {@link #getBits bitset}. It
- * also converts between those bits and the {@link Calendar#DAY_OF_WEEK} values for easier mutation
- * and querying.
- */
-public final class Weekdays {
-
- /**
- * The preferred starting day of the week can differ by locale. This enumerated value is used to
- * describe the preferred ordering.
- */
- public enum Order {
- SAT_TO_FRI(SATURDAY, SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY),
- SUN_TO_SAT(SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY),
- MON_TO_SUN(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY);
-
- private final List<Integer> mCalendarDays;
-
- Order(Integer... calendarDays) {
- mCalendarDays = Arrays.asList(calendarDays);
- }
-
- public List<Integer> getCalendarDays() {
- return mCalendarDays;
- }
- }
-
- /** All valid bits set. */
- private static final int ALL_DAYS = 0x7F;
-
- /** An instance with all weekdays in the weekly repeat cycle. */
- public static final Weekdays ALL = Weekdays.fromBits(ALL_DAYS);
-
- /** An instance with no weekdays in the weekly repeat cycle. */
- public static final Weekdays NONE = Weekdays.fromBits(0);
-
- /** Maps calendar weekdays to the bit masks that represent them in this class. */
- private static final Map<Integer, Integer> sCalendarDayToBit;
- static {
- final Map<Integer, Integer> map = new ArrayMap<>(7);
- map.put(MONDAY, 0x01);
- map.put(TUESDAY, 0x02);
- map.put(WEDNESDAY, 0x04);
- map.put(THURSDAY, 0x08);
- map.put(FRIDAY, 0x10);
- map.put(SATURDAY, 0x20);
- map.put(SUNDAY, 0x40);
- sCalendarDayToBit = Collections.unmodifiableMap(map);
- }
-
- /** An encoded form of a weekly repeat schedule. */
- private final int mBits;
-
- private Weekdays(int bits) {
- // Mask off the unused bits.
- mBits = ALL_DAYS & bits;
- }
-
- /**
- * @param bits {@link #getBits bits} representing the encoded weekly repeat schedule
- * @return a Weekdays instance representing the same repeat schedule as the {@code bits}
- */
- public static Weekdays fromBits(int bits) {
- return new Weekdays(bits);
- }
-
- /**
- * @param calendarDays an array containing any or all of the following values
- * <ul>
- * <li>{@link Calendar#SUNDAY}</li>
- * <li>{@link Calendar#MONDAY}</li>
- * <li>{@link Calendar#TUESDAY}</li>
- * <li>{@link Calendar#WEDNESDAY}</li>
- * <li>{@link Calendar#THURSDAY}</li>
- * <li>{@link Calendar#FRIDAY}</li>
- * <li>{@link Calendar#SATURDAY}</li>
- * </ul>
- * @return a Weekdays instance representing the given {@code calendarDays}
- */
- public static Weekdays fromCalendarDays(int... calendarDays) {
- int bits = 0;
- for (int calendarDay : calendarDays) {
- final Integer bit = sCalendarDayToBit.get(calendarDay);
- if (bit != null) {
- bits = bits | bit;
- }
- }
- return new Weekdays(bits);
- }
-
- /**
- * @param calendarDay any of the following values
- * <ul>
- * <li>{@link Calendar#SUNDAY}</li>
- * <li>{@link Calendar#MONDAY}</li>
- * <li>{@link Calendar#TUESDAY}</li>
- * <li>{@link Calendar#WEDNESDAY}</li>
- * <li>{@link Calendar#THURSDAY}</li>
- * <li>{@link Calendar#FRIDAY}</li>
- * <li>{@link Calendar#SATURDAY}</li>
- * </ul>
- * @param on {@code true} if the {@code calendarDay} is on; {@code false} otherwise
- * @return a WeekDays instance with the {@code calendarDay} mutated
- */
- public Weekdays setBit(int calendarDay, boolean on) {
- final Integer bit = sCalendarDayToBit.get(calendarDay);
- if (bit == null) {
- return this;
- }
- return new Weekdays(on ? (mBits | bit) : (mBits & ~bit));
- }
-
- /**
- * @param calendarDay any of the following values
- * <ul>
- * <li>{@link Calendar#SUNDAY}</li>
- * <li>{@link Calendar#MONDAY}</li>
- * <li>{@link Calendar#TUESDAY}</li>
- * <li>{@link Calendar#WEDNESDAY}</li>
- * <li>{@link Calendar#THURSDAY}</li>
- * <li>{@link Calendar#FRIDAY}</li>
- * <li>{@link Calendar#SATURDAY}</li>
- * </ul>
- * @return {@code true} if the given {@code calendarDay}
- */
- public boolean isBitOn(int calendarDay) {
- final Integer bit = sCalendarDayToBit.get(calendarDay);
- if (bit == null) {
- throw new IllegalArgumentException(calendarDay + " is not a valid weekday");
- }
- return (mBits & bit) > 0;
- }
-
- /**
- * @return the weekly repeat schedule encoded as an integer
- */
- public int getBits() { return mBits; }
-
- /**
- * @return {@code true} iff at least one weekday is enabled in the repeat schedule
- */
- public boolean isRepeating() { return mBits != 0; }
-
- /**
- * Note: only the day-of-week is read from the {@code time}. The time fields
- * are not considered in this computation.
- *
- * @param time a timestamp relative to which the answer is given
- * @return the number of days between the given {@code time} and the previous enabled weekday
- * which is always between 1 and 7 inclusive; {@code -1} if no weekdays are enabled
- */
- public int getDistanceToPreviousDay(Calendar time) {
- int calendarDay = time.get(DAY_OF_WEEK);
- for (int count = 1; count <= 7; count++) {
- calendarDay--;
- if (calendarDay < Calendar.SUNDAY) {
- calendarDay = Calendar.SATURDAY;
- }
- if (isBitOn(calendarDay)) {
- return count;
- }
- }
-
- return -1;
- }
-
- /**
- * Note: only the day-of-week is read from the {@code time}. The time fields
- * are not considered in this computation.
- *
- * @param time a timestamp relative to which the answer is given
- * @return the number of days between the given {@code time} and the next enabled weekday which
- * is always between 0 and 6 inclusive; {@code -1} if no weekdays are enabled
- */
- public int getDistanceToNextDay(Calendar time) {
- int calendarDay = time.get(DAY_OF_WEEK);
- for (int count = 0; count < 7; count++) {
- if (isBitOn(calendarDay)) {
- return count;
- }
-
- calendarDay++;
- if (calendarDay > Calendar.SATURDAY) {
- calendarDay = Calendar.SUNDAY;
- }
- }
-
- return -1;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- final Weekdays weekdays = (Weekdays) o;
- return mBits == weekdays.mBits;
- }
-
- @Override
- public int hashCode() {
- return mBits;
- }
-
- @Override
- public String toString() {
- final StringBuilder builder = new StringBuilder(19);
- builder.append("[");
- if (isBitOn(MONDAY)) {
- builder.append(builder.length() > 1 ? " M" : "M");
- }
- if (isBitOn(TUESDAY)) {
- builder.append(builder.length() > 1 ? " T" : "T");
- }
- if (isBitOn(WEDNESDAY)) {
- builder.append(builder.length() > 1 ? " W" : "W");
- }
- if (isBitOn(THURSDAY)) {
- builder.append(builder.length() > 1 ? " Th" : "Th");
- }
- if (isBitOn(FRIDAY)) {
- builder.append(builder.length() > 1 ? " F" : "F");
- }
- if (isBitOn(SATURDAY)) {
- builder.append(builder.length() > 1 ? " Sa" : "Sa");
- }
- if (isBitOn(SUNDAY)) {
- builder.append(builder.length() > 1 ? " Su" : "Su");
- }
- builder.append("]");
- return builder.toString();
- }
-
- /**
- * @param context for accessing resources
- * @param order the order in which to present the weekdays
- * @return the enabled weekdays in the given {@code order}
- */
- public String toString(Context context, Order order) {
- return toString(context, order, false /* forceLongNames */);
- }
-
- /**
- * @param context for accessing resources
- * @param order the order in which to present the weekdays
- * @return the enabled weekdays in the given {@code order} in a manner that
- * is most appropriate for talk-back
- */
- public String toAccessibilityString(Context context, Order order) {
- return toString(context, order, true /* forceLongNames */);
- }
-
- @VisibleForTesting
- int getCount() {
- int count = 0;
- for (int calendarDay = SUNDAY; calendarDay <= SATURDAY; calendarDay++) {
- if (isBitOn(calendarDay)) {
- count++;
- }
- }
- return count;
- }
-
- /**
- * @param context for accessing resources
- * @param order the order in which to present the weekdays
- * @param forceLongNames if {@code true} the un-abbreviated weekdays are used
- * @return the enabled weekdays in the given {@code order}
- */
- private String toString(Context context, Order order, boolean forceLongNames) {
- if (!isRepeating()) {
- return "";
- }
-
- if (mBits == ALL_DAYS) {
- return context.getString(R.string.every_day);
- }
-
- final boolean longNames = forceLongNames || getCount() <= 1;
- final DateFormatSymbols dfs = new DateFormatSymbols();
- final String[] weekdays = longNames ? dfs.getWeekdays() : dfs.getShortWeekdays();
-
- final String separator = context.getString(R.string.day_concat);
-
- final StringBuilder builder = new StringBuilder(40);
- for (int calendarDay : order.getCalendarDays()) {
- if (isBitOn(calendarDay)) {
- if (builder.length() > 0) {
- builder.append(separator);
- }
- builder.append(weekdays[calendarDay]);
- }
- }
- return builder.toString();
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/Weekdays.kt b/src/com/android/deskclock/data/Weekdays.kt
new file mode 100644
index 0000000..3f792fc
--- /dev/null
+++ b/src/com/android/deskclock/data/Weekdays.kt
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.R
+
+import java.text.DateFormatSymbols
+import java.util.Calendar
+
+/**
+ * This class is responsible for encoding a weekly repeat cycle in a [bitset][.getBits]. It
+ * also converts between those bits and the [Calendar.DAY_OF_WEEK] values for easier mutation
+ * and querying.
+ */
+class Weekdays private constructor(bits: Int) {
+ /**
+ * The preferred starting day of the week can differ by locale. This enumerated value is used to
+ * describe the preferred ordering.
+ */
+ enum class Order(vararg calendarDays: Int) {
+ SAT_TO_FRI(Calendar.SATURDAY, Calendar.SUNDAY, Calendar.MONDAY,
+ Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY),
+ SUN_TO_SAT(Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+ Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY),
+ MON_TO_SUN(Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY,
+ Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY);
+
+ val calendarDays: List<Int> = calendarDays.asList()
+ }
+
+ companion object {
+ /** All valid bits set. */
+ private const val ALL_DAYS = 0x7F
+
+ /** An instance with all weekdays in the weekly repeat cycle. */
+ @JvmField
+ val ALL = fromBits(ALL_DAYS)
+
+ /** An instance with no weekdays in the weekly repeat cycle. */
+ @JvmField
+ val NONE = fromBits(0)
+
+ /** Maps calendar weekdays to the bit masks that represent them in this class. */
+ private val sCalendarDayToBit: Map<Int, Int>
+
+ init {
+ val map: MutableMap<Int, Int> = mutableMapOf()
+ map[Calendar.MONDAY] = 0x01
+ map[Calendar.TUESDAY] = 0x02
+ map[Calendar.WEDNESDAY] = 0x04
+ map[Calendar.THURSDAY] = 0x08
+ map[Calendar.FRIDAY] = 0x10
+ map[Calendar.SATURDAY] = 0x20
+ map[Calendar.SUNDAY] = 0x40
+ sCalendarDayToBit = map
+ }
+
+ /**
+ * @param bits [bits][.getBits] representing the encoded weekly repeat schedule
+ * @return a Weekdays instance representing the same repeat schedule as the `bits`
+ */
+ @JvmStatic
+ fun fromBits(bits: Int): Weekdays {
+ return Weekdays(bits)
+ }
+
+ /**
+ * @param calendarDays an array containing any or all of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return a Weekdays instance representing the given `calendarDays`
+ */
+ @JvmStatic
+ fun fromCalendarDays(vararg calendarDays: Int): Weekdays {
+ var bits = 0
+ for (calendarDay in calendarDays) {
+ val bit = sCalendarDayToBit[calendarDay]
+ if (bit != null) {
+ bits = bits or bit
+ }
+ }
+ return Weekdays(bits)
+ }
+ }
+
+ /** An encoded form of a weekly repeat schedule. */
+ val bits: Int = ALL_DAYS and bits
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @param on `true` if the `calendarDay` is on; `false` otherwise
+ * @return a WeekDays instance with the `calendarDay` mutated
+ */
+ fun setBit(calendarDay: Int, on: Boolean): Weekdays {
+ val bit = sCalendarDayToBit[calendarDay] ?: return this
+ return Weekdays(if (on) bits or bit else bits and bit.inv())
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return `true` if the given `calendarDay`
+ */
+ fun isBitOn(calendarDay: Int): Boolean {
+ val bit = sCalendarDayToBit[calendarDay]
+ ?: throw IllegalArgumentException("$calendarDay is not a valid weekday")
+ return bits and bit > 0
+ }
+
+ /**
+ * @return `true` iff at least one weekday is enabled in the repeat schedule
+ */
+ val isRepeating: Boolean
+ get() = bits != 0
+
+ /**
+ * Note: only the day-of-week is read from the `time`. The time fields
+ * are not considered in this computation.
+ *
+ * @param time a timestamp relative to which the answer is given
+ * @return the number of days between the given `time` and the previous enabled weekday
+ * which is always between 1 and 7 inclusive; `-1` if no weekdays are enabled
+ */
+ fun getDistanceToPreviousDay(time: Calendar): Int {
+ var calendarDay = time[Calendar.DAY_OF_WEEK]
+ for (count in 1..7) {
+ calendarDay--
+ if (calendarDay < Calendar.SUNDAY) {
+ calendarDay = Calendar.SATURDAY
+ }
+ if (isBitOn(calendarDay)) {
+ return count
+ }
+ }
+
+ return -1
+ }
+
+ /**
+ * Note: only the day-of-week is read from the `time`. The time fields
+ * are not considered in this computation.
+ *
+ * @param time a timestamp relative to which the answer is given
+ * @return the number of days between the given `time` and the next enabled weekday which
+ * is always between 0 and 6 inclusive; `-1` if no weekdays are enabled
+ */
+ fun getDistanceToNextDay(time: Calendar): Int {
+ var calendarDay = time[Calendar.DAY_OF_WEEK]
+ for (count in 0..6) {
+ if (isBitOn(calendarDay)) {
+ return count
+ }
+
+ calendarDay++
+ if (calendarDay > Calendar.SATURDAY) {
+ calendarDay = Calendar.SUNDAY
+ }
+ }
+
+ return -1
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+
+ val weekdays = other as Weekdays
+ return bits == weekdays.bits
+ }
+
+ override fun hashCode(): Int {
+ return bits
+ }
+
+ override fun toString(): String {
+ val builder = StringBuilder(19)
+ builder.append("[")
+ if (isBitOn(Calendar.MONDAY)) {
+ builder.append(if (builder.length > 1) " M" else "M")
+ }
+ if (isBitOn(Calendar.TUESDAY)) {
+ builder.append(if (builder.length > 1) " T" else "T")
+ }
+ if (isBitOn(Calendar.WEDNESDAY)) {
+ builder.append(if (builder.length > 1) " W" else "W")
+ }
+ if (isBitOn(Calendar.THURSDAY)) {
+ builder.append(if (builder.length > 1) " Th" else "Th")
+ }
+ if (isBitOn(Calendar.FRIDAY)) {
+ builder.append(if (builder.length > 1) " F" else "F")
+ }
+ if (isBitOn(Calendar.SATURDAY)) {
+ builder.append(if (builder.length > 1) " Sa" else "Sa")
+ }
+ if (isBitOn(Calendar.SUNDAY)) {
+ builder.append(if (builder.length > 1) " Su" else "Su")
+ }
+ builder.append("]")
+ return builder.toString()
+ }
+
+ /**
+ * @param context for accessing resources
+ * @param order the order in which to present the weekdays
+ * @return the enabled weekdays in the given `order`
+ */
+ fun toString(context: Context, order: Order): String {
+ return toString(context, order, false /* forceLongNames */)
+ }
+
+ /**
+ * @param context for accessing resources
+ * @param order the order in which to present the weekdays
+ * @return the enabled weekdays in the given `order` in a manner that
+ * is most appropriate for talk-back
+ */
+ fun toAccessibilityString(context: Context, order: Order): String {
+ return toString(context, order, true /* forceLongNames */)
+ }
+
+ @get:VisibleForTesting
+ val count: Int
+ get() {
+ var count = 0
+ for (calendarDay in Calendar.SUNDAY..Calendar.SATURDAY) {
+ if (isBitOn(calendarDay)) {
+ count++
+ }
+ }
+ return count
+ }
+
+ /**
+ * @param context for accessing resources
+ * @param order the order in which to present the weekdays
+ * @param forceLongNames if `true` the un-abbreviated weekdays are used
+ * @return the enabled weekdays in the given `order`
+ */
+ private fun toString(context: Context, order: Order, forceLongNames: Boolean): String {
+ if (!isRepeating) {
+ return ""
+ }
+
+ if (bits == ALL_DAYS) {
+ return context.getString(R.string.every_day)
+ }
+
+ val longNames = forceLongNames || count <= 1
+ val dfs = DateFormatSymbols()
+ val weekdays = if (longNames) dfs.weekdays else dfs.shortWeekdays
+
+ val separator: String = context.getString(R.string.day_concat)
+
+ val builder = StringBuilder(40)
+ for (calendarDay in order.calendarDays) {
+ if (isBitOn(calendarDay)) {
+ if (builder.isNotEmpty()) {
+ builder.append(separator)
+ }
+ builder.append(weekdays[calendarDay])
+ }
+ }
+ return builder.toString()
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/WidgetDAO.java b/src/com/android/deskclock/data/WidgetDAO.java
deleted file mode 100644
index da7dc19..0000000
--- a/src/com/android/deskclock/data/WidgetDAO.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.data;
-
-import android.content.SharedPreferences;
-
-/**
- * This class encapsulates the transfer of data between widget objects and their permanent storage
- * in {@link SharedPreferences}.
- */
-final class WidgetDAO {
-
- /** Suffix for a key to a preference that stores the instance count for a given widget type. */
- private static final String WIDGET_COUNT = "_widget_count";
-
- private WidgetDAO() {}
-
- /**
- * @param widgetProviderClass indicates the type of widget being counted
- * @param count the number of widgets of the given type
- * @return the delta between the new count and the old count
- */
- static int updateWidgetCount(SharedPreferences prefs, Class widgetProviderClass, int count) {
- final String key = widgetProviderClass.getSimpleName() + WIDGET_COUNT;
- final int oldCount = prefs.getInt(key, 0);
- if (count == 0) {
- prefs.edit().remove(key).apply();
- } else {
- prefs.edit().putInt(key, count).apply();
- }
- return count - oldCount;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/WidgetDAO.kt b/src/com/android/deskclock/data/WidgetDAO.kt
new file mode 100644
index 0000000..3259971
--- /dev/null
+++ b/src/com/android/deskclock/data/WidgetDAO.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data
+
+import android.content.SharedPreferences
+
+/**
+ * This class encapsulates the transfer of data between widget objects and their permanent storage
+ * in [SharedPreferences].
+ */
+internal object WidgetDAO {
+ /** Suffix for a key to a preference that stores the instance count for a given widget type. */
+ private const val WIDGET_COUNT = "_widget_count"
+
+ /**
+ * @param widgetProviderClass indicates the type of widget being counted
+ * @param count the number of widgets of the given type
+ * @return the delta between the new count and the old count
+ */
+ fun updateWidgetCount(
+ prefs: SharedPreferences,
+ widgetProviderClass: Class<*>,
+ count: Int
+ ): Int {
+ val key = widgetProviderClass.simpleName + WIDGET_COUNT
+ val oldCount: Int = prefs.getInt(key, 0)
+ if (count == 0) {
+ prefs.edit().remove(key).apply()
+ } else {
+ prefs.edit().putInt(key, count).apply()
+ }
+ return count - oldCount
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/WidgetModel.java b/src/com/android/deskclock/data/WidgetModel.kt
similarity index 60%
rename from src/com/android/deskclock/data/WidgetModel.java
rename to src/com/android/deskclock/data/WidgetModel.kt
index e05b8ff..b63014f 100644
--- a/src/com/android/deskclock/data/WidgetModel.java
+++ b/src/com/android/deskclock/data/WidgetModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,37 +14,32 @@
* limitations under the License.
*/
-package com.android.deskclock.data;
+package com.android.deskclock.data
-import android.content.SharedPreferences;
-import androidx.annotation.StringRes;
+import android.content.SharedPreferences
+import androidx.annotation.StringRes
-import com.android.deskclock.R;
-import com.android.deskclock.events.Events;
+import com.android.deskclock.R
+import com.android.deskclock.events.Events
/**
* All widget data is accessed via this model.
*/
-final class WidgetModel {
-
- private final SharedPreferences mPrefs;
-
- WidgetModel(SharedPreferences prefs) {
- mPrefs = prefs;
- }
-
+internal class WidgetModel(private val mPrefs: SharedPreferences) {
/**
* @param widgetClass indicates the type of widget being counted
* @param count the number of widgets of the given type
* @param eventCategoryId identifies the category of event to send
*/
- void updateWidgetCount(Class widgetClass, int count, @StringRes int eventCategoryId) {
- int delta = WidgetDAO.updateWidgetCount(mPrefs, widgetClass, count);
- for (; delta > 0; delta--) {
- Events.sendEvent(eventCategoryId, R.string.action_create, 0);
+ fun updateWidgetCount(widgetClass: Class<*>, count: Int, @StringRes eventCategoryId: Int) {
+ var delta = WidgetDAO.updateWidgetCount(mPrefs, widgetClass, count)
+ while (delta > 0) {
+ Events.sendEvent(eventCategoryId, R.string.action_create, 0)
+ delta--
}
- for (; delta < 0; delta++) {
- Events.sendEvent(eventCategoryId, R.string.action_delete, 0);
+ while (delta < 0) {
+ Events.sendEvent(eventCategoryId, R.string.action_delete, 0)
+ delta++
}
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/EventTracker.java b/src/com/android/deskclock/events/EventTracker.kt
similarity index 73%
rename from src/com/android/deskclock/events/EventTracker.java
rename to src/com/android/deskclock/events/EventTracker.kt
index e92d657..39a4f11 100644
--- a/src/com/android/deskclock/events/EventTracker.java
+++ b/src/com/android/deskclock/events/EventTracker.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,18 +14,19 @@
* limitations under the License.
*/
-package com.android.deskclock.events;
+package com.android.deskclock.events
-import androidx.annotation.StringRes;
+import androidx.annotation.StringRes
-public interface EventTracker {
+interface EventTracker {
+
/**
* Record the event in some form or fashion.
*
* @param category indicates what entity raised the event: Alarm, Clock, Timer or Stopwatch
* @param action indicates how the entity was altered; e.g. create, delete, fire, etc.
* @param label indicates where the action originated; e.g. DeskClock (UI), Intent,
- * Notification, etc.; 0 indicates no label could be established
+ * Notification, etc.; 0 indicates no label could be established
*/
- void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label);
+ fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/Events.java b/src/com/android/deskclock/events/Events.java
deleted file mode 100644
index 5e5129c..0000000
--- a/src/com/android/deskclock/events/Events.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.events;
-
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.R;
-import com.android.deskclock.controller.Controller;
-
-/**
- * This thin layer over {@link Controller#sendEvent} eases the API usage.
- */
-public final class Events {
-
- /** Extra describing the entity responsible for the action being performed. */
- public static final String EXTRA_EVENT_LABEL = "com.android.deskclock.extra.EVENT_LABEL";
-
- /**
- * Tracks an alarm event.
- *
- * @param action resource id of event action
- * @param label resource id of event label
- */
- public static void sendAlarmEvent(@StringRes int action, @StringRes int label) {
- sendEvent(R.string.category_alarm, action, label);
- }
-
- /**
- * Tracks a clock event.
- *
- * @param action resource id of event action
- * @param label resource id of event label
- */
- public static void sendClockEvent(@StringRes int action, @StringRes int label) {
- sendEvent(R.string.category_clock, action, label);
- }
-
- /**
- * Tracks a timer event.
- *
- * @param action resource id of event action
- * @param label resource id of event label
- */
- public static void sendTimerEvent(@StringRes int action, @StringRes int label) {
- sendEvent(R.string.category_timer, action, label);
- }
-
- /**
- * Tracks a stopwatch event.
- *
- * @param action resource id of event action
- * @param label resource id of event label
- */
- public static void sendStopwatchEvent(@StringRes int action, @StringRes int label) {
- sendEvent(R.string.category_stopwatch, action, label);
- }
-
- /**
- * Tracks a screensaver event.
- *
- * @param action resource id of event action
- * @param label resource id of event label
- */
- public static void sendScreensaverEvent(@StringRes int action, @StringRes int label) {
- sendEvent(R.string.category_screensaver, action, label);
- }
-
- /**
- * Tracks an event. Events have a category, action, label and value. This
- * method can be used to track events such as button presses or other user
- * interactions with your application (value is not used in this app).
- *
- * @param category resource id of event category
- * @param action resource id of event action
- * @param label resource id of event label
- */
- public static void sendEvent(@StringRes int category, @StringRes int action,
- @StringRes int label) {
- Controller.getController().sendEvent(category, action, label);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/Events.kt b/src/com/android/deskclock/events/Events.kt
new file mode 100644
index 0000000..d4b116e
--- /dev/null
+++ b/src/com/android/deskclock/events/Events.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.events
+
+import androidx.annotation.StringRes
+
+import com.android.deskclock.R
+import com.android.deskclock.controller.Controller
+
+/**
+ * This thin layer over [Controller.sendEvent] eases the API usage.
+ */
+object Events {
+ /** Extra describing the entity responsible for the action being performed. */
+ const val EXTRA_EVENT_LABEL = "com.android.deskclock.extra.EVENT_LABEL"
+
+ /**
+ * Tracks an alarm event.
+ *
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ @JvmStatic
+ fun sendAlarmEvent(@StringRes action: Int, @StringRes label: Int) {
+ sendEvent(R.string.category_alarm, action, label)
+ }
+
+ /**
+ * Tracks a clock event.
+ *
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ @JvmStatic
+ fun sendClockEvent(@StringRes action: Int, @StringRes label: Int) {
+ sendEvent(R.string.category_clock, action, label)
+ }
+
+ /**
+ * Tracks a timer event.
+ *
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ @JvmStatic
+ fun sendTimerEvent(@StringRes action: Int, @StringRes label: Int) {
+ sendEvent(R.string.category_timer, action, label)
+ }
+
+ /**
+ * Tracks a stopwatch event.
+ *
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ @JvmStatic
+ fun sendStopwatchEvent(@StringRes action: Int, @StringRes label: Int) {
+ sendEvent(R.string.category_stopwatch, action, label)
+ }
+
+ /**
+ * Tracks a screensaver event.
+ *
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ @JvmStatic
+ fun sendScreensaverEvent(@StringRes action: Int, @StringRes label: Int) {
+ sendEvent(R.string.category_screensaver, action, label)
+ }
+
+ /**
+ * Tracks an event. Events have a category, action, label and value. This
+ * method can be used to track events such as button presses or other user
+ * interactions with your application (value is not used in this app).
+ *
+ * @param category resource id of event category
+ * @param action resource id of event action
+ * @param label resource id of event label
+ */
+ @JvmStatic
+ fun sendEvent(@StringRes category: Int, @StringRes action: Int, @StringRes label: Int) {
+ Controller.getController().sendEvent(category, action, label)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/LogEventTracker.java b/src/com/android/deskclock/events/LogEventTracker.java
deleted file mode 100644
index 870f8a3..0000000
--- a/src/com/android/deskclock/events/LogEventTracker.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.events;
-
-import android.content.Context;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.LogUtils;
-
-public final class LogEventTracker implements EventTracker {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Events");
-
- private final Context mContext;
-
- public LogEventTracker(Context context) {
- mContext = context;
- }
-
- @Override
- public void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
- if (label == 0) {
- LOGGER.d("[%s] [%s]", safeGetString(category), safeGetString(action));
- } else {
- LOGGER.d("[%s] [%s] [%s]", safeGetString(category), safeGetString(action),
- safeGetString(label));
- }
- }
-
- /**
- * @return Resource string represented by a given resource id, null if resId is invalid (0).
- */
- private String safeGetString(@StringRes int resId) {
- return resId == 0 ? null : mContext.getString(resId);
- }
-}
diff --git a/src/com/android/deskclock/events/LogEventTracker.kt b/src/com/android/deskclock/events/LogEventTracker.kt
new file mode 100644
index 0000000..c7cdedc
--- /dev/null
+++ b/src/com/android/deskclock/events/LogEventTracker.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.events
+
+import android.content.Context
+import androidx.annotation.StringRes
+
+import com.android.deskclock.LogUtils
+
+class LogEventTracker(val context: Context) : EventTracker {
+
+ override fun sendEvent(
+ @StringRes category: Int,
+ @StringRes action: Int,
+ @StringRes label: Int
+ ) {
+ if (label == 0) {
+ LOGGER.d("[%s] [%s]", safeGetString(category), safeGetString(action))
+ } else {
+ LOGGER.d("[%s] [%s] [%s]", safeGetString(category), safeGetString(action),
+ safeGetString(label))
+ }
+ }
+
+ /**
+ * @return Resource string represented by a given resource id, null if resId is invalid (0).
+ */
+ private fun safeGetString(@StringRes resId: Int): String? {
+ return if (resId == 0) null else context.getString(resId)
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("Events")
+ }
+}
diff --git a/src/com/android/deskclock/events/ShortcutEventTracker.java b/src/com/android/deskclock/events/ShortcutEventTracker.java
deleted file mode 100644
index 4b956ce..0000000
--- a/src/com/android/deskclock/events/ShortcutEventTracker.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.events;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.pm.ShortcutManager;
-import android.os.Build;
-import androidx.annotation.StringRes;
-import android.util.ArraySet;
-
-import com.android.deskclock.R;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.util.Set;
-
-@TargetApi(Build.VERSION_CODES.N_MR1)
-public final class ShortcutEventTracker implements EventTracker {
-
- private final ShortcutManager mShortcutManager;
- private final Set<String> shortcuts = new ArraySet<>(5);
-
- public ShortcutEventTracker(Context context) {
- mShortcutManager = context.getSystemService(ShortcutManager.class);
- final UiDataModel uidm = UiDataModel.getUiDataModel();
- shortcuts.add(uidm.getShortcutId(R.string.category_alarm, R.string.action_create));
- shortcuts.add(uidm.getShortcutId(R.string.category_timer, R.string.action_create));
- shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_pause));
- shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_start));
- shortcuts.add(uidm.getShortcutId(R.string.category_screensaver, R.string.action_show));
- }
-
- @Override
- public void sendEvent(@StringRes int category, @StringRes int action, @StringRes int label) {
- final String shortcutId = UiDataModel.getUiDataModel().getShortcutId(category, action);
- if (shortcuts.contains(shortcutId)) {
- mShortcutManager.reportShortcutUsed(shortcutId);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/events/ShortcutEventTracker.kt b/src/com/android/deskclock/events/ShortcutEventTracker.kt
new file mode 100644
index 0000000..2b2bdf3
--- /dev/null
+++ b/src/com/android/deskclock/events/ShortcutEventTracker.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.events
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.content.pm.ShortcutManager
+import android.os.Build
+import android.util.ArraySet
+import androidx.annotation.StringRes
+
+import com.android.deskclock.R
+import com.android.deskclock.uidata.UiDataModel
+
+@TargetApi(Build.VERSION_CODES.N_MR1)
+class ShortcutEventTracker(context: Context) : EventTracker {
+ private val mShortcutManager: ShortcutManager =
+ context.getSystemService(ShortcutManager::class.java)
+ private val shortcuts: MutableSet<String> = ArraySet(5)
+
+ init {
+ val uidm = UiDataModel.uiDataModel
+ shortcuts.add(uidm.getShortcutId(R.string.category_alarm, R.string.action_create))
+ shortcuts.add(uidm.getShortcutId(R.string.category_timer, R.string.action_create))
+ shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_pause))
+ shortcuts.add(uidm.getShortcutId(R.string.category_stopwatch, R.string.action_start))
+ shortcuts.add(uidm.getShortcutId(R.string.category_screensaver, R.string.action_show))
+ }
+
+ override fun sendEvent(
+ @StringRes category: Int,
+ @StringRes action: Int,
+ @StringRes label: Int
+ ) {
+ val shortcutId = UiDataModel.uiDataModel.getShortcutId(category, action)
+ if (shortcuts.contains(shortcutId)) {
+ mShortcutManager.reportShortcutUsed(shortcutId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/Alarm.java b/src/com/android/deskclock/provider/Alarm.java
deleted file mode 100644
index fc8aebd..0000000
--- a/src/com/android/deskclock/provider/Alarm.java
+++ /dev/null
@@ -1,468 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.provider;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.CursorLoader;
-import android.content.Intent;
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Weekdays;
-
-import java.util.Calendar;
-import java.util.LinkedList;
-import java.util.List;
-
-public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
- /**
- * Alarms start with an invalid id when it hasn't been saved to the database.
- */
- public static final long INVALID_ID = -1;
-
- /**
- * The default sort order for this table
- */
- private static final String DEFAULT_SORT_ORDER =
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + ", " +
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES + " ASC" + ", " +
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ClockContract.AlarmsColumns._ID + " DESC";
-
- private static final String[] QUERY_COLUMNS = {
- _ID,
- HOUR,
- MINUTES,
- DAYS_OF_WEEK,
- ENABLED,
- VIBRATE,
- LABEL,
- RINGTONE,
- DELETE_AFTER_USE
- };
-
- private static final String[] QUERY_ALARMS_WITH_INSTANCES_COLUMNS = {
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DAYS_OF_WEEK,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + VIBRATE,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + LABEL,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + RINGTONE,
- ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DELETE_AFTER_USE,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "."
- + ClockContract.InstancesColumns.ALARM_STATE,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns._ID,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.YEAR,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MONTH,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.DAY,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.HOUR,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MINUTES,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.LABEL,
- ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.VIBRATE
- };
-
- /**
- * These save calls to cursor.getColumnIndexOrThrow()
- * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
- */
- private static final int ID_INDEX = 0;
- private static final int HOUR_INDEX = 1;
- private static final int MINUTES_INDEX = 2;
- private static final int DAYS_OF_WEEK_INDEX = 3;
- private static final int ENABLED_INDEX = 4;
- private static final int VIBRATE_INDEX = 5;
- private static final int LABEL_INDEX = 6;
- private static final int RINGTONE_INDEX = 7;
- private static final int DELETE_AFTER_USE_INDEX = 8;
- private static final int INSTANCE_STATE_INDEX = 9;
- public static final int INSTANCE_ID_INDEX = 10;
- public static final int INSTANCE_YEAR_INDEX = 11;
- public static final int INSTANCE_MONTH_INDEX = 12;
- public static final int INSTANCE_DAY_INDEX = 13;
- public static final int INSTANCE_HOUR_INDEX = 14;
- public static final int INSTANCE_MINUTE_INDEX = 15;
- public static final int INSTANCE_LABEL_INDEX = 16;
- public static final int INSTANCE_VIBRATE_INDEX = 17;
-
- private static final int COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1;
- private static final int ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1;
-
- public static ContentValues createContentValues(Alarm alarm) {
- ContentValues values = new ContentValues(COLUMN_COUNT);
- if (alarm.id != INVALID_ID) {
- values.put(ClockContract.AlarmsColumns._ID, alarm.id);
- }
-
- values.put(ENABLED, alarm.enabled ? 1 : 0);
- values.put(HOUR, alarm.hour);
- values.put(MINUTES, alarm.minutes);
- values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBits());
- values.put(VIBRATE, alarm.vibrate ? 1 : 0);
- values.put(LABEL, alarm.label);
- values.put(DELETE_AFTER_USE, alarm.deleteAfterUse);
- if (alarm.alert == null) {
- // We want to put null, so default alarm changes
- values.putNull(RINGTONE);
- } else {
- values.put(RINGTONE, alarm.alert.toString());
- }
-
- return values;
- }
-
- public static Intent createIntent(Context context, Class<?> cls, long alarmId) {
- return new Intent(context, cls).setData(getContentUri(alarmId));
- }
-
- public static Uri getContentUri(long alarmId) {
- return ContentUris.withAppendedId(CONTENT_URI, alarmId);
- }
-
- public static long getId(Uri contentUri) {
- return ContentUris.parseId(contentUri);
- }
-
- /**
- * Get alarm cursor loader for all alarms.
- *
- * @param context to query the database.
- * @return cursor loader with all the alarms.
- */
- public static CursorLoader getAlarmsCursorLoader(Context context) {
- return new CursorLoader(context, ALARMS_WITH_INSTANCES_URI,
- QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) {
- @Override
- public void onContentChanged() {
- // There is a bug in Loader which can result in stale data if a loader is stopped
- // immediately after a call to onContentChanged. As a workaround we stop the
- // loader before delivering onContentChanged to ensure mContentChanged is set to
- // true before forceLoad is called.
- if (isStarted() && !isAbandoned()) {
- stopLoading();
- super.onContentChanged();
- startLoading();
- } else {
- super.onContentChanged();
- }
- }
-
- @Override
- public Cursor loadInBackground() {
- // Prime the ringtone title cache for later access. Most alarms will refer to
- // system ringtones.
- DataModel.getDataModel().loadRingtoneTitles();
-
- return super.loadInBackground();
- }
- };
- }
-
- /**
- * Get alarm by id.
- *
- * @param cr provides access to the content model
- * @param alarmId for the desired alarm.
- * @return alarm if found, null otherwise
- */
- public static Alarm getAlarm(ContentResolver cr, long alarmId) {
- try (Cursor cursor = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)) {
- if (cursor.moveToFirst()) {
- return new Alarm(cursor);
- }
- }
-
- return null;
- }
- /**
- * Get alarm for the {@code contentUri}.
- *
- * @param cr provides access to the content model
- * @param contentUri the {@link #getContentUri deeplink} for the desired alarm
- * @return instance if found, null otherwise
- */
- public static Alarm getAlarm(ContentResolver cr, Uri contentUri) {
- return getAlarm(cr, ContentUris.parseId(contentUri));
- }
-
- /**
- * Get all alarms given conditions.
- *
- * @param cr provides access to the content model
- * @param selection A filter declaring which rows to return, formatted as an
- * SQL WHERE clause (excluding the WHERE itself). Passing null will
- * return all rows for the given URI.
- * @param selectionArgs You may include ?s in selection, which will be
- * replaced by the values from selectionArgs, in the order that they
- * appear in the selection. The values will be bound as Strings.
- * @return list of alarms matching where clause or empty list if none found.
- */
- public static List<Alarm> getAlarms(ContentResolver cr, String selection,
- String... selectionArgs) {
- final List<Alarm> result = new LinkedList<>();
- try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- do {
- result.add(new Alarm(cursor));
- } while (cursor.moveToNext());
- }
- }
-
- return result;
- }
-
- public static boolean isTomorrow(Alarm alarm, Calendar now) {
- if (alarm.instanceState == AlarmInstance.SNOOZE_STATE) {
- return false;
- }
-
- final int totalAlarmMinutes = alarm.hour * 60 + alarm.minutes;
- final int totalNowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE);
- return totalAlarmMinutes <= totalNowMinutes;
- }
-
- public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) {
- ContentValues values = createContentValues(alarm);
- Uri uri = contentResolver.insert(CONTENT_URI, values);
- alarm.id = getId(uri);
- return alarm;
- }
-
- public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) {
- if (alarm.id == Alarm.INVALID_ID) return false;
- ContentValues values = createContentValues(alarm);
- long rowsUpdated = contentResolver.update(getContentUri(alarm.id), values, null, null);
- return rowsUpdated == 1;
- }
-
- public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) {
- if (alarmId == INVALID_ID) return false;
- int deletedRows = contentResolver.delete(getContentUri(alarmId), "", null);
- return deletedRows == 1;
- }
-
- public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
- public Alarm createFromParcel(Parcel p) {
- return new Alarm(p);
- }
-
- public Alarm[] newArray(int size) {
- return new Alarm[size];
- }
- };
-
- // Public fields
- // TODO: Refactor instance names
- public long id;
- public boolean enabled;
- public int hour;
- public int minutes;
- public Weekdays daysOfWeek;
- public boolean vibrate;
- public String label;
- public Uri alert;
- public boolean deleteAfterUse;
- public int instanceState;
- public int instanceId;
-
- // Creates a default alarm at the current time.
- public Alarm() {
- this(0, 0);
- }
-
- public Alarm(int hour, int minutes) {
- this.id = INVALID_ID;
- this.hour = hour;
- this.minutes = minutes;
- this.vibrate = true;
- this.daysOfWeek = Weekdays.NONE;
- this.label = "";
- this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
- this.deleteAfterUse = false;
- }
-
- public Alarm(Cursor c) {
- id = c.getLong(ID_INDEX);
- enabled = c.getInt(ENABLED_INDEX) == 1;
- hour = c.getInt(HOUR_INDEX);
- minutes = c.getInt(MINUTES_INDEX);
- daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX));
- vibrate = c.getInt(VIBRATE_INDEX) == 1;
- label = c.getString(LABEL_INDEX);
- deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1;
-
- if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
- instanceState = c.getInt(INSTANCE_STATE_INDEX);
- instanceId = c.getInt(INSTANCE_ID_INDEX);
- }
-
- if (c.isNull(RINGTONE_INDEX)) {
- // Should we be saving this with the current ringtone or leave it null
- // so it changes when user changes default ringtone?
- alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
- } else {
- alert = Uri.parse(c.getString(RINGTONE_INDEX));
- }
- }
-
- Alarm(Parcel p) {
- id = p.readLong();
- enabled = p.readInt() == 1;
- hour = p.readInt();
- minutes = p.readInt();
- daysOfWeek = Weekdays.fromBits(p.readInt());
- vibrate = p.readInt() == 1;
- label = p.readString();
- alert = p.readParcelable(null);
- deleteAfterUse = p.readInt() == 1;
- }
-
- /**
- * @return the deeplink that identifies this alarm
- */
- public Uri getContentUri() {
- return getContentUri(id);
- }
-
- public String getLabelOrDefault(Context context) {
- return label.isEmpty() ? context.getString(R.string.default_label) : label;
- }
-
- /**
- * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
- * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
- */
- public boolean canPreemptivelyDismiss() {
- return instanceState == AlarmInstance.SNOOZE_STATE
- || instanceState == AlarmInstance.HIGH_NOTIFICATION_STATE
- || instanceState == AlarmInstance.LOW_NOTIFICATION_STATE
- || instanceState == AlarmInstance.HIDE_NOTIFICATION_STATE;
- }
-
- public void writeToParcel(Parcel p, int flags) {
- p.writeLong(id);
- p.writeInt(enabled ? 1 : 0);
- p.writeInt(hour);
- p.writeInt(minutes);
- p.writeInt(daysOfWeek.getBits());
- p.writeInt(vibrate ? 1 : 0);
- p.writeString(label);
- p.writeParcelable(alert, flags);
- p.writeInt(deleteAfterUse ? 1 : 0);
- }
-
- public int describeContents() {
- return 0;
- }
-
- public AlarmInstance createInstanceAfter(Calendar time) {
- Calendar nextInstanceTime = getNextAlarmTime(time);
- AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
- result.mVibrate = vibrate;
- result.mLabel = label;
- result.mRingtone = alert;
- return result;
- }
-
- /**
- *
- * @param currentTime the current time
- * @return previous firing time, or null if this is a one-time alarm.
- */
- public Calendar getPreviousAlarmTime(Calendar currentTime) {
- final Calendar previousInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
- previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
- previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
- previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
- previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
- previousInstanceTime.set(Calendar.MINUTE, minutes);
- previousInstanceTime.set(Calendar.SECOND, 0);
- previousInstanceTime.set(Calendar.MILLISECOND, 0);
-
- final int subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime);
- if (subtractDays > 0) {
- previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays);
- return previousInstanceTime;
- } else {
- return null;
- }
- }
-
- public Calendar getNextAlarmTime(Calendar currentTime) {
- final Calendar nextInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
- nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
- nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
- nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
- nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
- nextInstanceTime.set(Calendar.MINUTE, minutes);
- nextInstanceTime.set(Calendar.SECOND, 0);
- nextInstanceTime.set(Calendar.MILLISECOND, 0);
-
- // If we are still behind the passed in currentTime, then add a day
- if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) {
- nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
- }
-
- // The day of the week might be invalid, so find next valid one
- final int addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime);
- if (addDays > 0) {
- nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
- }
-
- // Daylight Savings Time can alter the hours and minutes when adjusting the day above.
- // Reset the desired hour and minute now that the correct day has been chosen.
- nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
- nextInstanceTime.set(Calendar.MINUTE, minutes);
-
- return nextInstanceTime;
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof Alarm)) return false;
- final Alarm other = (Alarm) o;
- return id == other.id;
- }
-
- @Override
- public int hashCode() {
- return Long.valueOf(id).hashCode();
- }
-
- @Override
- public String toString() {
- return "Alarm{" +
- "alert=" + alert +
- ", id=" + id +
- ", enabled=" + enabled +
- ", hour=" + hour +
- ", minutes=" + minutes +
- ", daysOfWeek=" + daysOfWeek +
- ", vibrate=" + vibrate +
- ", label='" + label + '\'' +
- ", deleteAfterUse=" + deleteAfterUse +
- '}';
- }
-}
diff --git a/src/com/android/deskclock/provider/Alarm.kt b/src/com/android/deskclock/provider/Alarm.kt
new file mode 100644
index 0000000..7999b5c
--- /dev/null
+++ b/src/com/android/deskclock/provider/Alarm.kt
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.provider
+
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.database.Cursor
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+import android.provider.BaseColumns
+import androidx.loader.content.CursorLoader
+
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Weekdays
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.util.Calendar
+import java.util.LinkedList
+
+class Alarm : Parcelable, AlarmsColumns {
+ // Public fields
+ // TODO: Refactor instance names
+ @JvmField
+ var id: Long
+
+ @JvmField
+ var enabled = false
+
+ @JvmField
+ var hour: Int
+
+ @JvmField
+ var minutes: Int
+
+ @JvmField
+ var daysOfWeek: Weekdays
+
+ @JvmField
+ var vibrate: Boolean
+
+ @JvmField
+ var label: String?
+
+ @JvmField
+ var alert: Uri? = null
+
+ @JvmField
+ var deleteAfterUse: Boolean
+
+ @JvmField
+ var instanceState = 0
+
+ var instanceId = 0
+
+ // Creates a default alarm at the current time.
+ @JvmOverloads
+ constructor(hour: Int = 0, minutes: Int = 0) {
+ id = INVALID_ID
+ this.hour = hour
+ this.minutes = minutes
+ vibrate = true
+ daysOfWeek = Weekdays.NONE
+ label = ""
+ alert = DataModel.dataModel.defaultAlarmRingtoneUri
+ deleteAfterUse = false
+ }
+
+ constructor(c: Cursor) {
+ id = c.getLong(ID_INDEX)
+ enabled = c.getInt(ENABLED_INDEX) == 1
+ hour = c.getInt(HOUR_INDEX)
+ minutes = c.getInt(MINUTES_INDEX)
+ daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX))
+ vibrate = c.getInt(VIBRATE_INDEX) == 1
+ label = c.getString(LABEL_INDEX)
+ deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1
+
+ if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
+ instanceState = c.getInt(INSTANCE_STATE_INDEX)
+ instanceId = c.getInt(INSTANCE_ID_INDEX)
+ }
+
+ alert = if (c.isNull(RINGTONE_INDEX)) {
+ // Should we be saving this with the current ringtone or leave it null
+ // so it changes when user changes default ringtone?
+ RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+ } else {
+ Uri.parse(c.getString(RINGTONE_INDEX))
+ }
+ }
+
+ internal constructor(p: Parcel) {
+ id = p.readLong()
+ enabled = p.readInt() == 1
+ hour = p.readInt()
+ minutes = p.readInt()
+ daysOfWeek = Weekdays.fromBits(p.readInt())
+ vibrate = p.readInt() == 1
+ label = p.readString()
+ alert = p.readParcelable(null)
+ deleteAfterUse = p.readInt() == 1
+ }
+
+ /**
+ * @return the deeplink that identifies this alarm
+ */
+ val contentUri: Uri
+ get() = getContentUri(id)
+
+ fun getLabelOrDefault(context: Context): String {
+ return if (label.isNullOrEmpty()) context.getString(R.string.default_label) else label!!
+ }
+
+ /**
+ * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
+ * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
+ */
+ fun canPreemptivelyDismiss(): Boolean {
+ return instanceState == InstancesColumns.SNOOZE_STATE ||
+ instanceState == InstancesColumns.HIGH_NOTIFICATION_STATE ||
+ instanceState == InstancesColumns.LOW_NOTIFICATION_STATE ||
+ instanceState == InstancesColumns.HIDE_NOTIFICATION_STATE
+ }
+
+ override fun writeToParcel(p: Parcel, flags: Int) {
+ p.writeLong(id)
+ p.writeInt(if (enabled) 1 else 0)
+ p.writeInt(hour)
+ p.writeInt(minutes)
+ p.writeInt(daysOfWeek.bits)
+ p.writeInt(if (vibrate) 1 else 0)
+ p.writeString(label)
+ p.writeParcelable(alert, flags)
+ p.writeInt(if (deleteAfterUse) 1 else 0)
+ }
+
+ override fun describeContents(): Int = 0
+
+ fun createInstanceAfter(time: Calendar): AlarmInstance {
+ val nextInstanceTime = getNextAlarmTime(time)
+ val result = AlarmInstance(nextInstanceTime, id)
+ result.mVibrate = vibrate
+ result.mLabel = label
+ result.mRingtone = alert
+ return result
+ }
+
+ /**
+ *
+ * @param currentTime the current time
+ * @return previous firing time, or null if this is a one-time alarm.
+ */
+ fun getPreviousAlarmTime(currentTime: Calendar): Calendar? {
+ val previousInstanceTime = Calendar.getInstance(currentTime.timeZone)
+ previousInstanceTime[Calendar.YEAR] = currentTime[Calendar.YEAR]
+ previousInstanceTime[Calendar.MONTH] = currentTime[Calendar.MONTH]
+ previousInstanceTime[Calendar.DAY_OF_MONTH] = currentTime[Calendar.DAY_OF_MONTH]
+ previousInstanceTime[Calendar.HOUR_OF_DAY] = hour
+ previousInstanceTime[Calendar.MINUTE] = minutes
+ previousInstanceTime[Calendar.SECOND] = 0
+ previousInstanceTime[Calendar.MILLISECOND] = 0
+
+ val subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime)
+ return if (subtractDays > 0) {
+ previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays)
+ previousInstanceTime
+ } else {
+ null
+ }
+ }
+
+ fun getNextAlarmTime(currentTime: Calendar): Calendar {
+ val nextInstanceTime = Calendar.getInstance(currentTime.timeZone)
+ nextInstanceTime[Calendar.YEAR] = currentTime[Calendar.YEAR]
+ nextInstanceTime[Calendar.MONTH] = currentTime[Calendar.MONTH]
+ nextInstanceTime[Calendar.DAY_OF_MONTH] = currentTime[Calendar.DAY_OF_MONTH]
+ nextInstanceTime[Calendar.HOUR_OF_DAY] = hour
+ nextInstanceTime[Calendar.MINUTE] = minutes
+ nextInstanceTime[Calendar.SECOND] = 0
+ nextInstanceTime[Calendar.MILLISECOND] = 0
+
+ // If we are still behind the passed in currentTime, then add a day
+ if (nextInstanceTime.timeInMillis <= currentTime.timeInMillis) {
+ nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1)
+ }
+
+ // The day of the week might be invalid, so find next valid one
+ val addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime)
+ if (addDays > 0) {
+ nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays)
+ }
+
+ // Daylight Savings Time can alter the hours and minutes when adjusting the day above.
+ // Reset the desired hour and minute now that the correct day has been chosen.
+ nextInstanceTime[Calendar.HOUR_OF_DAY] = hour
+ nextInstanceTime[Calendar.MINUTE] = minutes
+
+ return nextInstanceTime
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is Alarm) return false
+ return id == other.id
+ }
+
+ override fun hashCode(): Int {
+ return java.lang.Long.valueOf(id).hashCode()
+ }
+
+ override fun toString(): String {
+ return "Alarm{" +
+ "alert=" + alert +
+ ", id=" + id +
+ ", enabled=" + enabled +
+ ", hour=" + hour +
+ ", minutes=" + minutes +
+ ", daysOfWeek=" + daysOfWeek +
+ ", vibrate=" + vibrate +
+ ", label='" + label + '\'' +
+ ", deleteAfterUse=" + deleteAfterUse +
+ '}'
+ }
+
+ companion object {
+ /**
+ * Alarms start with an invalid id when it hasn't been saved to the database.
+ */
+ const val INVALID_ID: Long = -1
+
+ /**
+ * The default sort order for this table
+ */
+ private val DEFAULT_SORT_ORDER = ClockDatabaseHelper.ALARMS_TABLE_NAME + "." +
+ AlarmsColumns.HOUR + ", " + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." +
+ AlarmsColumns.MINUTES + " ASC" + ", " +
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + BaseColumns._ID + " DESC"
+
+ private val QUERY_COLUMNS = arrayOf(
+ BaseColumns._ID,
+ AlarmsColumns.HOUR,
+ AlarmsColumns.MINUTES,
+ AlarmsColumns.DAYS_OF_WEEK,
+ AlarmsColumns.ENABLED,
+ AlarmSettingColumns.VIBRATE,
+ AlarmSettingColumns.LABEL,
+ AlarmSettingColumns.RINGTONE,
+ AlarmsColumns.DELETE_AFTER_USE
+ )
+
+ private val QUERY_ALARMS_WITH_INSTANCES_COLUMNS = arrayOf(
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + BaseColumns._ID,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE,
+ ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + BaseColumns._ID,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL,
+ ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
+ )
+
+ /**
+ * These save calls to cursor.getColumnIndexOrThrow()
+ * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
+ */
+ private const val ID_INDEX = 0
+ private const val HOUR_INDEX = 1
+ private const val MINUTES_INDEX = 2
+ private const val DAYS_OF_WEEK_INDEX = 3
+ private const val ENABLED_INDEX = 4
+ private const val VIBRATE_INDEX = 5
+ private const val LABEL_INDEX = 6
+ private const val RINGTONE_INDEX = 7
+ private const val DELETE_AFTER_USE_INDEX = 8
+ private const val INSTANCE_STATE_INDEX = 9
+ const val INSTANCE_ID_INDEX = 10
+ const val INSTANCE_YEAR_INDEX = 11
+ const val INSTANCE_MONTH_INDEX = 12
+ const val INSTANCE_DAY_INDEX = 13
+ const val INSTANCE_HOUR_INDEX = 14
+ const val INSTANCE_MINUTE_INDEX = 15
+ const val INSTANCE_LABEL_INDEX = 16
+ const val INSTANCE_VIBRATE_INDEX = 17
+
+ private const val COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1
+ private const val ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1
+
+ @JvmStatic
+ fun createContentValues(alarm: Alarm): ContentValues {
+ val values = ContentValues(COLUMN_COUNT)
+ if (alarm.id != INVALID_ID) {
+ values.put(BaseColumns._ID, alarm.id)
+ }
+
+ values.put(AlarmsColumns.ENABLED, if (alarm.enabled) 1 else 0)
+ values.put(AlarmsColumns.HOUR, alarm.hour)
+ values.put(AlarmsColumns.MINUTES, alarm.minutes)
+ values.put(AlarmsColumns.DAYS_OF_WEEK, alarm.daysOfWeek.bits)
+ values.put(AlarmSettingColumns.VIBRATE, if (alarm.vibrate) 1 else 0)
+ values.put(AlarmSettingColumns.LABEL, alarm.label)
+ values.put(AlarmsColumns.DELETE_AFTER_USE, alarm.deleteAfterUse)
+ if (alarm.alert == null) {
+ // We want to put null, so default alarm changes
+ values.putNull(AlarmSettingColumns.RINGTONE)
+ } else {
+ values.put(AlarmSettingColumns.RINGTONE, alarm.alert.toString())
+ }
+ return values
+ }
+
+ @JvmStatic
+ fun createIntent(context: Context?, cls: Class<*>?, alarmId: Long): Intent {
+ return Intent(context, cls).setData(getContentUri(alarmId))
+ }
+
+ fun getContentUri(alarmId: Long): Uri {
+ return ContentUris.withAppendedId(AlarmsColumns.CONTENT_URI, alarmId)
+ }
+
+ fun getId(contentUri: Uri): Long {
+ return ContentUris.parseId(contentUri)
+ }
+
+ /**
+ * Get alarm cursor loader for all alarms.
+ *
+ * @param context to query the database.
+ * @return cursor loader with all the alarms.
+ */
+ @JvmStatic
+ fun getAlarmsCursorLoader(context: Context): CursorLoader {
+ return object : CursorLoader(context, AlarmsColumns.ALARMS_WITH_INSTANCES_URI,
+ QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) {
+
+ override fun onContentChanged() {
+ // There is a bug in Loader which can result in stale data if a loader is stopped
+ // immediately after a call to onContentChanged. As a workaround we stop the
+ // loader before delivering onContentChanged to ensure mContentChanged is set to
+ // true before forceLoad is called.
+ if (isStarted() && !isAbandoned()) {
+ stopLoading()
+ super.onContentChanged()
+ startLoading()
+ } else {
+ super.onContentChanged()
+ }
+ }
+
+ override fun loadInBackground(): Cursor? {
+ // Prime the ringtone title cache for later access. Most alarms will refer to
+ // system ringtones.
+ DataModel.dataModel.loadRingtoneTitles()
+ return super.loadInBackground()
+ }
+ }
+ }
+
+ /**
+ * Get alarm by id.
+ *
+ * @param cr provides access to the content model
+ * @param alarmId for the desired alarm.
+ * @return alarm if found, null otherwise
+ */
+ @JvmStatic
+ fun getAlarm(cr: ContentResolver, alarmId: Long): Alarm? {
+ val cursor: Cursor? = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)
+ cursor?.let {
+ if (cursor.moveToFirst()) {
+ return Alarm(cursor)
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Get all alarms given conditions.
+ *
+ * @param cr provides access to the content model
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null will
+ * return all rows for the given URI.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in the order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @return list of alarms matching where clause or empty list if none found.
+ */
+ @JvmStatic
+ fun getAlarms(
+ cr: ContentResolver,
+ selection: String?,
+ vararg selectionArgs: String?
+ ): List<Alarm> {
+ val result: MutableList<Alarm> = LinkedList()
+ val cursor: Cursor? =
+ cr.query(AlarmsColumns.CONTENT_URI, QUERY_COLUMNS,
+ selection, selectionArgs, null)
+ cursor?.let {
+ if (cursor.moveToFirst()) {
+ do {
+ result.add(Alarm(cursor))
+ } while (cursor.moveToNext())
+ }
+ }
+
+ return result
+ }
+
+ @JvmStatic
+ fun isTomorrow(alarm: Alarm, now: Calendar): Boolean {
+ if (alarm.instanceState == InstancesColumns.SNOOZE_STATE) {
+ return false
+ }
+
+ val totalAlarmMinutes = alarm.hour * 60 + alarm.minutes
+ val totalNowMinutes = now[Calendar.HOUR_OF_DAY] * 60 + now[Calendar.MINUTE]
+ return totalAlarmMinutes <= totalNowMinutes
+ }
+
+ @JvmStatic
+ fun addAlarm(contentResolver: ContentResolver, alarm: Alarm): Alarm {
+ val values: ContentValues = createContentValues(alarm)
+ val uri: Uri = contentResolver.insert(AlarmsColumns.CONTENT_URI, values)!!
+ alarm.id = getId(uri)
+ return alarm
+ }
+
+ @JvmStatic
+ fun updateAlarm(contentResolver: ContentResolver, alarm: Alarm): Boolean {
+ if (alarm.id == INVALID_ID) return false
+ val values: ContentValues = createContentValues(alarm)
+ val rowsUpdated: Long =
+ contentResolver.update(getContentUri(alarm.id), values, null, null).toLong()
+ return rowsUpdated == 1L
+ }
+
+ @JvmStatic
+ fun deleteAlarm(contentResolver: ContentResolver, alarmId: Long): Boolean {
+ if (alarmId == INVALID_ID) return false
+ val deletedRows: Int = contentResolver.delete(getContentUri(alarmId), "", null)
+ return deletedRows == 1
+ }
+
+ val CREATOR: Parcelable.Creator<Alarm> = object : Parcelable.Creator<Alarm> {
+ override fun createFromParcel(p: Parcel): Alarm {
+ return Alarm(p)
+ }
+
+ override fun newArray(size: Int): Array<Alarm?> {
+ return arrayOfNulls(size)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/AlarmInstance.java b/src/com/android/deskclock/provider/AlarmInstance.java
deleted file mode 100644
index 9fb7a7b..0000000
--- a/src/com/android/deskclock/provider/AlarmInstance.java
+++ /dev/null
@@ -1,476 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.provider;
-
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.alarms.AlarmStateManager;
-import com.android.deskclock.data.DataModel;
-
-import java.util.Calendar;
-import java.util.LinkedList;
-import java.util.List;
-
-public final class AlarmInstance implements ClockContract.InstancesColumns {
- /**
- * Offset from alarm time to show low priority notification
- */
- public static final int LOW_NOTIFICATION_HOUR_OFFSET = -2;
-
- /**
- * Offset from alarm time to show high priority notification
- */
- public static final int HIGH_NOTIFICATION_MINUTE_OFFSET = -30;
-
- /**
- * Offset from alarm time to stop showing missed notification.
- */
- private static final int MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12;
-
- /**
- * AlarmInstances start with an invalid id when it hasn't been saved to the database.
- */
- public static final long INVALID_ID = -1;
-
- private static final String[] QUERY_COLUMNS = {
- _ID,
- YEAR,
- MONTH,
- DAY,
- HOUR,
- MINUTES,
- LABEL,
- VIBRATE,
- RINGTONE,
- ALARM_ID,
- ALARM_STATE
- };
-
- /**
- * These save calls to cursor.getColumnIndexOrThrow()
- * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
- */
- private static final int ID_INDEX = 0;
- private static final int YEAR_INDEX = 1;
- private static final int MONTH_INDEX = 2;
- private static final int DAY_INDEX = 3;
- private static final int HOUR_INDEX = 4;
- private static final int MINUTES_INDEX = 5;
- private static final int LABEL_INDEX = 6;
- private static final int VIBRATE_INDEX = 7;
- private static final int RINGTONE_INDEX = 8;
- private static final int ALARM_ID_INDEX = 9;
- private static final int ALARM_STATE_INDEX = 10;
-
- private static final int COLUMN_COUNT = ALARM_STATE_INDEX + 1;
-
- public static ContentValues createContentValues(AlarmInstance instance) {
- ContentValues values = new ContentValues(COLUMN_COUNT);
- if (instance.mId != INVALID_ID) {
- values.put(_ID, instance.mId);
- }
-
- values.put(YEAR, instance.mYear);
- values.put(MONTH, instance.mMonth);
- values.put(DAY, instance.mDay);
- values.put(HOUR, instance.mHour);
- values.put(MINUTES, instance.mMinute);
- values.put(LABEL, instance.mLabel);
- values.put(VIBRATE, instance.mVibrate ? 1 : 0);
- if (instance.mRingtone == null) {
- // We want to put null in the database, so we'll be able
- // to pick up on changes to the default alarm
- values.putNull(RINGTONE);
- } else {
- values.put(RINGTONE, instance.mRingtone.toString());
- }
- values.put(ALARM_ID, instance.mAlarmId);
- values.put(ALARM_STATE, instance.mAlarmState);
- return values;
- }
-
- public static Intent createIntent(String action, long instanceId) {
- return new Intent(action).setData(getContentUri(instanceId));
- }
-
- public static Intent createIntent(Context context, Class<?> cls, long instanceId) {
- return new Intent(context, cls).setData(getContentUri(instanceId));
- }
-
- public static long getId(Uri contentUri) {
- return ContentUris.parseId(contentUri);
- }
-
- /**
- * @return the {@link Uri} identifying the alarm instance
- */
- public static Uri getContentUri(long instanceId) {
- return ContentUris.withAppendedId(CONTENT_URI, instanceId);
- }
-
- /**
- * Get alarm instance from instanceId.
- *
- * @param cr provides access to the content model
- * @param instanceId for the desired instance.
- * @return instance if found, null otherwise
- */
- public static AlarmInstance getInstance(ContentResolver cr, long instanceId) {
- try (Cursor cursor = cr.query(getContentUri(instanceId), QUERY_COLUMNS, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- return new AlarmInstance(cursor, false /* joinedTable */);
- }
- }
-
- return null;
- }
-
- /**
- * Get alarm instance for the {@code contentUri}.
- *
- * @param cr provides access to the content model
- * @param contentUri the {@link #getContentUri deeplink} for the desired instance
- * @return instance if found, null otherwise
- */
- public static AlarmInstance getInstance(ContentResolver cr, Uri contentUri) {
- final long instanceId = ContentUris.parseId(contentUri);
- return getInstance(cr, instanceId);
- }
-
- /**
- * Get an alarm instances by alarmId.
- *
- * @param contentResolver provides access to the content model
- * @param alarmId of instances desired.
- * @return list of alarms instances that are owned by alarmId.
- */
- public static List<AlarmInstance> getInstancesByAlarmId(ContentResolver contentResolver,
- long alarmId) {
- return getInstances(contentResolver, ALARM_ID + "=" + alarmId);
- }
-
- /**
- * Get the next instance of an alarm given its alarmId
- * @param contentResolver provides access to the content model
- * @param alarmId of instance desired
- * @return the next instance of an alarm by alarmId.
- */
- public static AlarmInstance getNextUpcomingInstanceByAlarmId(ContentResolver contentResolver,
- long alarmId) {
- final List<AlarmInstance> alarmInstances = getInstancesByAlarmId(contentResolver, alarmId);
- if (alarmInstances.isEmpty()) {
- return null;
- }
- AlarmInstance nextAlarmInstance = alarmInstances.get(0);
- for (AlarmInstance instance : alarmInstances) {
- if (instance.getAlarmTime().before(nextAlarmInstance.getAlarmTime())) {
- nextAlarmInstance = instance;
- }
- }
- return nextAlarmInstance;
- }
-
- /**
- * Get alarm instance by id and state.
- */
- public static List<AlarmInstance> getInstancesByInstanceIdAndState(
- ContentResolver contentResolver, long alarmInstanceId, int state) {
- return getInstances(contentResolver, _ID + "=" + alarmInstanceId + " AND " + ALARM_STATE +
- "=" + state);
- }
-
- /**
- * Get alarm instances in the specified state.
- */
- public static List<AlarmInstance> getInstancesByState(
- ContentResolver contentResolver, int state) {
- return getInstances(contentResolver, ALARM_STATE + "=" + state);
- }
-
- /**
- * Get a list of instances given selection.
- *
- * @param cr provides access to the content model
- * @param selection A filter declaring which rows to return, formatted as an
- * SQL WHERE clause (excluding the WHERE itself). Passing null will
- * return all rows for the given URI.
- * @param selectionArgs You may include ?s in selection, which will be
- * replaced by the values from selectionArgs, in the order that they
- * appear in the selection. The values will be bound as Strings.
- * @return list of alarms matching where clause or empty list if none found.
- */
- public static List<AlarmInstance> getInstances(ContentResolver cr, String selection,
- String... selectionArgs) {
- final List<AlarmInstance> result = new LinkedList<>();
- try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- do {
- result.add(new AlarmInstance(cursor, false /* joinedTable */));
- } while (cursor.moveToNext());
- }
- }
-
- return result;
- }
-
- public static AlarmInstance addInstance(ContentResolver contentResolver,
- AlarmInstance instance) {
- // Make sure we are not adding a duplicate instances. This is not a
- // fix and should never happen. This is only a safe guard against bad code, and you
- // should fix the root issue if you see the error message.
- String dupSelector = AlarmInstance.ALARM_ID + " = " + instance.mAlarmId;
- for (AlarmInstance otherInstances : getInstances(contentResolver, dupSelector)) {
- if (otherInstances.getAlarmTime().equals(instance.getAlarmTime())) {
- LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to "
- + instance);
- // Copy over the new instance values and update the db
- instance.mId = otherInstances.mId;
- updateInstance(contentResolver, instance);
- return instance;
- }
- }
-
- ContentValues values = createContentValues(instance);
- Uri uri = contentResolver.insert(CONTENT_URI, values);
- instance.mId = getId(uri);
- return instance;
- }
-
- public static boolean updateInstance(ContentResolver contentResolver, AlarmInstance instance) {
- if (instance.mId == INVALID_ID) return false;
- ContentValues values = createContentValues(instance);
- long rowsUpdated = contentResolver.update(getContentUri(instance.mId), values, null, null);
- return rowsUpdated == 1;
- }
-
- public static boolean deleteInstance(ContentResolver contentResolver, long instanceId) {
- if (instanceId == INVALID_ID) return false;
- int deletedRows = contentResolver.delete(getContentUri(instanceId), "", null);
- return deletedRows == 1;
- }
-
- public static void deleteOtherInstances(Context context, ContentResolver contentResolver,
- long alarmId, long instanceId) {
- final List<AlarmInstance> instances = getInstancesByAlarmId(contentResolver, alarmId);
- for (AlarmInstance instance : instances) {
- if (instance.mId != instanceId) {
- AlarmStateManager.unregisterInstance(context, instance);
- deleteInstance(contentResolver, instance.mId);
- }
- }
- }
-
- // Public fields
- public long mId;
- public int mYear;
- public int mMonth;
- public int mDay;
- public int mHour;
- public int mMinute;
- public String mLabel;
- public boolean mVibrate;
- public Uri mRingtone;
- public Long mAlarmId;
- public int mAlarmState;
-
- public AlarmInstance(Calendar calendar, Long alarmId) {
- this(calendar);
- mAlarmId = alarmId;
- }
-
- public AlarmInstance(Calendar calendar) {
- mId = INVALID_ID;
- setAlarmTime(calendar);
- mLabel = "";
- mVibrate = false;
- mRingtone = null;
- mAlarmState = SILENT_STATE;
- }
-
- public AlarmInstance(AlarmInstance instance) {
- this.mId = instance.mId;
- this.mYear = instance.mYear;
- this.mMonth = instance.mMonth;
- this.mDay = instance.mDay;
- this.mHour = instance.mHour;
- this.mMinute = instance.mMinute;
- this.mLabel = instance.mLabel;
- this.mVibrate = instance.mVibrate;
- this.mRingtone = instance.mRingtone;
- this.mAlarmId = instance.mAlarmId;
- this.mAlarmState = instance.mAlarmState;
- }
-
- public AlarmInstance(Cursor c, boolean joinedTable) {
- if (joinedTable) {
- mId = c.getLong(Alarm.INSTANCE_ID_INDEX);
- mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX);
- mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX);
- mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX);
- mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX);
- mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX);
- mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX);
- mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1;
- } else {
- mId = c.getLong(ID_INDEX);
- mYear = c.getInt(YEAR_INDEX);
- mMonth = c.getInt(MONTH_INDEX);
- mDay = c.getInt(DAY_INDEX);
- mHour = c.getInt(HOUR_INDEX);
- mMinute = c.getInt(MINUTES_INDEX);
- mLabel = c.getString(LABEL_INDEX);
- mVibrate = c.getInt(VIBRATE_INDEX) == 1;
- }
- if (c.isNull(RINGTONE_INDEX)) {
- // Should we be saving this with the current ringtone or leave it null
- // so it changes when user changes default ringtone?
- mRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
- } else {
- mRingtone = Uri.parse(c.getString(RINGTONE_INDEX));
- }
-
- if (!c.isNull(ALARM_ID_INDEX)) {
- mAlarmId = c.getLong(ALARM_ID_INDEX);
- }
- mAlarmState = c.getInt(ALARM_STATE_INDEX);
- }
-
- /**
- * @return the deeplink that identifies this alarm instance
- */
- public Uri getContentUri() {
- return getContentUri(mId);
- }
-
- public String getLabelOrDefault(Context context) {
- return mLabel.isEmpty() ? context.getString(R.string.default_label) : mLabel;
- }
-
- public void setAlarmTime(Calendar calendar) {
- mYear = calendar.get(Calendar.YEAR);
- mMonth = calendar.get(Calendar.MONTH);
- mDay = calendar.get(Calendar.DAY_OF_MONTH);
- mHour = calendar.get(Calendar.HOUR_OF_DAY);
- mMinute = calendar.get(Calendar.MINUTE);
- }
-
- /**
- * Return the time when a alarm should fire.
- *
- * @return the time
- */
- public Calendar getAlarmTime() {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.YEAR, mYear);
- calendar.set(Calendar.MONTH, mMonth);
- calendar.set(Calendar.DAY_OF_MONTH, mDay);
- calendar.set(Calendar.HOUR_OF_DAY, mHour);
- calendar.set(Calendar.MINUTE, mMinute);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- return calendar;
- }
-
- /**
- * Return the time when a low priority notification should be shown.
- *
- * @return the time
- */
- public Calendar getLowNotificationTime() {
- Calendar calendar = getAlarmTime();
- calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET);
- return calendar;
- }
-
- /**
- * Return the time when a high priority notification should be shown.
- *
- * @return the time
- */
- public Calendar getHighNotificationTime() {
- Calendar calendar = getAlarmTime();
- calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET);
- return calendar;
- }
-
- /**
- * Return the time when a missed notification should be removed.
- *
- * @return the time
- */
- public Calendar getMissedTimeToLive() {
- Calendar calendar = getAlarmTime();
- calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET);
- return calendar;
- }
-
- /**
- * Return the time when the alarm should stop firing and be marked as missed.
- *
- * @return the time when alarm should be silence, or null if never
- */
- public Calendar getTimeout() {
- final int timeoutMinutes = DataModel.getDataModel().getAlarmTimeout();
-
- // Alarm silence has been set to "None"
- if (timeoutMinutes < 0) {
- return null;
- }
-
- Calendar calendar = getAlarmTime();
- calendar.add(Calendar.MINUTE, timeoutMinutes);
- return calendar;
- }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof AlarmInstance)) return false;
- final AlarmInstance other = (AlarmInstance) o;
- return mId == other.mId;
- }
-
- @Override
- public int hashCode() {
- return Long.valueOf(mId).hashCode();
- }
-
- @Override
- public String toString() {
- return "AlarmInstance{" +
- "mId=" + mId +
- ", mYear=" + mYear +
- ", mMonth=" + mMonth +
- ", mDay=" + mDay +
- ", mHour=" + mHour +
- ", mMinute=" + mMinute +
- ", mLabel=" + mLabel +
- ", mVibrate=" + mVibrate +
- ", mRingtone=" + mRingtone +
- ", mAlarmId=" + mAlarmId +
- ", mAlarmState=" + mAlarmState +
- '}';
- }
-}
diff --git a/src/com/android/deskclock/provider/AlarmInstance.kt b/src/com/android/deskclock/provider/AlarmInstance.kt
new file mode 100644
index 0000000..d69ae5b
--- /dev/null
+++ b/src/com/android/deskclock/provider/AlarmInstance.kt
@@ -0,0 +1,527 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.provider
+
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.database.Cursor
+import android.media.RingtoneManager
+import android.net.Uri
+import android.provider.BaseColumns._ID
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.alarms.AlarmStateManager
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.util.Calendar
+import java.util.LinkedList
+
+class AlarmInstance : InstancesColumns {
+ // Public fields
+ var mYear = 0
+ var mMonth = 0
+ var mDay = 0
+ var mHour = 0
+ var mMinute = 0
+
+ @JvmField
+ var mId: Long = 0
+
+ @JvmField
+ var mLabel: String? = null
+
+ @JvmField
+ var mVibrate = false
+
+ @JvmField
+ var mRingtone: Uri? = null
+
+ @JvmField
+ var mAlarmId: Long? = null
+
+ @JvmField
+ var mAlarmState: Int
+
+ constructor(calendar: Calendar, alarmId: Long?) : this(calendar) {
+ mAlarmId = alarmId
+ }
+
+ constructor(calendar: Calendar) {
+ mId = INVALID_ID
+ alarmTime = calendar
+ mLabel = ""
+ mVibrate = false
+ mRingtone = null
+ mAlarmState = InstancesColumns.SILENT_STATE
+ }
+
+ constructor(instance: AlarmInstance) {
+ mId = instance.mId
+ mYear = instance.mYear
+ mMonth = instance.mMonth
+ mDay = instance.mDay
+ mHour = instance.mHour
+ mMinute = instance.mMinute
+ mLabel = instance.mLabel
+ mVibrate = instance.mVibrate
+ mRingtone = instance.mRingtone
+ mAlarmId = instance.mAlarmId
+ mAlarmState = instance.mAlarmState
+ }
+
+ constructor(c: Cursor, joinedTable: Boolean) {
+ if (joinedTable) {
+ mId = c.getLong(Alarm.INSTANCE_ID_INDEX)
+ mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX)
+ mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX)
+ mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX)
+ mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX)
+ mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX)
+ mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX)
+ mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1
+ } else {
+ mId = c.getLong(ID_INDEX)
+ mYear = c.getInt(YEAR_INDEX)
+ mMonth = c.getInt(MONTH_INDEX)
+ mDay = c.getInt(DAY_INDEX)
+ mHour = c.getInt(HOUR_INDEX)
+ mMinute = c.getInt(MINUTES_INDEX)
+ mLabel = c.getString(LABEL_INDEX)
+ mVibrate = c.getInt(VIBRATE_INDEX) == 1
+ }
+ mRingtone = if (c.isNull(RINGTONE_INDEX)) {
+ // Should we be saving this with the current ringtone or leave it null
+ // so it changes when user changes default ringtone?
+ RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+ } else {
+ Uri.parse(c.getString(RINGTONE_INDEX))
+ }
+
+ if (!c.isNull(ALARM_ID_INDEX)) {
+ mAlarmId = c.getLong(ALARM_ID_INDEX)
+ }
+ mAlarmState = c.getInt(ALARM_STATE_INDEX)
+ }
+
+ /**
+ * @return the deeplink that identifies this alarm instance
+ */
+ val contentUri: Uri
+ get() = getContentUri(mId)
+
+ fun getLabelOrDefault(context: Context): String {
+ return if (mLabel.isNullOrEmpty()) context.getString(R.string.default_label) else mLabel!!
+ }
+
+ /**
+ * Return the time when a alarm should fire.
+ *
+ * @return the time
+ */
+ var alarmTime: Calendar
+ get() {
+ val calendar = Calendar.getInstance()
+ calendar[Calendar.YEAR] = mYear
+ calendar[Calendar.MONTH] = mMonth
+ calendar[Calendar.DAY_OF_MONTH] = mDay
+ calendar[Calendar.HOUR_OF_DAY] = mHour
+ calendar[Calendar.MINUTE] = mMinute
+ calendar[Calendar.SECOND] = 0
+ calendar[Calendar.MILLISECOND] = 0
+ return calendar
+ }
+ set(calendar) {
+ mYear = calendar[Calendar.YEAR]
+ mMonth = calendar[Calendar.MONTH]
+ mDay = calendar[Calendar.DAY_OF_MONTH]
+ mHour = calendar[Calendar.HOUR_OF_DAY]
+ mMinute = calendar[Calendar.MINUTE]
+ }
+
+ /**
+ * Return the time when a low priority notification should be shown.
+ *
+ * @return the time
+ */
+ val lowNotificationTime: Calendar
+ get() {
+ val calendar = alarmTime
+ calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET)
+ return calendar
+ }
+
+ /**
+ * Return the time when a high priority notification should be shown.
+ *
+ * @return the time
+ */
+ val highNotificationTime: Calendar
+ get() {
+ val calendar = alarmTime
+ calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET)
+ return calendar
+ }
+
+ /**
+ * Return the time when a missed notification should be removed.
+ *
+ * @return the time
+ */
+ val missedTimeToLive: Calendar
+ get() {
+ val calendar = alarmTime
+ calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET)
+ return calendar
+ }
+
+ /**
+ * Return the time when the alarm should stop firing and be marked as missed.
+ *
+ * @return the time when alarm should be silence, or null if never
+ */
+ val timeout: Calendar?
+ get() {
+ val timeoutMinutes = DataModel.dataModel.alarmTimeout
+
+ // Alarm silence has been set to "None"
+ if (timeoutMinutes < 0) {
+ return null
+ }
+
+ val calendar = alarmTime
+ calendar.add(Calendar.MINUTE, timeoutMinutes)
+ return calendar
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other !is AlarmInstance) return false
+ return mId == other.mId
+ }
+
+ override fun hashCode(): Int {
+ return java.lang.Long.valueOf(mId).hashCode()
+ }
+
+ override fun toString(): String {
+ return "AlarmInstance{" +
+ "mId=" + mId +
+ ", mYear=" + mYear +
+ ", mMonth=" + mMonth +
+ ", mDay=" + mDay +
+ ", mHour=" + mHour +
+ ", mMinute=" + mMinute +
+ ", mLabel=" + mLabel +
+ ", mVibrate=" + mVibrate +
+ ", mRingtone=" + mRingtone +
+ ", mAlarmId=" + mAlarmId +
+ ", mAlarmState=" + mAlarmState +
+ '}'
+ }
+
+ companion object {
+ /**
+ * Offset from alarm time to show low priority notification
+ */
+ const val LOW_NOTIFICATION_HOUR_OFFSET = -2
+
+ /**
+ * Offset from alarm time to show high priority notification
+ */
+ const val HIGH_NOTIFICATION_MINUTE_OFFSET = -30
+
+ /**
+ * Offset from alarm time to stop showing missed notification.
+ */
+ private const val MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12
+
+ /**
+ * AlarmInstances start with an invalid id when it hasn't been saved to the database.
+ */
+ const val INVALID_ID: Long = -1
+
+ private val QUERY_COLUMNS = arrayOf(
+ _ID,
+ InstancesColumns.YEAR,
+ InstancesColumns.MONTH,
+ InstancesColumns.DAY,
+ InstancesColumns.HOUR,
+ InstancesColumns.MINUTES,
+ AlarmSettingColumns.LABEL,
+ AlarmSettingColumns.VIBRATE,
+ AlarmSettingColumns.RINGTONE,
+ InstancesColumns.ALARM_ID,
+ InstancesColumns.ALARM_STATE
+ )
+
+ /**
+ * These save calls to cursor.getColumnIndexOrThrow()
+ * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
+ */
+ private const val ID_INDEX = 0
+ private const val YEAR_INDEX = 1
+ private const val MONTH_INDEX = 2
+ private const val DAY_INDEX = 3
+ private const val HOUR_INDEX = 4
+ private const val MINUTES_INDEX = 5
+ private const val LABEL_INDEX = 6
+ private const val VIBRATE_INDEX = 7
+ private const val RINGTONE_INDEX = 8
+ private const val ALARM_ID_INDEX = 9
+ private const val ALARM_STATE_INDEX = 10
+
+ private const val COLUMN_COUNT = ALARM_STATE_INDEX + 1
+
+ @JvmStatic
+ fun createContentValues(instance: AlarmInstance): ContentValues {
+ val values = ContentValues(COLUMN_COUNT)
+ if (instance.mId != INVALID_ID) {
+ values.put(_ID, instance.mId)
+ }
+
+ values.put(InstancesColumns.YEAR, instance.mYear)
+ values.put(InstancesColumns.MONTH, instance.mMonth)
+ values.put(InstancesColumns.DAY, instance.mDay)
+ values.put(InstancesColumns.HOUR, instance.mHour)
+ values.put(InstancesColumns.MINUTES, instance.mMinute)
+ values.put(AlarmSettingColumns.LABEL, instance.mLabel)
+ values.put(AlarmSettingColumns.VIBRATE, if (instance.mVibrate) 1 else 0)
+ if (instance.mRingtone == null) {
+ // We want to put null in the database, so we'll be able
+ // to pick up on changes to the default alarm
+ values.putNull(AlarmSettingColumns.RINGTONE)
+ } else {
+ values.put(AlarmSettingColumns.RINGTONE, instance.mRingtone.toString())
+ }
+ values.put(InstancesColumns.ALARM_ID, instance.mAlarmId)
+ values.put(InstancesColumns.ALARM_STATE, instance.mAlarmState)
+ return values
+ }
+
+ fun createIntent(action: String?, instanceId: Long): Intent {
+ return Intent(action).setData(getContentUri(instanceId))
+ }
+
+ @JvmStatic
+ fun createIntent(context: Context?, cls: Class<*>?, instanceId: Long): Intent {
+ return Intent(context, cls).setData(getContentUri(instanceId))
+ }
+
+ @JvmStatic
+ fun getId(contentUri: Uri): Long {
+ return ContentUris.parseId(contentUri)
+ }
+
+ /**
+ * @return the [Uri] identifying the alarm instance
+ */
+ fun getContentUri(instanceId: Long): Uri {
+ return ContentUris.withAppendedId(InstancesColumns.CONTENT_URI, instanceId)
+ }
+
+ /**
+ * Get alarm instance from instanceId.
+ *
+ * @param cr provides access to the content model
+ * @param instanceId for the desired instance.
+ * @return instance if found, null otherwise
+ */
+ @JvmStatic
+ fun getInstance(cr: ContentResolver, instanceId: Long): AlarmInstance? {
+ val cursor: Cursor? =
+ cr.query(getContentUri(instanceId), QUERY_COLUMNS, null, null, null)
+ cursor?.let {
+ if (cursor.moveToFirst()) {
+ return AlarmInstance(cursor, false /* joinedTable */)
+ }
+ }
+ return null
+ }
+
+ /**
+ * Get alarm instance for the `contentUri`.
+ *
+ * @param cr provides access to the content model
+ * @param contentUri the [deeplink][.getContentUri] for the desired instance
+ * @return instance if found, null otherwise
+ */
+ fun getInstance(cr: ContentResolver, contentUri: Uri): AlarmInstance? {
+ val instanceId: Long = ContentUris.parseId(contentUri)
+ return getInstance(cr, instanceId)
+ }
+
+ /**
+ * Get an alarm instances by alarmId.
+ *
+ * @param contentResolver provides access to the content model
+ * @param alarmId of instances desired.
+ * @return list of alarms instances that are owned by alarmId.
+ */
+ @JvmStatic
+ fun getInstancesByAlarmId(
+ contentResolver: ContentResolver,
+ alarmId: Long
+ ): List<AlarmInstance> {
+ return getInstances(contentResolver, InstancesColumns.ALARM_ID + "=" + alarmId)
+ }
+
+ /**
+ * Get the next instance of an alarm given its alarmId
+ * @param contentResolver provides access to the content model
+ * @param alarmId of instance desired
+ * @return the next instance of an alarm by alarmId.
+ */
+ @JvmStatic
+ fun getNextUpcomingInstanceByAlarmId(
+ contentResolver: ContentResolver,
+ alarmId: Long
+ ): AlarmInstance? {
+ val alarmInstances = getInstancesByAlarmId(contentResolver, alarmId)
+ if (alarmInstances.isEmpty()) {
+ return null
+ }
+ var nextAlarmInstance = alarmInstances[0]
+ for (instance in alarmInstances) {
+ if (instance.alarmTime.before(nextAlarmInstance.alarmTime)) {
+ nextAlarmInstance = instance
+ }
+ }
+ return nextAlarmInstance
+ }
+
+ /**
+ * Get alarm instance by id and state.
+ */
+ fun getInstancesByInstanceIdAndState(
+ contentResolver: ContentResolver,
+ alarmInstanceId: Long,
+ state: Int
+ ): List<AlarmInstance> {
+ return getInstances(contentResolver,
+ _ID.toString() + "=" + alarmInstanceId + " AND " +
+ InstancesColumns.ALARM_STATE + "=" + state)
+ }
+
+ /**
+ * Get alarm instances in the specified state.
+ */
+ @JvmStatic
+ fun getInstancesByState(
+ contentResolver: ContentResolver,
+ state: Int
+ ): List<AlarmInstance> {
+ return getInstances(contentResolver,
+ InstancesColumns.ALARM_STATE + "=" + state)
+ }
+
+ /**
+ * Get a list of instances given selection.
+ *
+ * @param cr provides access to the content model
+ * @param selection A filter declaring which rows to return, formatted as an
+ * SQL WHERE clause (excluding the WHERE itself). Passing null will
+ * return all rows for the given URI.
+ * @param selectionArgs You may include ?s in selection, which will be
+ * replaced by the values from selectionArgs, in the order that they
+ * appear in the selection. The values will be bound as Strings.
+ * @return list of alarms matching where clause or empty list if none found.
+ */
+ @JvmStatic
+ fun getInstances(
+ cr: ContentResolver,
+ selection: String?,
+ vararg selectionArgs: String?
+ ): MutableList<AlarmInstance> {
+ val result: MutableList<AlarmInstance> = LinkedList()
+ val cursor: Cursor? =
+ cr.query(InstancesColumns.CONTENT_URI, QUERY_COLUMNS,
+ selection, selectionArgs, null)
+ cursor?.let {
+ if (cursor.moveToFirst()) {
+ do {
+ result.add(AlarmInstance(cursor, false /* joinedTable */))
+ } while (cursor.moveToNext())
+ }
+ }
+
+ return result
+ }
+
+ @JvmStatic
+ fun addInstance(
+ contentResolver: ContentResolver,
+ instance: AlarmInstance
+ ): AlarmInstance {
+ // Make sure we are not adding a duplicate instances. This is not a
+ // fix and should never happen. This is only a safe guard against bad code, and you
+ // should fix the root issue if you see the error message.
+ val dupSelector = InstancesColumns.ALARM_ID + " = " + instance.mAlarmId
+ for (otherInstances in getInstances(contentResolver, dupSelector)) {
+ if (otherInstances.alarmTime == instance.alarmTime) {
+ LogUtils.i("Detected duplicate instance in DB. Updating " +
+ otherInstances + " to " + instance)
+ // Copy over the new instance values and update the db
+ instance.mId = otherInstances.mId
+ updateInstance(contentResolver, instance)
+ return instance
+ }
+ }
+
+ val values: ContentValues = createContentValues(instance)
+ val uri: Uri = contentResolver.insert(InstancesColumns.CONTENT_URI, values)!!
+ instance.mId = getId(uri)
+ return instance
+ }
+
+ @JvmStatic
+ fun updateInstance(contentResolver: ContentResolver, instance: AlarmInstance): Boolean {
+ if (instance.mId == INVALID_ID) return false
+ val values: ContentValues = createContentValues(instance)
+ val rowsUpdated: Long =
+ contentResolver.update(getContentUri(instance.mId), values, null, null).toLong()
+ return rowsUpdated == 1L
+ }
+
+ @JvmStatic
+ fun deleteInstance(contentResolver: ContentResolver, instanceId: Long): Boolean {
+ if (instanceId == INVALID_ID) return false
+ val deletedRows: Int = contentResolver.delete(getContentUri(instanceId), "", null)
+ return deletedRows == 1
+ }
+
+ @JvmStatic
+ fun deleteOtherInstances(
+ context: Context,
+ contentResolver: ContentResolver,
+ alarmId: Long,
+ instanceId: Long
+ ) {
+ val instances = getInstancesByAlarmId(contentResolver, alarmId)
+ for (instance in instances) {
+ if (instance.mId != instanceId) {
+ AlarmStateManager.unregisterInstance(context, instance)
+ deleteInstance(contentResolver, instance.mId)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/ClockContract.java b/src/com/android/deskclock/provider/ClockContract.java
deleted file mode 100644
index 5335ccc..0000000
--- a/src/com/android/deskclock/provider/ClockContract.java
+++ /dev/null
@@ -1,264 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.provider;
-
-import android.net.Uri;
-import android.provider.BaseColumns;
-
-import com.android.deskclock.BuildConfig;
-
-/**
- * <p>
- * The contract between the clock provider and desk clock. Contains
- * definitions for the supported URIs and data columns.
- * </p>
- * <h3>Overview</h3>
- * <p>
- * ClockContract defines the data model of clock related information.
- * This data is stored in a number of tables:
- * </p>
- * <ul>
- * <li>The {@link AlarmsColumns} table holds the user created alarms</li>
- * <li>The {@link InstancesColumns} table holds the current state of each
- * alarm in the AlarmsColumn table.
- * </li>
- * </ul>
- */
-public final class ClockContract {
- /**
- * This authority is used for writing to or querying from the clock
- * provider.
- */
- public static final String AUTHORITY = BuildConfig.APPLICATION_ID;
-
- /**
- * This utility class cannot be instantiated
- */
- private ClockContract() {}
-
- /**
- * Constants for tables with AlarmSettings.
- */
- private interface AlarmSettingColumns extends BaseColumns {
- /**
- * This string is used to indicate no ringtone.
- */
- Uri NO_RINGTONE_URI = Uri.EMPTY;
-
- /**
- * This string is used to indicate no ringtone.
- */
- String NO_RINGTONE = NO_RINGTONE_URI.toString();
-
- /**
- * True if alarm should vibrate
- * <p>Type: BOOLEAN</p>
- */
- String VIBRATE = "vibrate";
-
- /**
- * Alarm label.
- *
- * <p>Type: STRING</p>
- */
- String LABEL = "label";
-
- /**
- * Audio alert to play when alarm triggers. Null entry
- * means use system default and entry that equal
- * Uri.EMPTY.toString() means no ringtone.
- *
- * <p>Type: STRING</p>
- */
- String RINGTONE = "ringtone";
- }
-
- /**
- * Constants for the Alarms table, which contains the user created alarms.
- */
- protected interface AlarmsColumns extends AlarmSettingColumns, BaseColumns {
- /**
- * The content:// style URL for this table.
- */
- Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/alarms");
-
- /**
- * The content:// style URL for the alarms with instance tables, which is used to get the
- * next firing instance and the current state of an alarm.
- */
- Uri ALARMS_WITH_INSTANCES_URI = Uri.parse("content://" + AUTHORITY
- + "/alarms_with_instances");
-
- /**
- * Hour in 24-hour localtime 0 - 23.
- * <p>Type: INTEGER</p>
- */
- String HOUR = "hour";
-
- /**
- * Minutes in localtime 0 - 59.
- * <p>Type: INTEGER</p>
- */
- String MINUTES = "minutes";
-
- /**
- * Days of the week encoded as a bit set.
- * <p>Type: INTEGER</p>
- *
- * {@link com.android.deskclock.data.Weekdays}
- */
- String DAYS_OF_WEEK = "daysofweek";
-
- /**
- * True if alarm is active.
- * <p>Type: BOOLEAN</p>
- */
- String ENABLED = "enabled";
-
- /**
- * Determine if alarm is deleted after it has been used.
- * <p>Type: INTEGER</p>
- */
- String DELETE_AFTER_USE = "delete_after_use";
- }
-
- /**
- * Constants for the Instance table, which contains the state of each alarm.
- */
- protected interface InstancesColumns extends AlarmSettingColumns, BaseColumns {
- /**
- * The content:// style URL for this table.
- */
- Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/instances");
-
- /**
- * Alarm state when to show no notification.
- *
- * Can transitions to:
- * LOW_NOTIFICATION_STATE
- */
- int SILENT_STATE = 0;
-
- /**
- * Alarm state to show low priority alarm notification.
- *
- * Can transitions to:
- * HIDE_NOTIFICATION_STATE
- * HIGH_NOTIFICATION_STATE
- * DISMISSED_STATE
- */
- int LOW_NOTIFICATION_STATE = 1;
-
- /**
- * Alarm state to hide low priority alarm notification.
- *
- * Can transitions to:
- * HIGH_NOTIFICATION_STATE
- */
- int HIDE_NOTIFICATION_STATE = 2;
-
- /**
- * Alarm state to show high priority alarm notification.
- *
- * Can transitions to:
- * DISMISSED_STATE
- * FIRED_STATE
- */
- int HIGH_NOTIFICATION_STATE = 3;
-
- /**
- * Alarm state when alarm is in snooze.
- *
- * Can transitions to:
- * DISMISSED_STATE
- * FIRED_STATE
- */
- int SNOOZE_STATE = 4;
-
- /**
- * Alarm state when alarm is being fired.
- *
- * Can transitions to:
- * DISMISSED_STATE
- * SNOOZED_STATE
- * MISSED_STATE
- */
- int FIRED_STATE = 5;
-
- /**
- * Alarm state when alarm has been missed.
- *
- * Can transitions to:
- * DISMISSED_STATE
- */
- int MISSED_STATE = 6;
-
- /**
- * Alarm state when alarm is done.
- */
- int DISMISSED_STATE = 7;
-
- /**
- * Alarm state when alarm has been dismissed before its intended firing time.
- */
- int PREDISMISSED_STATE = 8;
-
- /**
- * Alarm year.
- *
- * <p>Type: INTEGER</p>
- */
- String YEAR = "year";
-
- /**
- * Alarm month in year.
- *
- * <p>Type: INTEGER</p>
- */
- String MONTH = "month";
-
- /**
- * Alarm day in month.
- *
- * <p>Type: INTEGER</p>
- */
- String DAY = "day";
-
- /**
- * Alarm hour in 24-hour localtime 0 - 23.
- * <p>Type: INTEGER</p>
- */
- String HOUR = "hour";
-
- /**
- * Alarm minutes in localtime 0 - 59
- * <p>Type: INTEGER</p>
- */
- String MINUTES = "minutes";
-
- /**
- * Foreign key to Alarms table
- * <p>Type: INTEGER (long)</p>
- */
- String ALARM_ID = "alarm_id";
-
- /**
- * Alarm state
- * <p>Type: INTEGER</p>
- */
- String ALARM_STATE = "alarm_state";
- }
-}
diff --git a/src/com/android/deskclock/provider/ClockContract.kt b/src/com/android/deskclock/provider/ClockContract.kt
new file mode 100644
index 0000000..ee922ce
--- /dev/null
+++ b/src/com/android/deskclock/provider/ClockContract.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.provider
+
+import android.net.Uri
+import android.provider.BaseColumns
+
+import com.android.deskclock.BuildConfig
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+/**
+ * The contract between the clock provider and desk clock. Contains
+ * definitions for the supported URIs and data columns.
+ *
+ * <h3>Overview</h3>
+ *
+ * ClockContract defines the data model of clock related information.
+ * This data is stored in a number of tables:
+ *
+ * * The [AlarmsColumns] table holds the user created alarms
+ * * The [InstancesColumns] table holds the current state of each
+ * alarm in the AlarmsColumn table.
+ */
+object ClockContract {
+ /**
+ * This authority is used for writing to or querying from the clock
+ * provider.
+ */
+ @JvmField
+ val AUTHORITY: String = BuildConfig.APPLICATION_ID
+
+ /**
+ * Constants for tables with AlarmSettings.
+ */
+ interface AlarmSettingColumns : BaseColumns {
+ companion object {
+ /**
+ * This string is used to indicate no ringtone.
+ */
+ @JvmField
+ val NO_RINGTONE_URI: Uri = Uri.EMPTY
+
+ /**
+ * This string is used to indicate no ringtone.
+ */
+ @JvmField
+ val NO_RINGTONE: String = NO_RINGTONE_URI.toString()
+
+ /**
+ * True if alarm should vibrate
+ *
+ * Type: BOOLEAN
+ */
+ @JvmField
+ val VIBRATE = "vibrate"
+
+ /**
+ * Alarm label.
+ *
+ * Type: STRING
+ */
+ @JvmField
+ val LABEL = "label"
+
+ /**
+ * Audio alert to play when alarm triggers. Null entry
+ * means use system default and entry that equal
+ * Uri.EMPTY.toString() means no ringtone.
+ *
+ * Type: STRING
+ */
+ @JvmField
+ val RINGTONE = "ringtone"
+ }
+ }
+
+ /**
+ * Constants for the Alarms table, which contains the user created alarms.
+ */
+ interface AlarmsColumns : AlarmSettingColumns, BaseColumns {
+ companion object {
+ /**
+ * The content:// style URL for this table.
+ */
+ val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/alarms")
+
+ /**
+ * The content:// style URL for the alarms with instance tables, which is used to get the
+ * next firing instance and the current state of an alarm.
+ */
+ val ALARMS_WITH_INSTANCES_URI: Uri = Uri.parse("content://" + AUTHORITY +
+ "/alarms_with_instances")
+
+ /**
+ * Hour in 24-hour localtime 0 - 23.
+ *
+ * Type: INTEGER
+ */
+ const val HOUR = "hour"
+
+ /**
+ * Minutes in localtime 0 - 59.
+ *
+ * Type: INTEGER
+ */
+ const val MINUTES = "minutes"
+
+ /**
+ * Days of the week encoded as a bit set.
+ *
+ * Type: INTEGER
+ *
+ * [com.android.deskclock.data.Weekdays]
+ */
+ const val DAYS_OF_WEEK = "daysofweek"
+
+ /**
+ * True if alarm is active.
+ *
+ * Type: BOOLEAN
+ */
+ const val ENABLED = "enabled"
+
+ /**
+ * Determine if alarm is deleted after it has been used.
+ *
+ * Type: INTEGER
+ */
+ const val DELETE_AFTER_USE = "delete_after_use"
+ }
+ }
+
+ /**
+ * Constants for the Instance table, which contains the state of each alarm.
+ */
+ interface InstancesColumns : AlarmSettingColumns, BaseColumns {
+ companion object {
+ /**
+ * The content:// style URL for this table.
+ */
+ val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/instances")
+
+ /**
+ * Alarm state when to show no notification.
+ *
+ * Can transitions to:
+ * LOW_NOTIFICATION_STATE
+ */
+ const val SILENT_STATE = 0
+
+ /**
+ * Alarm state to show low priority alarm notification.
+ *
+ * Can transitions to:
+ * HIDE_NOTIFICATION_STATE
+ * HIGH_NOTIFICATION_STATE
+ * DISMISSED_STATE
+ */
+ const val LOW_NOTIFICATION_STATE = 1
+
+ /**
+ * Alarm state to hide low priority alarm notification.
+ *
+ * Can transitions to:
+ * HIGH_NOTIFICATION_STATE
+ */
+ const val HIDE_NOTIFICATION_STATE = 2
+
+ /**
+ * Alarm state to show high priority alarm notification.
+ *
+ * Can transitions to:
+ * DISMISSED_STATE
+ * FIRED_STATE
+ */
+ const val HIGH_NOTIFICATION_STATE = 3
+
+ /**
+ * Alarm state when alarm is in snooze.
+ *
+ * Can transitions to:
+ * DISMISSED_STATE
+ * FIRED_STATE
+ */
+ const val SNOOZE_STATE = 4
+
+ /**
+ * Alarm state when alarm is being fired.
+ *
+ * Can transitions to:
+ * DISMISSED_STATE
+ * SNOOZED_STATE
+ * MISSED_STATE
+ */
+ const val FIRED_STATE = 5
+
+ /**
+ * Alarm state when alarm has been missed.
+ *
+ * Can transitions to:
+ * DISMISSED_STATE
+ */
+ const val MISSED_STATE = 6
+
+ /**
+ * Alarm state when alarm is done.
+ */
+ const val DISMISSED_STATE = 7
+
+ /**
+ * Alarm state when alarm has been dismissed before its intended firing time.
+ */
+ const val PREDISMISSED_STATE = 8
+
+ /**
+ * Alarm year.
+ *
+ * Type: INTEGER
+ */
+ const val YEAR = "year"
+
+ /**
+ * Alarm month in year.
+ *
+ * Type: INTEGER
+ */
+ const val MONTH = "month"
+
+ /**
+ * Alarm day in month.
+ *
+ * Type: INTEGER
+ */
+ const val DAY = "day"
+
+ /**
+ * Alarm hour in 24-hour localtime 0 - 23.
+ *
+ * Type: INTEGER
+ */
+ const val HOUR = "hour"
+
+ /**
+ * Alarm minutes in localtime 0 - 59
+ *
+ * Type: INTEGER
+ */
+ const val MINUTES = "minutes"
+
+ /**
+ * Foreign key to Alarms table
+ *
+ * Type: INTEGER (long)
+ */
+ const val ALARM_ID = "alarm_id"
+
+ /**
+ * Alarm state
+ *
+ * Type: INTEGER
+ */
+ const val ALARM_STATE = "alarm_state"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/provider/ClockDatabaseHelper.java b/src/com/android/deskclock/provider/ClockDatabaseHelper.java
deleted file mode 100644
index b6fc900..0000000
--- a/src/com/android/deskclock/provider/ClockDatabaseHelper.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.provider;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.text.TextUtils;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.data.Weekdays;
-
-import java.util.Calendar;
-
-/**
- * Helper class for opening the database from multiple providers. Also provides
- * some common functionality.
- */
-class ClockDatabaseHelper extends SQLiteOpenHelper {
- /**
- * Original Clock Database.
- **/
- private static final int VERSION_5 = 5;
-
- /**
- * Added alarm_instances table
- * Added selected_cities table
- * Added DELETE_AFTER_USE column to alarms table
- */
- private static final int VERSION_6 = 6;
-
- /**
- * Added alarm settings to instance table.
- */
- private static final int VERSION_7 = 7;
-
- /**
- * Removed selected_cities table.
- */
- private static final int VERSION_8 = 8;
-
- // This creates a default alarm at 8:30 for every Mon,Tue,Wed,Thu,Fri
- private static final String DEFAULT_ALARM_1 = "(8, 30, 31, 0, 1, '', NULL, 0);";
-
- // This creates a default alarm at 9:30 for every Sat,Sun
- private static final String DEFAULT_ALARM_2 = "(9, 00, 96, 0, 1, '', NULL, 0);";
-
- // Database and table names
- static final String DATABASE_NAME = "alarms.db";
- static final String OLD_ALARMS_TABLE_NAME = "alarms";
- static final String ALARMS_TABLE_NAME = "alarm_templates";
- static final String INSTANCES_TABLE_NAME = "alarm_instances";
- private static final String SELECTED_CITIES_TABLE_NAME = "selected_cities";
-
- private static void createAlarmsTable(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + ALARMS_TABLE_NAME + " (" +
- ClockContract.AlarmsColumns._ID + " INTEGER PRIMARY KEY," +
- ClockContract.AlarmsColumns.HOUR + " INTEGER NOT NULL, " +
- ClockContract.AlarmsColumns.MINUTES + " INTEGER NOT NULL, " +
- ClockContract.AlarmsColumns.DAYS_OF_WEEK + " INTEGER NOT NULL, " +
- ClockContract.AlarmsColumns.ENABLED + " INTEGER NOT NULL, " +
- ClockContract.AlarmsColumns.VIBRATE + " INTEGER NOT NULL, " +
- ClockContract.AlarmsColumns.LABEL + " TEXT NOT NULL, " +
- ClockContract.AlarmsColumns.RINGTONE + " TEXT, " +
- ClockContract.AlarmsColumns.DELETE_AFTER_USE + " INTEGER NOT NULL DEFAULT 0);");
- LogUtils.i("Alarms Table created");
- }
-
- private static void createInstanceTable(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE " + INSTANCES_TABLE_NAME + " (" +
- ClockContract.InstancesColumns._ID + " INTEGER PRIMARY KEY," +
- ClockContract.InstancesColumns.YEAR + " INTEGER NOT NULL, " +
- ClockContract.InstancesColumns.MONTH + " INTEGER NOT NULL, " +
- ClockContract.InstancesColumns.DAY + " INTEGER NOT NULL, " +
- ClockContract.InstancesColumns.HOUR + " INTEGER NOT NULL, " +
- ClockContract.InstancesColumns.MINUTES + " INTEGER NOT NULL, " +
- ClockContract.InstancesColumns.VIBRATE + " INTEGER NOT NULL, " +
- ClockContract.InstancesColumns.LABEL + " TEXT NOT NULL, " +
- ClockContract.InstancesColumns.RINGTONE + " TEXT, " +
- ClockContract.InstancesColumns.ALARM_STATE + " INTEGER NOT NULL, " +
- ClockContract.InstancesColumns.ALARM_ID + " INTEGER REFERENCES " +
- ALARMS_TABLE_NAME + "(" + ClockContract.AlarmsColumns._ID + ") " +
- "ON UPDATE CASCADE ON DELETE CASCADE" +
- ");");
- LogUtils.i("Instance table created");
- }
-
- public ClockDatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null, VERSION_8);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- createAlarmsTable(db);
- createInstanceTable(db);
-
- // insert default alarms
- LogUtils.i("Inserting default alarms");
- String cs = ", "; //comma and space
- String insertMe = "INSERT INTO " + ALARMS_TABLE_NAME + " (" +
- ClockContract.AlarmsColumns.HOUR + cs +
- ClockContract.AlarmsColumns.MINUTES + cs +
- ClockContract.AlarmsColumns.DAYS_OF_WEEK + cs +
- ClockContract.AlarmsColumns.ENABLED + cs +
- ClockContract.AlarmsColumns.VIBRATE + cs +
- ClockContract.AlarmsColumns.LABEL + cs +
- ClockContract.AlarmsColumns.RINGTONE + cs +
- ClockContract.AlarmsColumns.DELETE_AFTER_USE + ") VALUES ";
- db.execSQL(insertMe + DEFAULT_ALARM_1);
- db.execSQL(insertMe + DEFAULT_ALARM_2);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
- LogUtils.v("Upgrading alarms database from version %d to %d", oldVersion, currentVersion);
-
- if (oldVersion <= VERSION_7) {
- // This was not used in VERSION_7 or prior, so we can just drop it.
- db.execSQL("DROP TABLE IF EXISTS " + SELECTED_CITIES_TABLE_NAME + ";");
- }
-
- if (oldVersion <= VERSION_6) {
- // This was not used in VERSION_6 or prior, so we can just drop it.
- db.execSQL("DROP TABLE IF EXISTS " + INSTANCES_TABLE_NAME + ";");
-
- // Create new alarms table and copy over the data
- createAlarmsTable(db);
- createInstanceTable(db);
-
- LogUtils.i("Copying old alarms to new table");
- final String[] OLD_TABLE_COLUMNS = {
- "_id",
- "hour",
- "minutes",
- "daysofweek",
- "enabled",
- "vibrate",
- "message",
- "alert",
- };
- try (Cursor cursor = db.query(OLD_ALARMS_TABLE_NAME, OLD_TABLE_COLUMNS,
- null, null, null, null, null)) {
- final Calendar currentTime = Calendar.getInstance();
- while (cursor != null && cursor.moveToNext()) {
- final Alarm alarm = new Alarm();
- alarm.id = cursor.getLong(0);
- alarm.hour = cursor.getInt(1);
- alarm.minutes = cursor.getInt(2);
- alarm.daysOfWeek = Weekdays.fromBits(cursor.getInt(3));
- alarm.enabled = cursor.getInt(4) == 1;
- alarm.vibrate = cursor.getInt(5) == 1;
- alarm.label = cursor.getString(6);
-
- final String alertString = cursor.getString(7);
- if ("silent".equals(alertString)) {
- alarm.alert = Alarm.NO_RINGTONE_URI;
- } else {
- alarm.alert =
- TextUtils.isEmpty(alertString) ? null : Uri.parse(alertString);
- }
-
- // Save new version of alarm and create alarm instance for it
- db.insert(ALARMS_TABLE_NAME, null, Alarm.createContentValues(alarm));
- if (alarm.enabled) {
- AlarmInstance newInstance = alarm.createInstanceAfter(currentTime);
- db.insert(INSTANCES_TABLE_NAME, null,
- AlarmInstance.createContentValues(newInstance));
- }
- }
- }
-
- LogUtils.i("Dropping old alarm table");
- db.execSQL("DROP TABLE IF EXISTS " + OLD_ALARMS_TABLE_NAME + ";");
- }
- }
-
- long fixAlarmInsert(ContentValues values) {
- // Why are we doing this? Is this not a programming bug if we try to
- // insert an already used id?
- final SQLiteDatabase db = getWritableDatabase();
- db.beginTransaction();
- long rowId = -1;
- try {
- // Check if we are trying to re-use an existing id.
- final Object value = values.get(ClockContract.AlarmsColumns._ID);
- if (value != null) {
- long id = (Long) value;
- if (id > -1) {
- final String[] columns = {ClockContract.AlarmsColumns._ID};
- final String selection = ClockContract.AlarmsColumns._ID + " = ?";
- final String[] selectionArgs = {String.valueOf(id)};
- try (Cursor cursor = db.query(ALARMS_TABLE_NAME, columns, selection,
- selectionArgs, null, null, null)) {
- if (cursor.moveToFirst()) {
- // Record exists. Remove the id so sqlite can generate a new one.
- values.putNull(ClockContract.AlarmsColumns._ID);
- }
- }
- }
- }
-
- rowId = db.insert(ALARMS_TABLE_NAME, ClockContract.AlarmsColumns.RINGTONE, values);
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
- if (rowId < 0) {
- throw new SQLException("Failed to insert row");
- }
- LogUtils.v("Added alarm rowId = " + rowId);
-
- return rowId;
- }
-}
diff --git a/src/com/android/deskclock/provider/ClockDatabaseHelper.kt b/src/com/android/deskclock/provider/ClockDatabaseHelper.kt
new file mode 100644
index 0000000..d37e1a0
--- /dev/null
+++ b/src/com/android/deskclock/provider/ClockDatabaseHelper.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.provider
+
+import android.content.ContentValues
+import android.content.Context
+import android.database.Cursor
+import android.database.SQLException
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+import android.net.Uri
+import android.provider.BaseColumns
+import android.text.TextUtils
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.data.Weekdays
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+
+import java.util.Calendar
+
+/**
+ * Helper class for opening the database from multiple providers. Also provides
+ * some common functionality.
+ */
+class ClockDatabaseHelper(context: Context)
+ : SQLiteOpenHelper(context, DATABASE_NAME, null, VERSION_8) {
+
+ override fun onCreate(db: SQLiteDatabase) {
+ createAlarmsTable(db)
+ createInstanceTable(db)
+
+ // insert default alarms
+ LogUtils.i("Inserting default alarms")
+ val cs: String = ", " // comma and space
+ val insertMe: String = "INSERT INTO " + ALARMS_TABLE_NAME + " (" +
+ AlarmsColumns.HOUR + cs +
+ AlarmsColumns.MINUTES + cs +
+ AlarmsColumns.DAYS_OF_WEEK + cs +
+ AlarmsColumns.ENABLED + cs +
+ AlarmSettingColumns.VIBRATE + cs +
+ AlarmSettingColumns.LABEL + cs +
+ AlarmSettingColumns.RINGTONE + cs +
+ AlarmsColumns.DELETE_AFTER_USE + ") VALUES "
+ db.execSQL(insertMe + DEFAULT_ALARM_1)
+ db.execSQL(insertMe + DEFAULT_ALARM_2)
+ }
+
+ override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, currentVersion: Int) {
+ LogUtils.v("Upgrading alarms database from version %d to %d",
+ oldVersion, currentVersion)
+
+ if (oldVersion <= VERSION_7) {
+ // This was not used in VERSION_7 or prior, so we can just drop it.
+ db.execSQL("DROP TABLE IF EXISTS " + SELECTED_CITIES_TABLE_NAME + ";")
+ }
+
+ if (oldVersion <= VERSION_6) {
+ // This was not used in VERSION_6 or prior, so we can just drop it.
+ db.execSQL("DROP TABLE IF EXISTS " + INSTANCES_TABLE_NAME + ";")
+
+ // Create new alarms table and copy over the data
+ createAlarmsTable(db)
+ createInstanceTable(db)
+
+ LogUtils.i("Copying old alarms to new table")
+ val OLD_TABLE_COLUMNS: Array<String> = arrayOf(
+ "_id",
+ "hour",
+ "minutes",
+ "daysofweek",
+ "enabled",
+ "vibrate",
+ "message",
+ "alert"
+ )
+ val cursor: Cursor? =
+ db.query(OLD_ALARMS_TABLE_NAME, OLD_TABLE_COLUMNS, null, null, null, null, null)
+ val currentTime: Calendar = Calendar.getInstance()
+ while (cursor != null && cursor.moveToNext()) {
+ val alarm = Alarm()
+ alarm.id = cursor.getLong(0)
+ alarm.hour = cursor.getInt(1)
+ alarm.minutes = cursor.getInt(2)
+ alarm.daysOfWeek = Weekdays.fromBits(cursor.getInt(3))
+ alarm.enabled = cursor.getInt(4) == 1
+ alarm.vibrate = cursor.getInt(5) == 1
+ alarm.label = cursor.getString(6)
+
+ val alertString: String = cursor.getString(7)
+ if ("silent" == alertString) {
+ alarm.alert = AlarmSettingColumns.NO_RINGTONE_URI
+ } else {
+ alarm.alert = if (TextUtils.isEmpty(alertString)) {
+ null
+ } else {
+ Uri.parse(alertString)
+ }
+ }
+
+ // Save new version of alarm and create alarm instance for it
+ db.insert(ALARMS_TABLE_NAME, null, Alarm.createContentValues(alarm))
+ if (alarm.enabled) {
+ val newInstance: AlarmInstance = alarm.createInstanceAfter(currentTime)
+ db.insert(INSTANCES_TABLE_NAME, null,
+ AlarmInstance.createContentValues(newInstance))
+ }
+ }
+
+ LogUtils.i("Dropping old alarm table")
+ db.execSQL("DROP TABLE IF EXISTS " + OLD_ALARMS_TABLE_NAME + ";")
+ }
+ }
+
+ fun fixAlarmInsert(values: ContentValues): Long {
+ // Why are we doing this? Is this not a programming bug if we try to
+ // insert an already used id?
+ val db: SQLiteDatabase = getWritableDatabase()
+ db.beginTransaction()
+ val rowId: Long
+ try {
+ // Check if we are trying to re-use an existing id.
+ val value = values.get(BaseColumns._ID)
+ if (value != null) {
+ val id: Long = value as Long
+ if (id > -1) {
+ val columns: Array<String> = arrayOf(BaseColumns._ID)
+ val selection: String = BaseColumns._ID + " = ?"
+ val selectionArgs: Array<String> = arrayOf(id.toString())
+ val cursor: Cursor =
+ db.query(ALARMS_TABLE_NAME, columns,
+ selection, selectionArgs, null, null, null)
+ if (cursor.moveToFirst()) {
+ // Record exists. Remove the id so sqlite can generate a new one.
+ values.putNull(BaseColumns._ID)
+ }
+ }
+ }
+
+ rowId = db.insert(ALARMS_TABLE_NAME, AlarmSettingColumns.RINGTONE, values)
+ db.setTransactionSuccessful()
+ } finally {
+ db.endTransaction()
+ }
+
+ if (rowId < 0) {
+ throw SQLException("Failed to insert row")
+ }
+ LogUtils.v("Added alarm rowId = " + rowId)
+
+ return rowId
+ }
+
+ companion object {
+ /**
+ * Original Clock Database.
+ **/
+ private const val VERSION_5: Int = 5
+
+ /**
+ * Added alarm_instances table
+ * Added selected_cities table
+ * Added DELETE_AFTER_USE column to alarms table
+ */
+ private const val VERSION_6: Int = 6
+
+ /**
+ * Added alarm settings to instance table.
+ */
+ private const val VERSION_7: Int = 7
+
+ /**
+ * Removed selected_cities table.
+ */
+ private const val VERSION_8: Int = 8
+
+ // This creates a default alarm at 8:30 for every Mon,Tue,Wed,Thu,Fri
+ private const val DEFAULT_ALARM_1: String = "(8, 30, 31, 0, 1, '', NULL, 0);"
+
+ // This creates a default alarm at 9:30 for every Sat,Sun
+ private const val DEFAULT_ALARM_2: String = "(9, 00, 96, 0, 1, '', NULL, 0);"
+
+ // Database and table names
+ const val DATABASE_NAME: String = "alarms.db"
+ const val OLD_ALARMS_TABLE_NAME: String = "alarms"
+ const val ALARMS_TABLE_NAME: String = "alarm_templates"
+ const val INSTANCES_TABLE_NAME: String = "alarm_instances"
+ private const val SELECTED_CITIES_TABLE_NAME: String = "selected_cities"
+
+ private fun createAlarmsTable(db: SQLiteDatabase) {
+ db.execSQL("CREATE TABLE " + ALARMS_TABLE_NAME + " (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY," +
+ AlarmsColumns.HOUR + " INTEGER NOT NULL, " +
+ AlarmsColumns.MINUTES + " INTEGER NOT NULL, " +
+ AlarmsColumns.DAYS_OF_WEEK + " INTEGER NOT NULL, " +
+ AlarmsColumns.ENABLED + " INTEGER NOT NULL, " +
+ AlarmSettingColumns.VIBRATE + " INTEGER NOT NULL, " +
+ AlarmSettingColumns.LABEL + " TEXT NOT NULL, " +
+ AlarmSettingColumns.RINGTONE + " TEXT, " +
+ AlarmsColumns.DELETE_AFTER_USE + " INTEGER NOT NULL DEFAULT 0);")
+ LogUtils.i("Alarms Table created")
+ }
+
+ private fun createInstanceTable(db: SQLiteDatabase) {
+ db.execSQL("CREATE TABLE " + INSTANCES_TABLE_NAME + " (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY," +
+ InstancesColumns.YEAR + " INTEGER NOT NULL, " +
+ InstancesColumns.MONTH + " INTEGER NOT NULL, " +
+ InstancesColumns.DAY + " INTEGER NOT NULL, " +
+ InstancesColumns.HOUR + " INTEGER NOT NULL, " +
+ InstancesColumns.MINUTES + " INTEGER NOT NULL, " +
+ AlarmSettingColumns.VIBRATE + " INTEGER NOT NULL, " +
+ AlarmSettingColumns.LABEL + " TEXT NOT NULL, " +
+ AlarmSettingColumns.RINGTONE + " TEXT, " +
+ InstancesColumns.ALARM_STATE + " INTEGER NOT NULL, " +
+ InstancesColumns.ALARM_ID + " INTEGER REFERENCES " +
+ ALARMS_TABLE_NAME + "(" + BaseColumns._ID + ") " +
+ "ON UPDATE CASCADE ON DELETE CASCADE" +
+ ");")
+ LogUtils.i("Instance table created")
+ }
+ }
+}
diff --git a/src/com/android/deskclock/provider/ClockProvider.java b/src/com/android/deskclock/provider/ClockProvider.java
deleted file mode 100644
index 83480f3..0000000
--- a/src/com/android/deskclock/provider/ClockProvider.java
+++ /dev/null
@@ -1,306 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.provider;
-
-import android.annotation.TargetApi;
-import android.content.ContentProvider;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.UriMatcher;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteQueryBuilder;
-import android.net.Uri;
-import android.os.Build;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-import android.util.ArrayMap;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.Utils;
-
-import java.util.Map;
-
-import static com.android.deskclock.provider.ClockContract.AlarmsColumns;
-import static com.android.deskclock.provider.ClockContract.InstancesColumns;
-import static com.android.deskclock.provider.ClockDatabaseHelper.ALARMS_TABLE_NAME;
-import static com.android.deskclock.provider.ClockDatabaseHelper.INSTANCES_TABLE_NAME;
-
-public class ClockProvider extends ContentProvider {
-
- private ClockDatabaseHelper mOpenHelper;
-
- private static final int ALARMS = 1;
- private static final int ALARMS_ID = 2;
- private static final int INSTANCES = 3;
- private static final int INSTANCES_ID = 4;
- private static final int ALARMS_WITH_INSTANCES = 5;
-
- /**
- * Projection map used by query for snoozed alarms.
- */
- private static final Map<String, String> sAlarmsWithInstancesProjection = new ArrayMap<>();
- static {
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns._ID,
- ALARMS_TABLE_NAME + "." + AlarmsColumns._ID);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.VIBRATE,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.VIBRATE);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.LABEL,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.LABEL);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.RINGTONE,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.RINGTONE);
- sAlarmsWithInstancesProjection.put(ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE,
- ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "."
- + InstancesColumns.ALARM_STATE,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns._ID,
- INSTANCES_TABLE_NAME + "." + InstancesColumns._ID);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.LABEL,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.LABEL);
- sAlarmsWithInstancesProjection.put(INSTANCES_TABLE_NAME + "." + InstancesColumns.VIBRATE,
- INSTANCES_TABLE_NAME + "." + InstancesColumns.VIBRATE);
- }
-
- private static final String ALARM_JOIN_INSTANCE_TABLE_STATEMENT =
- ALARMS_TABLE_NAME + " LEFT JOIN " + INSTANCES_TABLE_NAME + " ON (" +
- ALARMS_TABLE_NAME + "." + AlarmsColumns._ID + " = " + InstancesColumns.ALARM_ID + ")";
-
- private static final String ALARM_JOIN_INSTANCE_WHERE_STATEMENT =
- INSTANCES_TABLE_NAME + "." + InstancesColumns._ID + " IS NULL OR " +
- INSTANCES_TABLE_NAME + "." + InstancesColumns._ID + " = (" +
- "SELECT " + InstancesColumns._ID +
- " FROM " + INSTANCES_TABLE_NAME +
- " WHERE " + InstancesColumns.ALARM_ID +
- " = " + ALARMS_TABLE_NAME + "." + AlarmsColumns._ID +
- " ORDER BY " + InstancesColumns.ALARM_STATE + ", " +
- InstancesColumns.YEAR + ", " + InstancesColumns.MONTH + ", " +
- InstancesColumns.DAY + " LIMIT 1)";
-
- private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- static {
- sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms", ALARMS);
- sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms/#", ALARMS_ID);
- sURIMatcher.addURI(ClockContract.AUTHORITY, "instances", INSTANCES);
- sURIMatcher.addURI(ClockContract.AUTHORITY, "instances/#", INSTANCES_ID);
- sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms_with_instances", ALARMS_WITH_INSTANCES);
- }
-
- public ClockProvider() {
- }
-
- @Override
- @TargetApi(Build.VERSION_CODES.N)
- public boolean onCreate() {
- final Context context = getContext();
- final Context storageContext;
- if (Utils.isNOrLater()) {
- // All N devices have split storage areas, but we may need to
- // migrate existing database into the new device encrypted
- // storage area, which is where our data lives from now on.
- storageContext = context.createDeviceProtectedStorageContext();
- if (!storageContext.moveDatabaseFrom(context, ClockDatabaseHelper.DATABASE_NAME)) {
- LogUtils.wtf("Failed to migrate database: %s", ClockDatabaseHelper.DATABASE_NAME);
- }
- } else {
- storageContext = context;
- }
-
- mOpenHelper = new ClockDatabaseHelper(storageContext);
- return true;
- }
-
- @Override
- public Cursor query(@NonNull Uri uri, String[] projectionIn, String selection,
- String[] selectionArgs, String sort) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- SQLiteDatabase db = mOpenHelper.getReadableDatabase();
-
- // Generate the body of the query
- int match = sURIMatcher.match(uri);
- switch (match) {
- case ALARMS:
- qb.setTables(ALARMS_TABLE_NAME);
- break;
- case ALARMS_ID:
- qb.setTables(ALARMS_TABLE_NAME);
- qb.appendWhere(AlarmsColumns._ID + "=");
- qb.appendWhere(uri.getLastPathSegment());
- break;
- case INSTANCES:
- qb.setTables(INSTANCES_TABLE_NAME);
- break;
- case INSTANCES_ID:
- qb.setTables(INSTANCES_TABLE_NAME);
- qb.appendWhere(InstancesColumns._ID + "=");
- qb.appendWhere(uri.getLastPathSegment());
- break;
- case ALARMS_WITH_INSTANCES:
- qb.setTables(ALARM_JOIN_INSTANCE_TABLE_STATEMENT);
- qb.appendWhere(ALARM_JOIN_INSTANCE_WHERE_STATEMENT);
- qb.setProjectionMap(sAlarmsWithInstancesProjection);
- break;
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
-
- Cursor ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);
-
- if (ret == null) {
- LogUtils.e("Alarms.query: failed");
- } else {
- ret.setNotificationUri(getContext().getContentResolver(), uri);
- }
-
- return ret;
- }
-
- @Override
- public String getType(@NonNull Uri uri) {
- int match = sURIMatcher.match(uri);
- switch (match) {
- case ALARMS:
- return "vnd.android.cursor.dir/alarms";
- case ALARMS_ID:
- return "vnd.android.cursor.item/alarms";
- case INSTANCES:
- return "vnd.android.cursor.dir/instances";
- case INSTANCES_ID:
- return "vnd.android.cursor.item/instances";
- default:
- throw new IllegalArgumentException("Unknown URI");
- }
- }
-
- @Override
- public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) {
- int count;
- String alarmId;
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- switch (sURIMatcher.match(uri)) {
- case ALARMS_ID:
- alarmId = uri.getLastPathSegment();
- count = db.update(ALARMS_TABLE_NAME, values,
- AlarmsColumns._ID + "=" + alarmId,
- null);
- break;
- case INSTANCES_ID:
- alarmId = uri.getLastPathSegment();
- count = db.update(INSTANCES_TABLE_NAME, values,
- InstancesColumns._ID + "=" + alarmId,
- null);
- break;
- default: {
- throw new UnsupportedOperationException("Cannot update URI: " + uri);
- }
- }
- LogUtils.v("*** notifyChange() id: " + alarmId + " url " + uri);
- notifyChange(getContext().getContentResolver(), uri);
- return count;
- }
-
- @Override
- public Uri insert(@NonNull Uri uri, ContentValues initialValues) {
- long rowId;
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- switch (sURIMatcher.match(uri)) {
- case ALARMS:
- rowId = mOpenHelper.fixAlarmInsert(initialValues);
- break;
- case INSTANCES:
- rowId = db.insert(INSTANCES_TABLE_NAME, null, initialValues);
- break;
- default:
- throw new IllegalArgumentException("Cannot insert from URI: " + uri);
- }
-
- Uri uriResult = ContentUris.withAppendedId(uri, rowId);
- notifyChange(getContext().getContentResolver(), uriResult);
- return uriResult;
- }
-
- @Override
- public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
- int count;
- String primaryKey;
- SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- switch (sURIMatcher.match(uri)) {
- case ALARMS:
- count = db.delete(ALARMS_TABLE_NAME, where, whereArgs);
- break;
- case ALARMS_ID:
- primaryKey = uri.getLastPathSegment();
- if (TextUtils.isEmpty(where)) {
- where = AlarmsColumns._ID + "=" + primaryKey;
- } else {
- where = AlarmsColumns._ID + "=" + primaryKey + " AND (" + where + ")";
- }
- count = db.delete(ALARMS_TABLE_NAME, where, whereArgs);
- break;
- case INSTANCES:
- count = db.delete(INSTANCES_TABLE_NAME, where, whereArgs);
- break;
- case INSTANCES_ID:
- primaryKey = uri.getLastPathSegment();
- if (TextUtils.isEmpty(where)) {
- where = InstancesColumns._ID + "=" + primaryKey;
- } else {
- where = InstancesColumns._ID + "=" + primaryKey + " AND (" + where + ")";
- }
- count = db.delete(INSTANCES_TABLE_NAME, where, whereArgs);
- break;
- default:
- throw new IllegalArgumentException("Cannot delete from URI: " + uri);
- }
-
- notifyChange(getContext().getContentResolver(), uri);
- return count;
- }
-
- /**
- * Notify affected URIs of changes.
- */
- private void notifyChange(ContentResolver resolver, Uri uri) {
- resolver.notifyChange(uri, null);
-
- final int match = sURIMatcher.match(uri);
- // Also notify the joined table of changes to instances or alarms.
- if (match == ALARMS || match == INSTANCES || match == ALARMS_ID || match == INSTANCES_ID) {
- resolver.notifyChange(AlarmsColumns.ALARMS_WITH_INSTANCES_URI, null);
- }
- }
-}
diff --git a/src/com/android/deskclock/provider/ClockProvider.kt b/src/com/android/deskclock/provider/ClockProvider.kt
new file mode 100644
index 0000000..effd531
--- /dev/null
+++ b/src/com/android/deskclock/provider/ClockProvider.kt
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.provider
+
+import android.annotation.TargetApi
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.UriMatcher
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteQueryBuilder
+import android.net.Uri
+import android.os.Build
+import android.provider.BaseColumns
+import android.text.TextUtils
+import android.util.ArrayMap
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
+import com.android.deskclock.provider.ClockContract.AlarmsColumns
+import com.android.deskclock.provider.ClockContract.InstancesColumns
+import com.android.deskclock.provider.ClockDatabaseHelper.Companion.ALARMS_TABLE_NAME
+import com.android.deskclock.provider.ClockDatabaseHelper.Companion.INSTANCES_TABLE_NAME
+
+class ClockProvider : ContentProvider() {
+
+ private lateinit var mOpenHelper: ClockDatabaseHelper
+
+ companion object {
+ private const val ALARMS = 1
+ private const val ALARMS_ID = 2
+ private const val INSTANCES = 3
+ private const val INSTANCES_ID = 4
+ private const val ALARMS_WITH_INSTANCES = 5
+
+ private val ALARM_JOIN_INSTANCE_TABLE_STATEMENT =
+ ALARMS_TABLE_NAME + " LEFT JOIN " +
+ INSTANCES_TABLE_NAME + " ON (" +
+ ALARMS_TABLE_NAME + "." +
+ BaseColumns._ID + " = " + InstancesColumns.ALARM_ID + ")"
+
+ private val ALARM_JOIN_INSTANCE_WHERE_STATEMENT = INSTANCES_TABLE_NAME +
+ "." + BaseColumns._ID + " IS NULL OR " +
+ INSTANCES_TABLE_NAME + "." + BaseColumns._ID + " = (" +
+ "SELECT " + BaseColumns._ID +
+ " FROM " + INSTANCES_TABLE_NAME +
+ " WHERE " + InstancesColumns.ALARM_ID +
+ " = " + ALARMS_TABLE_NAME + "." + BaseColumns._ID +
+ " ORDER BY " + InstancesColumns.ALARM_STATE + ", " +
+ InstancesColumns.YEAR + ", " + InstancesColumns.MONTH + ", " +
+ InstancesColumns.DAY + " LIMIT 1)"
+
+ /**
+ * Projection map used by query for snoozed alarms.
+ */
+ private val sAlarmsWithInstancesProjection: MutableMap<String, String> = ArrayMap()
+
+ private val sURIMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH)
+
+ init {
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + BaseColumns._ID] =
+ ALARMS_TABLE_NAME + "." + BaseColumns._ID
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR] =
+ ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES] =
+ ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK] =
+ ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED] =
+ ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE] =
+ ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL] =
+ ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE] =
+ ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE
+ sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." +
+ AlarmsColumns.DELETE_AFTER_USE] =
+ ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." +
+ InstancesColumns.ALARM_STATE] =
+ INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + BaseColumns._ID] =
+ INSTANCES_TABLE_NAME + "." + BaseColumns._ID
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR] =
+ INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH] =
+ INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY] =
+ INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR] =
+ INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES] =
+ INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL] =
+ INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL
+ sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." +
+ AlarmSettingColumns.VIBRATE] =
+ INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
+
+ sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms", ALARMS)
+ sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms/#", ALARMS_ID)
+ sURIMatcher.addURI(ClockContract.AUTHORITY, "instances", INSTANCES)
+ sURIMatcher.addURI(ClockContract.AUTHORITY, "instances/#", INSTANCES_ID)
+ sURIMatcher.addURI(ClockContract.AUTHORITY,
+ "alarms_with_instances", ALARMS_WITH_INSTANCES)
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ override fun onCreate(): Boolean {
+ val context: Context = getContext()!!
+ val storageContext: Context
+ if (Utils.isNOrLater) {
+ // All N devices have split storage areas, but we may need to
+ // migrate existing database into the new device encrypted
+ // storage area, which is where our data lives from now on.
+ storageContext = context.createDeviceProtectedStorageContext()
+ if (!storageContext.moveDatabaseFrom(context, ClockDatabaseHelper.DATABASE_NAME)) {
+ LogUtils.wtf("Failed to migrate database: %s",
+ ClockDatabaseHelper.DATABASE_NAME)
+ }
+ } else {
+ storageContext = context
+ }
+
+ mOpenHelper = ClockDatabaseHelper(storageContext)
+ return true
+ }
+
+ override fun query(
+ uri: Uri,
+ projectionIn: Array<String?>?,
+ selection: String?,
+ selectionArgs: Array<String?>?,
+ sort: String?
+ ): Cursor? {
+ val qb = SQLiteQueryBuilder()
+ val db: SQLiteDatabase = mOpenHelper.getReadableDatabase()
+
+ // Generate the body of the query
+ when (sURIMatcher.match(uri)) {
+ ALARMS -> qb.setTables(ALARMS_TABLE_NAME)
+ ALARMS_ID -> {
+ qb.setTables(ALARMS_TABLE_NAME)
+ qb.appendWhere(BaseColumns._ID.toString() + "=")
+ qb.appendWhere(uri.getLastPathSegment()!!)
+ }
+ INSTANCES -> qb.setTables(INSTANCES_TABLE_NAME)
+ INSTANCES_ID -> {
+ qb.setTables(INSTANCES_TABLE_NAME)
+ qb.appendWhere(BaseColumns._ID.toString() + "=")
+ qb.appendWhere(uri.getLastPathSegment()!!)
+ }
+ ALARMS_WITH_INSTANCES -> {
+ qb.setTables(ALARM_JOIN_INSTANCE_TABLE_STATEMENT)
+ qb.appendWhere(ALARM_JOIN_INSTANCE_WHERE_STATEMENT)
+ qb.setProjectionMap(sAlarmsWithInstancesProjection)
+ }
+ else -> throw IllegalArgumentException("Unknown URI $uri")
+ }
+
+ val ret: Cursor? = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort)
+ if (ret == null) {
+ LogUtils.e("Alarms.query: failed")
+ } else {
+ ret.setNotificationUri(getContext()!!.getContentResolver(), uri)
+ }
+
+ return ret
+ }
+
+ override fun getType(uri: Uri): String {
+ return when (sURIMatcher.match(uri)) {
+ ALARMS -> "vnd.android.cursor.dir/alarms"
+ ALARMS_ID -> "vnd.android.cursor.item/alarms"
+ INSTANCES -> "vnd.android.cursor.dir/instances"
+ INSTANCES_ID -> "vnd.android.cursor.item/instances"
+ else -> throw IllegalArgumentException("Unknown URI")
+ }
+ }
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ where: String?,
+ whereArgs: Array<String?>?
+ ): Int {
+ val count: Int
+ val alarmId: String?
+ val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
+ when (sURIMatcher.match(uri)) {
+ ALARMS_ID -> {
+ alarmId = uri.getLastPathSegment()
+ count = db.update(ALARMS_TABLE_NAME, values,
+ BaseColumns._ID.toString() + "=" + alarmId,
+ null)
+ }
+ INSTANCES_ID -> {
+ alarmId = uri.getLastPathSegment()
+ count = db.update(INSTANCES_TABLE_NAME, values,
+ BaseColumns._ID.toString() + "=" + alarmId,
+ null)
+ }
+ else -> {
+ throw UnsupportedOperationException("Cannot update URI: $uri")
+ }
+ }
+ LogUtils.v("*** notifyChange() id: $alarmId url $uri")
+ notifyChange(getContext()!!.getContentResolver(), uri)
+ return count
+ }
+
+ override fun insert(uri: Uri, initialValues: ContentValues?): Uri? {
+ val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
+ val rowId: Long = when (sURIMatcher.match(uri)) {
+ ALARMS -> mOpenHelper.fixAlarmInsert(initialValues!!)
+ INSTANCES -> db.insert(INSTANCES_TABLE_NAME, null, initialValues)
+ else -> throw IllegalArgumentException("Cannot insert from URI: $uri")
+ }
+
+ val uriResult: Uri = ContentUris.withAppendedId(uri, rowId)
+ notifyChange(getContext()!!.getContentResolver(), uriResult)
+ return uriResult
+ }
+
+ override fun delete(uri: Uri, where: String?, whereArgs: Array<String>?): Int {
+ var whereString = where
+ val count: Int
+ val primaryKey: String?
+ val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
+ when (sURIMatcher.match(uri)) {
+ ALARMS -> count =
+ db.delete(ALARMS_TABLE_NAME, whereString, whereArgs)
+ ALARMS_ID -> {
+ primaryKey = uri.getLastPathSegment()
+ whereString = if (TextUtils.isEmpty(whereString)) {
+ BaseColumns._ID.toString() + "=" + primaryKey
+ } else {
+ BaseColumns._ID.toString() + "=" + primaryKey + " AND (" + whereString + ")"
+ }
+ count = db.delete(ALARMS_TABLE_NAME, whereString, whereArgs)
+ }
+ INSTANCES -> count =
+ db.delete(INSTANCES_TABLE_NAME, whereString, whereArgs)
+ INSTANCES_ID -> {
+ primaryKey = uri.getLastPathSegment()
+ whereString = if (TextUtils.isEmpty(whereString)) {
+ BaseColumns._ID.toString() + "=" + primaryKey
+ } else {
+ BaseColumns._ID.toString() + "=" + primaryKey + " AND (" + whereString + ")"
+ }
+ count = db.delete(INSTANCES_TABLE_NAME, whereString, whereArgs)
+ }
+ else -> throw IllegalArgumentException("Cannot delete from URI: $uri")
+ }
+
+ notifyChange(getContext()!!.getContentResolver(), uri)
+ return count
+ }
+
+ /**
+ * Notify affected URIs of changes.
+ */
+ private fun notifyChange(resolver: ContentResolver, uri: Uri) {
+ resolver.notifyChange(uri, null)
+
+ val match: Int = sURIMatcher.match(uri)
+ // Also notify the joined table of changes to instances or alarms.
+ if (match == ALARMS || match == INSTANCES || match == ALARMS_ID || match == INSTANCES_ID) {
+ resolver.notifyChange(AlarmsColumns.ALARMS_WITH_INSTANCES_URI, null)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java
deleted file mode 100644
index ff37551..0000000
--- a/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.net.Uri;
-
-import com.android.deskclock.ItemAdapter;
-
-import static androidx.recyclerview.widget.RecyclerView.NO_ID;
-
-final class AddCustomRingtoneHolder extends ItemAdapter.ItemHolder<Uri> {
-
- AddCustomRingtoneHolder() {
- super(null, NO_ID);
- }
-
- @Override
- public int getItemViewType() {
- return AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.kt b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.kt
new file mode 100644
index 0000000..d80d41c
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/AddCustomRingtoneHolder.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.net.Uri
+import androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+
+internal class AddCustomRingtoneHolder : ItemHolder<Uri?>(null, NO_ID) {
+ override fun getItemViewType(): Int {
+ return AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.java b/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.java
deleted file mode 100644
index 5cc6fad..0000000
--- a/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.deskclock.ItemAdapter.ItemViewHolder;
-import com.android.deskclock.R;
-
-import static android.view.View.GONE;
-
-final class AddCustomRingtoneViewHolder extends ItemViewHolder<AddCustomRingtoneHolder>
- implements View.OnClickListener {
-
- static final int VIEW_TYPE_ADD_NEW = Integer.MIN_VALUE;
- static final int CLICK_ADD_NEW = VIEW_TYPE_ADD_NEW;
-
- private AddCustomRingtoneViewHolder(View itemView) {
- super(itemView);
- itemView.setOnClickListener(this);
-
- final View selectedView = itemView.findViewById(R.id.sound_image_selected);
- selectedView.setVisibility(GONE);
-
- final TextView nameView = (TextView) itemView.findViewById(R.id.ringtone_name);
- nameView.setText(itemView.getContext().getString(R.string.add_new_sound));
- nameView.setAlpha(0.63f);
-
- final ImageView imageView = (ImageView) itemView.findViewById(R.id.ringtone_image);
- imageView.setImageResource(R.drawable.ic_add_white_24dp);
- imageView.setAlpha(0.63f);
- }
-
- @Override
- public void onClick(View view) {
- notifyItemClicked(AddCustomRingtoneViewHolder.CLICK_ADD_NEW);
- }
-
- public static class Factory implements ItemViewHolder.Factory {
-
- private final LayoutInflater mInflater;
-
- Factory(LayoutInflater inflater) {
- mInflater = inflater;
- }
-
- @Override
- public ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
- final View itemView = mInflater.inflate(R.layout.ringtone_item_sound, parent, false);
- return new AddCustomRingtoneViewHolder(itemView);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.kt b/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.kt
new file mode 100644
index 0000000..b70df2f
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/AddCustomRingtoneViewHolder.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.R
+
+internal class AddCustomRingtoneViewHolder private constructor(itemView: View)
+ : ItemViewHolder<AddCustomRingtoneHolder>(itemView), View.OnClickListener {
+
+ init {
+ itemView.setOnClickListener(this)
+ val selectedView = itemView.findViewById<View>(R.id.sound_image_selected)
+ selectedView.visibility = View.GONE
+ val nameView = itemView.findViewById<View>(R.id.ringtone_name) as TextView
+ nameView.text = itemView.context.getString(R.string.add_new_sound)
+ nameView.alpha = 0.63f
+ val imageView = itemView.findViewById<View>(R.id.ringtone_image) as ImageView
+ imageView.setImageResource(R.drawable.ic_add_white_24dp)
+ imageView.alpha = 0.63f
+ }
+
+ override fun onClick(view: View) {
+ notifyItemClicked(CLICK_ADD_NEW)
+ }
+
+ class Factory internal constructor(private val mInflater: LayoutInflater)
+ : ItemViewHolder.Factory {
+ override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+ val itemView =
+ mInflater.inflate(R.layout.ringtone_item_sound, parent, false)
+ return AddCustomRingtoneViewHolder(itemView)
+ }
+ }
+
+ companion object {
+ const val VIEW_TYPE_ADD_NEW = Int.MIN_VALUE
+ const val CLICK_ADD_NEW = VIEW_TYPE_ADD_NEW
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java
deleted file mode 100644
index 619d45f..0000000
--- a/src/com/android/deskclock/ringtone/CustomRingtoneHolder.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import com.android.deskclock.data.CustomRingtone;
-
-class CustomRingtoneHolder extends RingtoneHolder {
-
- CustomRingtoneHolder(CustomRingtone ringtone) {
- super(ringtone.getUri(), ringtone.getTitle(), ringtone.hasPermissions());
- }
-
- @Override
- public int getItemViewType() {
- return RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/CustomRingtoneHolder.kt b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.kt
new file mode 100644
index 0000000..bd880ad
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/CustomRingtoneHolder.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import com.android.deskclock.data.CustomRingtone
+
+internal class CustomRingtoneHolder(ringtone: CustomRingtone)
+ : RingtoneHolder(ringtone.uri, ringtone.title, ringtone.hasPermissions()) {
+
+ override fun getItemViewType(): Int {
+ return RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/HeaderHolder.java b/src/com/android/deskclock/ringtone/HeaderHolder.java
deleted file mode 100644
index 4c52efe..0000000
--- a/src/com/android/deskclock/ringtone/HeaderHolder.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.net.Uri;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.ItemAdapter;
-
-import static androidx.recyclerview.widget.RecyclerView.NO_ID;
-
-final class HeaderHolder extends ItemAdapter.ItemHolder<Uri> {
-
- private final @StringRes int mTextResId;
-
- HeaderHolder(@StringRes int textResId) {
- super(null, NO_ID);
- mTextResId = textResId;
- }
-
- @StringRes int getTextResId() {
- return mTextResId;
- }
-
- @Override
- public int getItemViewType() {
- return HeaderViewHolder.VIEW_TYPE_ITEM_HEADER;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/HeaderHolder.kt b/src/com/android/deskclock/ringtone/HeaderHolder.kt
new file mode 100644
index 0000000..98836b0
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/HeaderHolder.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.net.Uri
+import androidx.annotation.StringRes
+import androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+
+internal class HeaderHolder(
+ @field:StringRes @get:StringRes
+ @param:StringRes val textResId: Int
+) : ItemHolder<Uri?>(null, NO_ID) {
+
+ override fun getItemViewType(): Int {
+ return HeaderViewHolder.VIEW_TYPE_ITEM_HEADER
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/HeaderViewHolder.java b/src/com/android/deskclock/ringtone/HeaderViewHolder.java
deleted file mode 100644
index eb88bb0..0000000
--- a/src/com/android/deskclock/ringtone/HeaderViewHolder.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.R;
-
-final class HeaderViewHolder extends ItemAdapter.ItemViewHolder<HeaderHolder> {
-
- static final int VIEW_TYPE_ITEM_HEADER = R.layout.ringtone_item_header;
-
- private final TextView mItemHeader;
-
- private HeaderViewHolder(View itemView) {
- super(itemView);
- mItemHeader = (TextView) itemView.findViewById(R.id.ringtone_item_header);
- }
-
- @Override
- protected void onBindItemView(HeaderHolder itemHolder) {
- mItemHeader.setText(itemHolder.getTextResId());
- }
-
- public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
-
- private final LayoutInflater mInflater;
-
- Factory(LayoutInflater inflater) {
- mInflater = inflater;
- }
-
- @Override
- public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
- return new HeaderViewHolder(mInflater.inflate(viewType, parent, false));
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/HeaderViewHolder.kt b/src/com/android/deskclock/ringtone/HeaderViewHolder.kt
new file mode 100644
index 0000000..3dd287f
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/HeaderViewHolder.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.R
+
+internal class HeaderViewHolder private constructor(itemView: View)
+ : ItemViewHolder<HeaderHolder>(itemView) {
+ private val mItemHeader: TextView =
+ itemView.findViewById<View>(R.id.ringtone_item_header) as TextView
+
+ override fun onBindItemView(itemHolder: HeaderHolder) {
+ mItemHeader.setText(itemHolder.textResId)
+ }
+
+ class Factory internal constructor(private val mInflater: LayoutInflater)
+ : ItemViewHolder.Factory {
+ override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+ return HeaderViewHolder(mInflater.inflate(viewType, parent, false))
+ }
+ }
+
+ companion object {
+ const val VIEW_TYPE_ITEM_HEADER = R.layout.ringtone_item_header
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneHolder.java b/src/com/android/deskclock/ringtone/RingtoneHolder.java
deleted file mode 100644
index 37c52b6..0000000
--- a/src/com/android/deskclock/ringtone/RingtoneHolder.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.net.Uri;
-
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-import static androidx.recyclerview.widget.RecyclerView.NO_ID;
-
-abstract class RingtoneHolder extends ItemAdapter.ItemHolder<Uri> {
-
- private final String mName;
- private final boolean mHasPermissions;
- private boolean mSelected;
- private boolean mPlaying;
-
- RingtoneHolder(Uri uri, String name) {
- this(uri, name, true);
- }
-
- RingtoneHolder(Uri uri, String name, boolean hasPermissions) {
- super(uri, NO_ID);
- mName = name;
- mHasPermissions = hasPermissions;
- }
-
- long getId() { return itemId; }
- boolean hasPermissions() { return mHasPermissions; }
- Uri getUri() { return item; }
-
- boolean isSilent() { return Utils.RINGTONE_SILENT.equals(getUri()); }
-
- boolean isSelected() { return mSelected; }
- void setSelected(boolean selected) { mSelected = selected; }
-
- boolean isPlaying() { return mPlaying; }
- void setPlaying(boolean playing) { mPlaying = playing; }
-
- String getName() {
- return mName != null ? mName : DataModel.getDataModel().getRingtoneTitle(getUri());
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneHolder.kt b/src/com/android/deskclock/ringtone/RingtoneHolder.kt
new file mode 100644
index 0000000..85e3d2c
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneHolder.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.net.Uri
+import androidx.recyclerview.widget.RecyclerView.NO_ID
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+internal abstract class RingtoneHolder @JvmOverloads constructor(
+ uri: Uri,
+ private val mName: String?,
+ private val mHasPermissions: Boolean = true
+) : ItemHolder<Uri?>(uri, NO_ID) {
+ var isSelected = false
+ var isPlaying = false
+
+ val id: Long
+ get() = itemId
+
+ fun hasPermissions(): Boolean {
+ return mHasPermissions
+ }
+
+ val uri: Uri
+ get() = item!!
+
+ val isSilent: Boolean
+ get() = Utils.RINGTONE_SILENT == uri
+
+ val name: String?
+ get() = mName ?: DataModel.dataModel.getRingtoneTitle(uri)
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneLoader.java b/src/com/android/deskclock/ringtone/RingtoneLoader.java
deleted file mode 100644
index bace949..0000000
--- a/src/com/android/deskclock/ringtone/RingtoneLoader.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.content.AsyncTaskLoader;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.media.RingtoneManager;
-import android.net.Uri;
-
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.data.CustomRingtone;
-import com.android.deskclock.data.DataModel;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static android.media.AudioManager.STREAM_ALARM;
-import static com.android.deskclock.Utils.RINGTONE_SILENT;
-
-/**
- * Assembles the list of ItemHolders that back the RecyclerView used to choose a ringtone.
- */
-class RingtoneLoader extends AsyncTaskLoader<List<ItemAdapter.ItemHolder<Uri>>> {
-
- private final Uri mDefaultRingtoneUri;
- private final String mDefaultRingtoneTitle;
- private List<CustomRingtone> mCustomRingtones;
-
- RingtoneLoader(Context context, Uri defaultRingtoneUri, String defaultRingtoneTitle) {
- super(context);
- mDefaultRingtoneUri = defaultRingtoneUri;
- mDefaultRingtoneTitle = defaultRingtoneTitle;
- }
-
- @Override
- protected void onStartLoading() {
- super.onStartLoading();
-
- mCustomRingtones = DataModel.getDataModel().getCustomRingtones();
- forceLoad();
- }
-
- @Override
- public List<ItemAdapter.ItemHolder<Uri>> loadInBackground() {
- // Prime the ringtone title cache for later access.
- DataModel.getDataModel().loadRingtoneTitles();
- DataModel.getDataModel().loadRingtonePermissions();
-
- // Fetch the standard system ringtones.
- final RingtoneManager ringtoneManager = new RingtoneManager(getContext());
- ringtoneManager.setType(STREAM_ALARM);
-
- Cursor systemRingtoneCursor;
- try {
- systemRingtoneCursor = ringtoneManager.getCursor();
- } catch (Exception e) {
- LogUtils.e("Could not get system ringtone cursor");
- systemRingtoneCursor = new MatrixCursor(new String[] {});
- }
- final int systemRingtoneCount = systemRingtoneCursor.getCount();
- // item count = # system ringtones + # custom ringtones + 2 headers + Add new music item
- final int itemCount = systemRingtoneCount + mCustomRingtones.size() + 3;
-
- final List<ItemAdapter.ItemHolder<Uri>> itemHolders = new ArrayList<>(itemCount);
-
- // Add the item holder for the Music heading.
- itemHolders.add(new HeaderHolder(R.string.your_sounds));
-
- // Add an item holder for each custom ringtone and also cache a pretty name.
- for (CustomRingtone ringtone : mCustomRingtones) {
- itemHolders.add(new CustomRingtoneHolder(ringtone));
- }
-
- // Add an item holder for the "Add new" music ringtone.
- itemHolders.add(new AddCustomRingtoneHolder());
-
- // Add an item holder for the Ringtones heading.
- itemHolders.add(new HeaderHolder(R.string.device_sounds));
-
- // Add an item holder for the silent ringtone.
- itemHolders.add(new SystemRingtoneHolder(RINGTONE_SILENT, null));
-
- // Add an item holder for the system default alarm sound.
- itemHolders.add(new SystemRingtoneHolder(mDefaultRingtoneUri, mDefaultRingtoneTitle));
-
- // Add an item holder for each system ringtone.
- for (int i = 0; i < systemRingtoneCount; i++) {
- final Uri ringtoneUri = ringtoneManager.getRingtoneUri(i);
- itemHolders.add(new SystemRingtoneHolder(ringtoneUri, null));
- }
-
- return itemHolders;
- }
-
- @Override
- protected void onReset() {
- super.onReset();
- mCustomRingtones = null;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneLoader.kt b/src/com/android/deskclock/ringtone/RingtoneLoader.kt
new file mode 100644
index 0000000..981dd31
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneLoader.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.content.Context
+import android.database.MatrixCursor
+import android.media.AudioManager
+import android.media.RingtoneManager
+import android.net.Uri
+import androidx.loader.content.AsyncTaskLoader
+
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.data.CustomRingtone
+import com.android.deskclock.data.DataModel
+
+/**
+ * Assembles the list of ItemHolders that back the RecyclerView used to choose a ringtone.
+ */
+internal class RingtoneLoader(
+ context: Context,
+ private val mDefaultRingtoneUri: Uri,
+ private val mDefaultRingtoneTitle: String
+) : AsyncTaskLoader<List<ItemHolder<Uri?>>>(context) {
+ private var mCustomRingtones: List<CustomRingtone>? = null
+
+ override fun onStartLoading() {
+ super.onStartLoading()
+
+ mCustomRingtones = DataModel.dataModel.customRingtones
+ forceLoad()
+ }
+
+ override fun loadInBackground(): List<ItemHolder<Uri?>> {
+ // Prime the ringtone title cache for later access.
+ DataModel.dataModel.loadRingtoneTitles()
+ DataModel.dataModel.loadRingtonePermissions()
+
+ // Fetch the standard system ringtones.
+ val ringtoneManager = RingtoneManager(context)
+ ringtoneManager.setType(AudioManager.STREAM_ALARM)
+
+ val systemRingtoneCursor = try {
+ ringtoneManager.cursor
+ } catch (e: Exception) {
+ LogUtils.e("Could not get system ringtone cursor")
+ MatrixCursor(arrayOf())
+ }
+ val systemRingtoneCount = systemRingtoneCursor.count
+ // item count = # system ringtones + # custom ringtones + 2 headers + Add new music item
+ val itemCount = systemRingtoneCount + mCustomRingtones!!.size + 3
+
+ val itemHolders: MutableList<ItemHolder<Uri?>> = ArrayList(itemCount)
+
+ // Add the item holder for the Music heading.
+ itemHolders.add(HeaderHolder(R.string.your_sounds))
+
+ // Add an item holder for each custom ringtone and also cache a pretty name.
+ for (ringtone in mCustomRingtones!!) {
+ itemHolders.add(CustomRingtoneHolder(ringtone))
+ }
+
+ // Add an item holder for the "Add new" music ringtone.
+ itemHolders.add(AddCustomRingtoneHolder())
+
+ // Add an item holder for the Ringtones heading.
+ itemHolders.add(HeaderHolder(R.string.device_sounds))
+
+ // Add an item holder for the silent ringtone.
+ itemHolders.add(SystemRingtoneHolder(Utils.RINGTONE_SILENT, null))
+
+ // Add an item holder for the system default alarm sound.
+ itemHolders.add(SystemRingtoneHolder(mDefaultRingtoneUri, mDefaultRingtoneTitle))
+
+ // Add an item holder for each system ringtone.
+ for (i in 0 until systemRingtoneCount) {
+ val ringtoneUri = ringtoneManager.getRingtoneUri(i)
+ itemHolders.add(SystemRingtoneHolder(ringtoneUri, null))
+ }
+
+ return itemHolders
+ }
+
+ override fun onReset() {
+ super.onReset()
+ mCustomRingtones = null
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtonePickerActivity.java b/src/com/android/deskclock/ringtone/RingtonePickerActivity.java
deleted file mode 100644
index 40ddb04..0000000
--- a/src/com/android/deskclock/ringtone/RingtonePickerActivity.java
+++ /dev/null
@@ -1,674 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.FragmentManager;
-import android.app.LoaderManager;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.Loader;
-import android.database.Cursor;
-import android.media.AudioManager;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.MediaStore;
-import androidx.annotation.VisibleForTesting;
-import androidx.appcompat.app.AlertDialog;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-
-import com.android.deskclock.BaseActivity;
-import com.android.deskclock.DropShadowController;
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.ItemAdapter.OnItemClickedListener;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.RingtonePreviewKlaxon;
-import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
-import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
-import com.android.deskclock.actionbarmenu.OptionsMenuManager;
-import com.android.deskclock.alarms.AlarmUpdateHandler;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.provider.Alarm;
-
-import java.util.List;
-
-import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
-import static android.media.RingtoneManager.TYPE_ALARM;
-import static android.provider.OpenableColumns.DISPLAY_NAME;
-import static com.android.deskclock.ItemAdapter.ItemViewHolder.Factory;
-import static com.android.deskclock.ringtone.AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW;
-import static com.android.deskclock.ringtone.HeaderViewHolder.VIEW_TYPE_ITEM_HEADER;
-import static com.android.deskclock.ringtone.RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND;
-import static com.android.deskclock.ringtone.RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND;
-
-/**
- * This activity presents a set of ringtones from which the user may select one. The set includes:
- * <ul>
- * <li>system ringtones from the Android framework</li>
- * <li>a ringtone representing pure silence</li>
- * <li>a ringtone representing a default ringtone</li>
- * <li>user-selected audio files available as ringtones</li>
- * </ul>
- */
-public class RingtonePickerActivity extends BaseActivity
- implements LoaderManager.LoaderCallbacks<List<ItemAdapter.ItemHolder<Uri>>> {
-
- /** Key to an extra that defines resource id to the title of this activity. */
- private static final String EXTRA_TITLE = "extra_title";
-
- /** Key to an extra that identifies the alarm to which the selected ringtone is attached. */
- private static final String EXTRA_ALARM_ID = "extra_alarm_id";
-
- /** Key to an extra that identifies the selected ringtone. */
- private static final String EXTRA_RINGTONE_URI = "extra_ringtone_uri";
-
- /** Key to an extra that defines the uri representing the default ringtone. */
- private static final String EXTRA_DEFAULT_RINGTONE_URI = "extra_default_ringtone_uri";
-
- /** Key to an extra that defines the name of the default ringtone. */
- private static final String EXTRA_DEFAULT_RINGTONE_NAME = "extra_default_ringtone_name";
-
- /** Key to an instance state value indicating if the selected ringtone is currently playing. */
- private static final String STATE_KEY_PLAYING = "extra_is_playing";
-
- /** The controller that shows the drop shadow when content is not scrolled to the top. */
- private DropShadowController mDropShadowController;
-
- /** Generates the items in the activity context menu. */
- private OptionsMenuManager mOptionsMenuManager;
-
- /** Displays a set of selectable ringtones. */
- private RecyclerView mRecyclerView;
-
- /** Stores the set of ItemHolders that wrap the selectable ringtones. */
- private ItemAdapter<ItemAdapter.ItemHolder<Uri>> mRingtoneAdapter;
-
- /** The title of the default ringtone. */
- private String mDefaultRingtoneTitle;
-
- /** The uri of the default ringtone. */
- private Uri mDefaultRingtoneUri;
-
- /** The uri of the ringtone to select after data is loaded. */
- private Uri mSelectedRingtoneUri;
-
- /** {@code true} indicates the {@link #mSelectedRingtoneUri} must be played after data load. */
- private boolean mIsPlaying;
-
- /** Identifies the alarm to receive the selected ringtone; -1 indicates there is no alarm. */
- private long mAlarmId;
-
- /** The location of the custom ringtone to be removed. */
- private int mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION;
-
- /**
- * @return an intent that launches the ringtone picker to edit the ringtone of the given
- * {@code alarm}
- */
- public static Intent createAlarmRingtonePickerIntent(Context context, Alarm alarm) {
- return new Intent(context, RingtonePickerActivity.class)
- .putExtra(EXTRA_TITLE, R.string.alarm_sound)
- .putExtra(EXTRA_ALARM_ID, alarm.id)
- .putExtra(EXTRA_RINGTONE_URI, alarm.alert)
- .putExtra(EXTRA_DEFAULT_RINGTONE_URI, RingtoneManager.getDefaultUri(TYPE_ALARM))
- .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_alarm_ringtone_title);
- }
-
- /**
- * @return an intent that launches the ringtone picker to edit the ringtone of all timers
- */
- public static Intent createTimerRingtonePickerIntent(Context context) {
- final DataModel dataModel = DataModel.getDataModel();
- return new Intent(context, RingtonePickerActivity.class)
- .putExtra(EXTRA_TITLE, R.string.timer_sound)
- .putExtra(EXTRA_RINGTONE_URI, dataModel.getTimerRingtoneUri())
- .putExtra(EXTRA_DEFAULT_RINGTONE_URI, dataModel.getDefaultTimerRingtoneUri())
- .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_timer_ringtone_title);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.ringtone_picker);
- setVolumeControlStream(AudioManager.STREAM_ALARM);
-
- mOptionsMenuManager = new OptionsMenuManager();
- mOptionsMenuManager.addMenuItemController(new NavUpMenuItemController(this))
- .addMenuItemController(MenuItemControllerFactory.getInstance()
- .buildMenuItemControllers(this));
-
- final Context context = getApplicationContext();
- final Intent intent = getIntent();
-
- if (savedInstanceState != null) {
- mIsPlaying = savedInstanceState.getBoolean(STATE_KEY_PLAYING);
- mSelectedRingtoneUri = savedInstanceState.getParcelable(EXTRA_RINGTONE_URI);
- }
-
- if (mSelectedRingtoneUri == null) {
- mSelectedRingtoneUri = intent.getParcelableExtra(EXTRA_RINGTONE_URI);
- }
-
- mAlarmId = intent.getLongExtra(EXTRA_ALARM_ID, -1);
- mDefaultRingtoneUri = intent.getParcelableExtra(EXTRA_DEFAULT_RINGTONE_URI);
- final int defaultRingtoneTitleId = intent.getIntExtra(EXTRA_DEFAULT_RINGTONE_NAME, 0);
- mDefaultRingtoneTitle = context.getString(defaultRingtoneTitleId);
-
- final LayoutInflater inflater = getLayoutInflater();
- final OnItemClickedListener listener = new ItemClickWatcher();
- final Factory ringtoneFactory = new RingtoneViewHolder.Factory(inflater);
- final Factory headerFactory = new HeaderViewHolder.Factory(inflater);
- final Factory addNewFactory = new AddCustomRingtoneViewHolder.Factory(inflater);
- mRingtoneAdapter = new ItemAdapter<>();
- mRingtoneAdapter.withViewTypes(headerFactory, null, VIEW_TYPE_ITEM_HEADER)
- .withViewTypes(addNewFactory, listener, VIEW_TYPE_ADD_NEW)
- .withViewTypes(ringtoneFactory, listener, VIEW_TYPE_SYSTEM_SOUND)
- .withViewTypes(ringtoneFactory, listener, VIEW_TYPE_CUSTOM_SOUND);
-
- mRecyclerView = (RecyclerView) findViewById(R.id.ringtone_content);
- mRecyclerView.setLayoutManager(new LinearLayoutManager(context));
- mRecyclerView.setAdapter(mRingtoneAdapter);
- mRecyclerView.setItemAnimator(null);
-
- mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
- if (mIndexOfRingtoneToRemove != RecyclerView.NO_POSITION) {
- closeContextMenu();
- }
- }
- });
-
- final int titleResourceId = intent.getIntExtra(EXTRA_TITLE, 0);
- setTitle(context.getString(titleResourceId));
-
- getLoaderManager().initLoader(0 /* id */, null /* args */, this /* callback */);
-
- registerForContextMenu(mRecyclerView);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- final View dropShadow = findViewById(R.id.drop_shadow);
- mDropShadowController = new DropShadowController(dropShadow, mRecyclerView);
- }
-
- @Override
- protected void onPause() {
- mDropShadowController.stop();
- mDropShadowController = null;
-
- if (mSelectedRingtoneUri != null) {
- if (mAlarmId != -1) {
- final Context context = getApplicationContext();
- final ContentResolver cr = getContentResolver();
-
- // Start a background task to fetch the alarm whose ringtone must be updated.
- new AsyncTask<Void, Void, Alarm>() {
- @Override
- protected Alarm doInBackground(Void... parameters) {
- final Alarm alarm = Alarm.getAlarm(cr, mAlarmId);
- if (alarm != null) {
- alarm.alert = mSelectedRingtoneUri;
- }
- return alarm;
- }
-
- @Override
- protected void onPostExecute(Alarm alarm) {
- // Update the default ringtone for future new alarms.
- DataModel.getDataModel().setDefaultAlarmRingtoneUri(alarm.alert);
-
- // Start a second background task to persist the updated alarm.
- new AlarmUpdateHandler(context, null, null)
- .asyncUpdateAlarm(alarm, false, true);
- }
- }.execute();
- } else {
- DataModel.getDataModel().setTimerRingtoneUri(mSelectedRingtoneUri);
- }
- }
-
- super.onPause();
- }
-
- @Override
- protected void onStop() {
- if (!isChangingConfigurations()) {
- stopPlayingRingtone(getSelectedRingtoneHolder(), false);
- }
- super.onStop();
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putBoolean(STATE_KEY_PLAYING, mIsPlaying);
- outState.putParcelable(EXTRA_RINGTONE_URI, mSelectedRingtoneUri);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- mOptionsMenuManager.onCreateOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- mOptionsMenuManager.onPrepareOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- return mOptionsMenuManager.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
- }
-
- @Override
- public Loader<List<ItemAdapter.ItemHolder<Uri>>> onCreateLoader(int id, Bundle args) {
- return new RingtoneLoader(getApplicationContext(), mDefaultRingtoneUri,
- mDefaultRingtoneTitle);
- }
-
- @Override
- public void onLoadFinished(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader,
- List<ItemAdapter.ItemHolder<Uri>> itemHolders) {
- // Update the adapter with fresh data.
- mRingtoneAdapter.setItems(itemHolders);
-
- // Attempt to select the requested ringtone.
- final RingtoneHolder toSelect = getRingtoneHolder(mSelectedRingtoneUri);
- if (toSelect != null) {
- toSelect.setSelected(true);
- mSelectedRingtoneUri = toSelect.getUri();
- toSelect.notifyItemChanged();
-
- // Start playing the ringtone if indicated.
- if (mIsPlaying) {
- startPlayingRingtone(toSelect);
- }
- } else {
- // Clear the selection since it does not exist in the data.
- RingtonePreviewKlaxon.stop(this);
- mSelectedRingtoneUri = null;
- mIsPlaying = false;
- }
- }
-
- @Override
- public void onLoaderReset(Loader<List<ItemAdapter.ItemHolder<Uri>>> loader) {}
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode != RESULT_OK) {
- return;
- }
-
- final Uri uri = data == null ? null : data.getData();
- if (uri == null) {
- return;
- }
-
- // Bail if the permission to read (playback) the audio at the uri was not granted.
- final int flags = data.getFlags() & FLAG_GRANT_READ_URI_PERMISSION;
- if (flags != FLAG_GRANT_READ_URI_PERMISSION) {
- return;
- }
-
- // Start a task to fetch the display name of the audio content and add the custom ringtone.
- new AddCustomRingtoneTask(uri).execute();
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- // Find the ringtone to be removed.
- final List<ItemAdapter.ItemHolder<Uri>> items = mRingtoneAdapter.getItems();
- final RingtoneHolder toRemove = (RingtoneHolder) items.get(mIndexOfRingtoneToRemove);
- mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION;
-
- // Launch the confirmation dialog.
- final FragmentManager manager = getFragmentManager();
- final boolean hasPermissions = toRemove.hasPermissions();
- ConfirmRemoveCustomRingtoneDialogFragment.show(manager, toRemove.getUri(), hasPermissions);
- return true;
- }
-
- private RingtoneHolder getRingtoneHolder(Uri uri) {
- for (ItemAdapter.ItemHolder<Uri> itemHolder : mRingtoneAdapter.getItems()) {
- if (itemHolder instanceof RingtoneHolder) {
- final RingtoneHolder ringtoneHolder = (RingtoneHolder) itemHolder;
- if (ringtoneHolder.getUri().equals(uri)) {
- return ringtoneHolder;
- }
- }
- }
-
- return null;
- }
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- RingtoneHolder getSelectedRingtoneHolder() {
- return getRingtoneHolder(mSelectedRingtoneUri);
- }
-
- /**
- * The given {@code ringtone} will be selected as a side-effect of playing the ringtone.
- *
- * @param ringtone the ringtone to be played
- */
- private void startPlayingRingtone(RingtoneHolder ringtone) {
- if (!ringtone.isPlaying() && !ringtone.isSilent()) {
- RingtonePreviewKlaxon.start(getApplicationContext(), ringtone.getUri());
- ringtone.setPlaying(true);
- mIsPlaying = true;
- }
- if (!ringtone.isSelected()) {
- ringtone.setSelected(true);
- mSelectedRingtoneUri = ringtone.getUri();
- }
- ringtone.notifyItemChanged();
- }
-
- /**
- * @param ringtone the ringtone to stop playing
- * @param deselect {@code true} indicates the ringtone should also be deselected;
- * {@code false} indicates its selection state should remain unchanged
- */
- private void stopPlayingRingtone(RingtoneHolder ringtone, boolean deselect) {
- if (ringtone == null) {
- return;
- }
-
- if (ringtone.isPlaying()) {
- RingtonePreviewKlaxon.stop(this);
- ringtone.setPlaying(false);
- mIsPlaying = false;
- }
- if (deselect && ringtone.isSelected()) {
- ringtone.setSelected(false);
- mSelectedRingtoneUri = null;
- }
- ringtone.notifyItemChanged();
- }
-
- /**
- * Proceeds with removing the custom ringtone with the given uri.
- *
- * @param toRemove identifies the custom ringtone to be removed
- */
- private void removeCustomRingtone(Uri toRemove) {
- new RemoveCustomRingtoneTask(toRemove).execute();
- }
-
- /**
- * This DialogFragment informs the user of the side-effects of removing a custom ringtone while
- * it is in use by alarms and/or timers and prompts them to confirm the removal.
- */
- public static class ConfirmRemoveCustomRingtoneDialogFragment extends DialogFragment {
-
- private static final String ARG_RINGTONE_URI_TO_REMOVE = "arg_ringtone_uri_to_remove";
- private static final String ARG_RINGTONE_HAS_PERMISSIONS = "arg_ringtone_has_permissions";
-
- static void show(FragmentManager manager, Uri toRemove, boolean hasPermissions) {
- if (manager.isDestroyed()) {
- return;
- }
-
- final Bundle args = new Bundle();
- args.putParcelable(ARG_RINGTONE_URI_TO_REMOVE, toRemove);
- args.putBoolean(ARG_RINGTONE_HAS_PERMISSIONS, hasPermissions);
-
- final DialogFragment fragment = new ConfirmRemoveCustomRingtoneDialogFragment();
- fragment.setArguments(args);
- fragment.setCancelable(hasPermissions);
- fragment.show(manager, "confirm_ringtone_remove");
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- final Bundle arguments = getArguments();
- final Uri toRemove = arguments.getParcelable(ARG_RINGTONE_URI_TO_REMOVE);
-
- final DialogInterface.OnClickListener okListener =
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- ((RingtonePickerActivity) getActivity()).removeCustomRingtone(toRemove);
- }
- };
-
- if (arguments.getBoolean(ARG_RINGTONE_HAS_PERMISSIONS)) {
- return new AlertDialog.Builder(getActivity())
- .setPositiveButton(R.string.remove_sound, okListener)
- .setNegativeButton(android.R.string.cancel, null /* listener */)
- .setMessage(R.string.confirm_remove_custom_ringtone)
- .create();
- } else {
- return new AlertDialog.Builder(getActivity())
- .setPositiveButton(R.string.remove_sound, okListener)
- .setMessage(R.string.custom_ringtone_lost_permissions)
- .create();
- }
- }
- }
-
- /**
- * This click handler alters selection and playback of ringtones. It also launches the system
- * file chooser to search for openable audio files that may serve as ringtones.
- */
- private class ItemClickWatcher implements OnItemClickedListener {
- @Override
- public void onItemClicked(ItemAdapter.ItemViewHolder<?> viewHolder, int id) {
- switch (id) {
- case AddCustomRingtoneViewHolder.CLICK_ADD_NEW:
- stopPlayingRingtone(getSelectedRingtoneHolder(), false);
- startActivityForResult(new Intent(Intent.ACTION_OPEN_DOCUMENT)
- .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType("audio/*"), 0);
- break;
-
- case RingtoneViewHolder.CLICK_NORMAL:
- final RingtoneHolder oldSelection = getSelectedRingtoneHolder();
- final RingtoneHolder newSelection = (RingtoneHolder) viewHolder.getItemHolder();
-
- // Tapping the existing selection toggles playback of the ringtone.
- if (oldSelection == newSelection) {
- if (newSelection.isPlaying()) {
- stopPlayingRingtone(newSelection, false);
- } else {
- startPlayingRingtone(newSelection);
- }
- } else {
- // Tapping a new selection changes the selection and playback.
- stopPlayingRingtone(oldSelection, true);
- startPlayingRingtone(newSelection);
- }
- break;
-
- case RingtoneViewHolder.CLICK_LONG_PRESS:
- mIndexOfRingtoneToRemove = viewHolder.getAdapterPosition();
- break;
-
- case RingtoneViewHolder.CLICK_NO_PERMISSIONS:
- ConfirmRemoveCustomRingtoneDialogFragment.show(getFragmentManager(),
- ((RingtoneHolder) viewHolder.getItemHolder()).getUri(), false);
- break;
- }
- }
- }
-
- /**
- * This task locates a displayable string in the background that is fit for use as the title of
- * the audio content. It adds a custom ringtone using the uri and title on the main thread.
- */
- private final class AddCustomRingtoneTask extends AsyncTask<Void, Void, String> {
-
- private final Uri mUri;
- private final Context mContext;
-
- private AddCustomRingtoneTask(Uri uri) {
- mUri = uri;
- mContext = getApplicationContext();
- }
-
- @Override
- protected String doInBackground(Void... voids) {
- final ContentResolver contentResolver = mContext.getContentResolver();
-
- // Take the long-term permission to read (playback) the audio at the uri.
- contentResolver.takePersistableUriPermission(mUri, FLAG_GRANT_READ_URI_PERMISSION);
-
- try (Cursor cursor = contentResolver.query(mUri, null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- // If the file was a media file, return its title.
- final int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
- if (titleIndex != -1) {
- return cursor.getString(titleIndex);
- }
-
- // If the file was a simple openable, return its display name.
- final int displayNameIndex = cursor.getColumnIndex(DISPLAY_NAME);
- if (displayNameIndex != -1) {
- String title = cursor.getString(displayNameIndex);
- final int dotIndex = title.lastIndexOf(".");
- if (dotIndex > 0) {
- title = title.substring(0, dotIndex);
- }
- return title;
- }
- } else {
- LogUtils.e("No ringtone for uri: %s", mUri);
- }
- } catch (Exception e) {
- LogUtils.e("Unable to locate title for custom ringtone: " + mUri, e);
- }
-
- return mContext.getString(R.string.unknown_ringtone_title);
- }
-
- @Override
- protected void onPostExecute(String title) {
- // Add the new custom ringtone to the data model.
- DataModel.getDataModel().addCustomRingtone(mUri, title);
-
- // When the loader completes, it must play the new ringtone.
- mSelectedRingtoneUri = mUri;
- mIsPlaying = true;
-
- // Reload the data to reflect the change in the UI.
- getLoaderManager().restartLoader(0 /* id */, null /* args */,
- RingtonePickerActivity.this /* callback */);
- }
- }
-
- /**
- * Removes a custom ringtone with the given uri. Taking this action has side-effects because
- * all alarms that use the custom ringtone are reassigned to the Android system default alarm
- * ringtone. If the application's default alarm ringtone is being removed, it is reset to the
- * Android system default alarm ringtone. If the application's timer ringtone is being removed,
- * it is reset to the application's default timer ringtone.
- */
- private final class RemoveCustomRingtoneTask extends AsyncTask<Void, Void, Void> {
-
- private final Uri mRemoveUri;
- private Uri mSystemDefaultRingtoneUri;
-
- private RemoveCustomRingtoneTask(Uri removeUri) {
- mRemoveUri = removeUri;
- }
-
- @Override
- protected Void doInBackground(Void... voids) {
- mSystemDefaultRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
-
- // Update all alarms that use the custom ringtone to use the system default.
- final ContentResolver cr = getContentResolver();
- final List<Alarm> alarms = Alarm.getAlarms(cr, null);
- for (Alarm alarm : alarms) {
- if (mRemoveUri.equals(alarm.alert)) {
- alarm.alert = mSystemDefaultRingtoneUri;
- // Start a second background task to persist the updated alarm.
- new AlarmUpdateHandler(RingtonePickerActivity.this, null, null)
- .asyncUpdateAlarm(alarm, false, true);
- }
- }
-
- try {
- // Release the permission to read (playback) the audio at the uri.
- cr.releasePersistableUriPermission(mRemoveUri, FLAG_GRANT_READ_URI_PERMISSION);
- } catch (SecurityException ignore) {
- // If the file was already deleted from the file system, a SecurityException is
- // thrown indicating this app did not hold the read permission being released.
- LogUtils.w("SecurityException while releasing read permission for " + mRemoveUri);
- }
-
- return null;
- }
-
- @Override
- protected void onPostExecute(Void v) {
- // Reset the default alarm ringtone if it was just removed.
- if (mRemoveUri.equals(DataModel.getDataModel().getDefaultAlarmRingtoneUri())) {
- DataModel.getDataModel().setDefaultAlarmRingtoneUri(mSystemDefaultRingtoneUri);
- }
-
- // Reset the timer ringtone if it was just removed.
- if (mRemoveUri.equals(DataModel.getDataModel().getTimerRingtoneUri())) {
- final Uri timerRingtoneUri = DataModel.getDataModel().getDefaultTimerRingtoneUri();
- DataModel.getDataModel().setTimerRingtoneUri(timerRingtoneUri);
- }
-
- // Remove the corresponding custom ringtone.
- DataModel.getDataModel().removeCustomRingtone(mRemoveUri);
-
- // Find the ringtone to be removed from the adapter.
- final RingtoneHolder toRemove = getRingtoneHolder(mRemoveUri);
- if (toRemove == null) {
- return;
- }
-
- // If the ringtone to remove is also the selected ringtone, adjust the selection.
- if (toRemove.isSelected()) {
- stopPlayingRingtone(toRemove, false);
- final RingtoneHolder defaultRingtone = getRingtoneHolder(mDefaultRingtoneUri);
- if (defaultRingtone != null) {
- defaultRingtone.setSelected(true);
- mSelectedRingtoneUri = defaultRingtone.getUri();
- defaultRingtone.notifyItemChanged();
- }
- }
-
- // Remove the ringtone from the adapter.
- mRingtoneAdapter.removeItem(toRemove);
- }
- }
-}
diff --git a/src/com/android/deskclock/ringtone/RingtonePickerActivity.kt b/src/com/android/deskclock/ringtone/RingtonePickerActivity.kt
new file mode 100644
index 0000000..1dadc69
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtonePickerActivity.kt
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.app.Dialog
+import android.content.Context
+import android.content.ContentResolver
+import android.content.DialogInterface
+import android.content.Intent
+import android.media.AudioManager
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Bundle
+import android.provider.MediaStore
+import android.provider.OpenableColumns
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.annotation.Keep
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+import androidx.loader.app.LoaderManager
+import androidx.loader.app.LoaderManager.LoaderCallbacks
+import androidx.loader.content.Loader
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.DropShadowController
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.RingtonePreviewKlaxon
+import com.android.deskclock.ItemAdapter
+import com.android.deskclock.ItemAdapter.ItemHolder
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.ItemAdapter.OnItemClickedListener
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory
+import com.android.deskclock.actionbarmenu.NavUpMenuItemController
+import com.android.deskclock.actionbarmenu.OptionsMenuManager
+import com.android.deskclock.alarms.AlarmUpdateHandler
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.provider.Alarm
+
+/**
+ * This activity presents a set of ringtones from which the user may select one. The set includes:
+ *
+ * * system ringtones from the Android framework
+ * * a ringtone representing pure silence
+ * * a ringtone representing a default ringtone
+ * * user-selected audio files available as ringtones
+ *
+ */
+// TODO(b/165664115) Replace deprecated AsyncTask calls
+class RingtonePickerActivity : BaseActivity(), LoaderCallbacks<List<ItemHolder<Uri?>>> {
+ /** The controller that shows the drop shadow when content is not scrolled to the top. */
+ private var mDropShadowController: DropShadowController? = null
+
+ /** Generates the items in the activity context menu. */
+ private lateinit var mOptionsMenuManager: OptionsMenuManager
+
+ /** Displays a set of selectable ringtones. */
+ private lateinit var mRecyclerView: RecyclerView
+
+ /** Stores the set of ItemHolders that wrap the selectable ringtones. */
+ private lateinit var mRingtoneAdapter: ItemAdapter<ItemHolder<Uri?>>
+
+ /** The title of the default ringtone. */
+ private var mDefaultRingtoneTitle: String? = null
+
+ /** The uri of the default ringtone. */
+ private var mDefaultRingtoneUri: Uri? = null
+
+ /** The uri of the ringtone to select after data is loaded. */
+ private var mSelectedRingtoneUri: Uri? = null
+
+ /** `true` indicates the [.mSelectedRingtoneUri] must be played after data load. */
+ private var mIsPlaying = false
+
+ /** Identifies the alarm to receive the selected ringtone; -1 indicates there is no alarm. */
+ private var mAlarmId: Long = -1
+
+ /** The location of the custom ringtone to be removed. */
+ private var mIndexOfRingtoneToRemove: Int = RecyclerView.NO_POSITION
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.ringtone_picker)
+ setVolumeControlStream(AudioManager.STREAM_ALARM)
+
+ mOptionsMenuManager = OptionsMenuManager()
+ mOptionsMenuManager.addMenuItemController(NavUpMenuItemController(this))
+ .addMenuItemController(*MenuItemControllerFactory.buildMenuItemControllers(this))
+
+ val context: Context = getApplicationContext()
+ val intent: Intent = getIntent()
+
+ if (savedInstanceState != null) {
+ mIsPlaying = savedInstanceState.getBoolean(STATE_KEY_PLAYING)
+ mSelectedRingtoneUri = savedInstanceState.getParcelable(EXTRA_RINGTONE_URI)
+ }
+
+ if (mSelectedRingtoneUri == null) {
+ mSelectedRingtoneUri = intent.getParcelableExtra(EXTRA_RINGTONE_URI)
+ }
+
+ mAlarmId = intent.getLongExtra(EXTRA_ALARM_ID, -1)
+ mDefaultRingtoneUri = intent.getParcelableExtra(EXTRA_DEFAULT_RINGTONE_URI)
+ val defaultRingtoneTitleId = intent.getIntExtra(EXTRA_DEFAULT_RINGTONE_NAME, 0)
+ mDefaultRingtoneTitle = context.getString(defaultRingtoneTitleId)
+
+ val inflater: LayoutInflater = getLayoutInflater()
+ val listener: OnItemClickedListener = ItemClickWatcher()
+ val ringtoneFactory: ItemViewHolder.Factory = RingtoneViewHolder.Factory(inflater)
+ val headerFactory: ItemViewHolder.Factory = HeaderViewHolder.Factory(inflater)
+ val addNewFactory: ItemViewHolder.Factory = AddCustomRingtoneViewHolder.Factory(inflater)
+ mRingtoneAdapter = ItemAdapter()
+ mRingtoneAdapter
+ .withViewTypes(headerFactory, null, HeaderViewHolder.VIEW_TYPE_ITEM_HEADER)
+ .withViewTypes(addNewFactory, listener,
+ AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW)
+ .withViewTypes(ringtoneFactory, listener, RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND)
+ .withViewTypes(ringtoneFactory, listener, RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND)
+
+ mRecyclerView = findViewById(R.id.ringtone_content) as RecyclerView
+ mRecyclerView.setLayoutManager(LinearLayoutManager(context))
+ mRecyclerView.setAdapter(mRingtoneAdapter)
+ mRecyclerView.setItemAnimator(null)
+
+ mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ if (mIndexOfRingtoneToRemove != RecyclerView.NO_POSITION) {
+ closeContextMenu()
+ }
+ }
+ })
+
+ val titleResourceId = intent.getIntExtra(EXTRA_TITLE, 0)
+ setTitle(context.getString(titleResourceId))
+
+ LoaderManager.getInstance(this).initLoader(0 /* id */, Bundle.EMPTY /* args */,
+ this /* callback */)
+
+ registerForContextMenu(mRecyclerView)
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ val dropShadow: View = findViewById(R.id.drop_shadow)
+ mDropShadowController = DropShadowController(dropShadow, mRecyclerView)
+ }
+
+ override fun onPause() {
+ mDropShadowController!!.stop()
+ mDropShadowController = null
+
+ mSelectedRingtoneUri?.let {
+ if (mAlarmId != -1L) {
+ val context: Context = getApplicationContext()
+ val cr: ContentResolver = getContentResolver()
+
+ // Start a background task to fetch the alarm whose ringtone must be updated.
+ object : AsyncTask<Void?, Void?, Alarm>() {
+ override fun doInBackground(vararg parameters: Void?): Alarm? {
+ val alarm = Alarm.getAlarm(cr, mAlarmId)
+ if (alarm != null) {
+ alarm.alert = it
+ }
+ return alarm
+ }
+
+ override fun onPostExecute(alarm: Alarm) {
+ // Update the default ringtone for future new alarms.
+ DataModel.dataModel.defaultAlarmRingtoneUri = alarm.alert!!
+
+ // Start a second background task to persist the updated alarm.
+ AlarmUpdateHandler(context, mScrollHandler = null, mSnackbarAnchor = null)
+ .asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+ }
+ }.execute()
+ } else {
+ DataModel.dataModel.timerRingtoneUri = it
+ }
+ }
+
+ super.onPause()
+ }
+
+ override fun onStop() {
+ if (!isChangingConfigurations()) {
+ stopPlayingRingtone(selectedRingtoneHolder, false)
+ }
+ super.onStop()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+
+ outState.putBoolean(STATE_KEY_PLAYING, mIsPlaying)
+ outState.putParcelable(EXTRA_RINGTONE_URI, mSelectedRingtoneUri)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ mOptionsMenuManager.onCreateOptionsMenu(menu)
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ mOptionsMenuManager.onPrepareOptionsMenu(menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return mOptionsMenuManager.onOptionsItemSelected(item) ||
+ super.onOptionsItemSelected(item)
+ }
+
+ override fun onCreateLoader(id: Int, args: Bundle?): Loader<List<ItemHolder<Uri?>>> {
+ return RingtoneLoader(getApplicationContext(), mDefaultRingtoneUri!!,
+ mDefaultRingtoneTitle!!)
+ }
+
+ override fun onLoadFinished(
+ loader: Loader<List<ItemHolder<Uri?>>>,
+ itemHolders: List<ItemHolder<Uri?>>
+ ) {
+ // Update the adapter with fresh data.
+ mRingtoneAdapter.setItems(itemHolders)
+
+ // Attempt to select the requested ringtone.
+ val toSelect = getRingtoneHolder(mSelectedRingtoneUri)
+ if (toSelect != null) {
+ toSelect.isSelected = true
+ mSelectedRingtoneUri = toSelect.uri
+ toSelect.notifyItemChanged()
+
+ // Start playing the ringtone if indicated.
+ if (mIsPlaying) {
+ startPlayingRingtone(toSelect)
+ }
+ } else {
+ // Clear the selection since it does not exist in the data.
+ RingtonePreviewKlaxon.stop(this)
+ mSelectedRingtoneUri = null
+ mIsPlaying = false
+ }
+ }
+
+ override fun onLoaderReset(loader: Loader<List<ItemHolder<Uri?>>>) {
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode != RESULT_OK) {
+ return
+ }
+
+ val uri = data?.data ?: return
+
+ // Bail if the permission to read (playback) the audio at the uri was not granted.
+ val flags = data.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION
+ if (flags != Intent.FLAG_GRANT_READ_URI_PERMISSION) {
+ return
+ }
+
+ // Start a task to fetch the display name of the audio content and add the custom ringtone.
+ AddCustomRingtoneTask(uri).execute()
+ }
+
+ override fun onContextItemSelected(item: MenuItem): Boolean {
+ // Find the ringtone to be removed.
+ val items = mRingtoneAdapter.items
+ val toRemove = items!![mIndexOfRingtoneToRemove] as RingtoneHolder
+ mIndexOfRingtoneToRemove = RecyclerView.NO_POSITION
+
+ // Launch the confirmation dialog.
+ val manager: FragmentManager = supportFragmentManager
+ val hasPermissions = toRemove.hasPermissions()
+ ConfirmRemoveCustomRingtoneDialogFragment.show(manager, toRemove.uri, hasPermissions)
+ return true
+ }
+
+ private fun getRingtoneHolder(uri: Uri?): RingtoneHolder? {
+ for (itemHolder in mRingtoneAdapter.items!!) {
+ if (itemHolder is RingtoneHolder) {
+ if (itemHolder.uri == uri) {
+ return itemHolder
+ }
+ }
+ }
+
+ return null
+ }
+
+ @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal val selectedRingtoneHolder: RingtoneHolder?
+ get() = getRingtoneHolder(mSelectedRingtoneUri)
+
+ /**
+ * The given `ringtone` will be selected as a side-effect of playing the ringtone.
+ *
+ * @param ringtone the ringtone to be played
+ */
+ private fun startPlayingRingtone(ringtone: RingtoneHolder) {
+ if (!ringtone.isPlaying && !ringtone.isSilent) {
+ RingtonePreviewKlaxon.start(getApplicationContext(), ringtone.uri)
+ ringtone.isPlaying = true
+ mIsPlaying = true
+ }
+ if (!ringtone.isSelected) {
+ ringtone.isSelected = true
+ mSelectedRingtoneUri = ringtone.uri
+ }
+ ringtone.notifyItemChanged()
+ }
+
+ /**
+ * @param ringtone the ringtone to stop playing
+ * @param deselect `true` indicates the ringtone should also be deselected;
+ * `false` indicates its selection state should remain unchanged
+ */
+ private fun stopPlayingRingtone(ringtone: RingtoneHolder?, deselect: Boolean) {
+ if (ringtone == null) {
+ return
+ }
+
+ if (ringtone.isPlaying) {
+ RingtonePreviewKlaxon.stop(this)
+ ringtone.isPlaying = false
+ mIsPlaying = false
+ }
+ if (deselect && ringtone.isSelected) {
+ ringtone.isSelected = false
+ mSelectedRingtoneUri = null
+ }
+ ringtone.notifyItemChanged()
+ }
+
+ /**
+ * Proceeds with removing the custom ringtone with the given uri.
+ *
+ * @param toRemove identifies the custom ringtone to be removed
+ */
+ private fun removeCustomRingtone(toRemove: Uri) {
+ RemoveCustomRingtoneTask(toRemove).execute()
+ }
+
+ /**
+ * This DialogFragment informs the user of the side-effects of removing a custom ringtone while
+ * it is in use by alarms and/or timers and prompts them to confirm the removal.
+ */
+ class ConfirmRemoveCustomRingtoneDialogFragment : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val arguments = requireArguments()
+ val toRemove = arguments.getParcelable<Uri>(ARG_RINGTONE_URI_TO_REMOVE)
+
+ val okListener = DialogInterface.OnClickListener { _, _ ->
+ (activity as RingtonePickerActivity).removeCustomRingtone(toRemove!!)
+ }
+
+ return if (arguments.getBoolean(ARG_RINGTONE_HAS_PERMISSIONS)) {
+ AlertDialog.Builder(requireActivity())
+ .setPositiveButton(R.string.remove_sound, okListener)
+ .setNegativeButton(android.R.string.cancel, null /* listener */)
+ .setMessage(R.string.confirm_remove_custom_ringtone)
+ .create()
+ } else {
+ AlertDialog.Builder(requireActivity())
+ .setPositiveButton(R.string.remove_sound, okListener)
+ .setMessage(R.string.custom_ringtone_lost_permissions)
+ .create()
+ }
+ }
+
+ companion object {
+ private const val ARG_RINGTONE_URI_TO_REMOVE = "arg_ringtone_uri_to_remove"
+ private const val ARG_RINGTONE_HAS_PERMISSIONS = "arg_ringtone_has_permissions"
+
+ fun show(manager: FragmentManager, toRemove: Uri?, hasPermissions: Boolean) {
+ if (manager.isDestroyed) {
+ return
+ }
+
+ val args = Bundle()
+ args.putParcelable(ARG_RINGTONE_URI_TO_REMOVE, toRemove)
+ args.putBoolean(ARG_RINGTONE_HAS_PERMISSIONS, hasPermissions)
+
+ val fragment: DialogFragment = ConfirmRemoveCustomRingtoneDialogFragment()
+ fragment.arguments = args
+ fragment.isCancelable = hasPermissions
+ fragment.show(manager, "confirm_ringtone_remove")
+ }
+ }
+ }
+
+ /**
+ * This click handler alters selection and playback of ringtones. It also launches the system
+ * file chooser to search for openable audio files that may serve as ringtones.
+ */
+ private inner class ItemClickWatcher : OnItemClickedListener {
+ override fun onItemClicked(viewHolder: ItemViewHolder<*>, id: Int) {
+ when (id) {
+ AddCustomRingtoneViewHolder.CLICK_ADD_NEW -> {
+ stopPlayingRingtone(selectedRingtoneHolder, false)
+ startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT)
+ .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("audio/*"), 0)
+ }
+ RingtoneViewHolder.CLICK_NORMAL -> {
+ val oldSelection = selectedRingtoneHolder
+ val newSelection = viewHolder.itemHolder as RingtoneHolder
+
+ // Tapping the existing selection toggles playback of the ringtone.
+ if (oldSelection === newSelection) {
+ if (newSelection.isPlaying) {
+ stopPlayingRingtone(newSelection, false)
+ } else {
+ startPlayingRingtone(newSelection)
+ }
+ } else {
+ // Tapping a new selection changes the selection and playback.
+ stopPlayingRingtone(oldSelection, true)
+ startPlayingRingtone(newSelection)
+ }
+ }
+ RingtoneViewHolder.CLICK_LONG_PRESS -> {
+ mIndexOfRingtoneToRemove = viewHolder.getAdapterPosition()
+ }
+ RingtoneViewHolder.CLICK_NO_PERMISSIONS -> {
+ ConfirmRemoveCustomRingtoneDialogFragment.show(supportFragmentManager,
+ (viewHolder.itemHolder as RingtoneHolder).uri, false)
+ }
+ }
+ }
+ }
+
+ /**
+ * This task locates a displayable string in the background that is fit for use as the title of
+ * the audio content. It adds a custom ringtone using the uri and title on the main thread.
+ */
+ private inner class AddCustomRingtoneTask(private val mUri: Uri)
+ : AsyncTask<Void?, Void?, String>() {
+ private val mContext: Context = getApplicationContext()
+
+ override fun doInBackground(vararg voids: Void?): String {
+ val contentResolver = mContext.contentResolver
+
+ // Take the long-term permission to read (playback) the audio at the uri.
+ contentResolver
+ .takePersistableUriPermission(mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ try {
+ contentResolver.query(mUri, null, null, null, null).use { cursor ->
+ if (cursor != null && cursor.moveToFirst()) {
+ // If the file was a media file, return its title.
+ val titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)
+ if (titleIndex != -1) {
+ return cursor.getString(titleIndex)
+ }
+
+ // If the file was a simple openable, return its display name.
+ val displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (displayNameIndex != -1) {
+ var title = cursor.getString(displayNameIndex)
+ val dotIndex = title.lastIndexOf(".")
+ if (dotIndex > 0) {
+ title = title.substring(0, dotIndex)
+ }
+ return title
+ }
+ } else {
+ LogUtils.e("No ringtone for uri: %s", mUri)
+ }
+ }
+ } catch (e: Exception) {
+ LogUtils.e("Unable to locate title for custom ringtone: $mUri", e)
+ }
+
+ return mContext.getString(R.string.unknown_ringtone_title)
+ }
+
+ override fun onPostExecute(title: String) {
+ // Add the new custom ringtone to the data model.
+ DataModel.dataModel.addCustomRingtone(mUri, title)
+
+ // When the loader completes, it must play the new ringtone.
+ mSelectedRingtoneUri = mUri
+ mIsPlaying = true
+
+ // Reload the data to reflect the change in the UI.
+ LoaderManager.getInstance(this@RingtonePickerActivity).restartLoader(0 /* id */,
+ null /* args */, this@RingtonePickerActivity /* callback */)
+ }
+ }
+
+ /**
+ * Removes a custom ringtone with the given uri. Taking this action has side-effects because
+ * all alarms that use the custom ringtone are reassigned to the Android system default alarm
+ * ringtone. If the application's default alarm ringtone is being removed, it is reset to the
+ * Android system default alarm ringtone. If the application's timer ringtone is being removed,
+ * it is reset to the application's default timer ringtone.
+ */
+ private inner class RemoveCustomRingtoneTask(private val mRemoveUri: Uri)
+ : AsyncTask<Void?, Void?, Void?>() {
+ private lateinit var mSystemDefaultRingtoneUri: Uri
+
+ override fun doInBackground(vararg voids: Void?): Void? {
+ mSystemDefaultRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
+
+ // Update all alarms that use the custom ringtone to use the system default.
+ val cr: ContentResolver = getContentResolver()
+ val alarms = Alarm.getAlarms(cr, null)
+ for (alarm in alarms) {
+ if (mRemoveUri == alarm.alert) {
+ alarm.alert = mSystemDefaultRingtoneUri
+ // Start a second background task to persist the updated alarm.
+ AlarmUpdateHandler(this@RingtonePickerActivity, null, null)
+ .asyncUpdateAlarm(alarm, popToast = false, minorUpdate = true)
+ }
+ }
+
+ try {
+ // Release the permission to read (playback) the audio at the uri.
+ cr.releasePersistableUriPermission(mRemoveUri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ } catch (ignore: SecurityException) {
+ // If the file was already deleted from the file system, a SecurityException is
+ // thrown indicating this app did not hold the read permission being released.
+ LogUtils.w("SecurityException while releasing read permission for $mRemoveUri")
+ }
+
+ return null
+ }
+
+ override fun onPostExecute(v: Void?) {
+ // Reset the default alarm ringtone if it was just removed.
+ if (mRemoveUri == DataModel.dataModel.defaultAlarmRingtoneUri) {
+ DataModel.dataModel.defaultAlarmRingtoneUri = mSystemDefaultRingtoneUri
+ }
+
+ // Reset the timer ringtone if it was just removed.
+ if (mRemoveUri == DataModel.dataModel.timerRingtoneUri) {
+ val timerRingtoneUri = DataModel.dataModel.defaultTimerRingtoneUri
+ DataModel.dataModel.timerRingtoneUri = timerRingtoneUri
+ }
+
+ // Remove the corresponding custom ringtone.
+ DataModel.dataModel.removeCustomRingtone(mRemoveUri)
+
+ // Find the ringtone to be removed from the adapter.
+ val toRemove = getRingtoneHolder(mRemoveUri) ?: return
+
+ // If the ringtone to remove is also the selected ringtone, adjust the selection.
+ if (toRemove.isSelected) {
+ stopPlayingRingtone(toRemove, false)
+ val defaultRingtone = getRingtoneHolder(mDefaultRingtoneUri)
+ if (defaultRingtone != null) {
+ defaultRingtone.isSelected = true
+ mSelectedRingtoneUri = defaultRingtone.uri
+ defaultRingtone.notifyItemChanged()
+ }
+ }
+
+ // Remove the ringtone from the adapter.
+ mRingtoneAdapter.removeItem(toRemove)
+ }
+ }
+
+ companion object {
+ /** Key to an extra that defines resource id to the title of this activity. */
+ private const val EXTRA_TITLE = "extra_title"
+
+ /** Key to an extra that identifies the alarm to which the selected ringtone is attached. */
+ private const val EXTRA_ALARM_ID = "extra_alarm_id"
+
+ /** Key to an extra that identifies the selected ringtone. */
+ private const val EXTRA_RINGTONE_URI = "extra_ringtone_uri"
+
+ /** Key to an extra that defines the uri representing the default ringtone. */
+ private const val EXTRA_DEFAULT_RINGTONE_URI = "extra_default_ringtone_uri"
+
+ /** Key to an extra that defines the name of the default ringtone. */
+ private const val EXTRA_DEFAULT_RINGTONE_NAME = "extra_default_ringtone_name"
+
+ /** Key to an instance state value indicating if the
+ * selected ringtone is currently playing. */
+ private const val STATE_KEY_PLAYING = "extra_is_playing"
+
+ /**
+ * @return an intent that launches the ringtone picker to edit the ringtone of the given
+ * `alarm`
+ */
+ @JvmStatic
+ @Keep
+ fun createAlarmRingtonePickerIntent(context: Context, alarm: Alarm): Intent {
+ return Intent(context, RingtonePickerActivity::class.java)
+ .putExtra(EXTRA_TITLE, R.string.alarm_sound)
+ .putExtra(EXTRA_ALARM_ID, alarm.id)
+ .putExtra(EXTRA_RINGTONE_URI, alarm.alert)
+ .putExtra(EXTRA_DEFAULT_RINGTONE_URI,
+ RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM))
+ .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_alarm_ringtone_title)
+ }
+
+ /**
+ * @return an intent that launches the ringtone picker to edit the ringtone of all timers
+ */
+ @JvmStatic
+ @Keep
+ fun createTimerRingtonePickerIntent(context: Context): Intent {
+ val dataModel = DataModel.dataModel
+ return Intent(context, RingtonePickerActivity::class.java)
+ .putExtra(EXTRA_TITLE, R.string.timer_sound)
+ .putExtra(EXTRA_RINGTONE_URI, dataModel.timerRingtoneUri)
+ .putExtra(EXTRA_DEFAULT_RINGTONE_URI, dataModel.defaultTimerRingtoneUri)
+ .putExtra(EXTRA_DEFAULT_RINGTONE_NAME, R.string.default_timer_ringtone_title)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/ringtone/RingtoneViewHolder.java b/src/com/android/deskclock/ringtone/RingtoneViewHolder.java
deleted file mode 100644
index 3e0a0b8..0000000
--- a/src/com/android/deskclock/ringtone/RingtoneViewHolder.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.ringtone;
-
-import android.graphics.PorterDuff;
-import androidx.core.content.ContextCompat;
-import android.view.ContextMenu;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.deskclock.AnimatorUtils;
-import com.android.deskclock.ItemAdapter;
-import com.android.deskclock.R;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.Utils;
-
-import static android.view.View.GONE;
-import static android.view.View.OnClickListener;
-import static android.view.View.OnCreateContextMenuListener;
-import static android.view.View.VISIBLE;
-
-final class RingtoneViewHolder extends ItemAdapter.ItemViewHolder<RingtoneHolder>
- implements OnClickListener, OnCreateContextMenuListener {
-
- static final int VIEW_TYPE_SYSTEM_SOUND = R.layout.ringtone_item_sound;
- static final int VIEW_TYPE_CUSTOM_SOUND = -R.layout.ringtone_item_sound;
- static final int CLICK_NORMAL = 0;
- static final int CLICK_LONG_PRESS = -1;
- static final int CLICK_NO_PERMISSIONS = -2;
-
- private final View mSelectedView;
- private final TextView mNameView;
- private final ImageView mImageView;
-
- private RingtoneViewHolder(View itemView) {
- super(itemView);
- itemView.setOnClickListener(this);
-
- mSelectedView = itemView.findViewById(R.id.sound_image_selected);
- mNameView = (TextView) itemView.findViewById(R.id.ringtone_name);
- mImageView = (ImageView) itemView.findViewById(R.id.ringtone_image);
- }
-
- @Override
- protected void onBindItemView(RingtoneHolder itemHolder) {
- mNameView.setText(itemHolder.getName());
- final boolean opaque = itemHolder.isSelected() || !itemHolder.hasPermissions();
- mNameView.setAlpha(opaque ? 1f : .63f);
- mImageView.setAlpha(opaque ? 1f : .63f);
- mImageView.clearColorFilter();
-
- final int itemViewType = getItemViewType();
- if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
- if (!itemHolder.hasPermissions()) {
- mImageView.setImageResource(R.drawable.ic_ringtone_not_found);
- final int colorAccent = ThemeUtils.resolveColor(itemView.getContext(),
- R.attr.colorAccent);
- mImageView.setColorFilter(colorAccent, PorterDuff.Mode.SRC_ATOP);
- } else {
- mImageView.setImageResource(R.drawable.placeholder_album_artwork);
- }
- } else if (itemHolder.item == Utils.RINGTONE_SILENT) {
- mImageView.setImageResource(R.drawable.ic_ringtone_silent);
- } else if (itemHolder.isPlaying()) {
- mImageView.setImageResource(R.drawable.ic_ringtone_active);
- } else {
- mImageView.setImageResource(R.drawable.ic_ringtone);
- }
- AnimatorUtils.startDrawableAnimation(mImageView);
-
- mSelectedView.setVisibility(itemHolder.isSelected() ? VISIBLE : GONE);
-
- final int bgColorId = itemHolder.isSelected() ? R.color.white_08p : R.color.transparent;
- itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), bgColorId));
-
- if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
- itemView.setOnCreateContextMenuListener(this);
- }
- }
-
- @Override
- public void onClick(View view) {
- if (getItemHolder().hasPermissions()) {
- notifyItemClicked(RingtoneViewHolder.CLICK_NORMAL);
- } else {
- notifyItemClicked(RingtoneViewHolder.CLICK_NO_PERMISSIONS);
- }
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu contextMenu, View view,
- ContextMenu.ContextMenuInfo contextMenuInfo) {
- notifyItemClicked(RingtoneViewHolder.CLICK_LONG_PRESS);
- contextMenu.add(Menu.NONE, 0, Menu.NONE, R.string.remove_sound);
- }
-
- public static class Factory implements ItemAdapter.ItemViewHolder.Factory {
-
- private final LayoutInflater mInflater;
-
- Factory(LayoutInflater inflater) {
- mInflater = inflater;
- }
-
- @Override
- public ItemAdapter.ItemViewHolder<?> createViewHolder(ViewGroup parent, int viewType) {
- final View itemView = mInflater.inflate(R.layout.ringtone_item_sound, parent, false);
- return new RingtoneViewHolder(itemView);
- }
- }
-}
diff --git a/src/com/android/deskclock/ringtone/RingtoneViewHolder.kt b/src/com/android/deskclock/ringtone/RingtoneViewHolder.kt
new file mode 100644
index 0000000..86c3fc7
--- /dev/null
+++ b/src/com/android/deskclock/ringtone/RingtoneViewHolder.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone
+
+import android.graphics.PorterDuff
+import android.view.ContextMenu
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.View
+import android.view.ViewGroup
+import android.view.ContextMenu.ContextMenuInfo
+import android.view.View.OnCreateContextMenuListener
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.ItemAdapter.ItemViewHolder
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+
+internal class RingtoneViewHolder private constructor(itemView: View)
+ : ItemViewHolder<RingtoneHolder>(itemView), View.OnClickListener, OnCreateContextMenuListener {
+ private val mSelectedView: View = itemView.findViewById(R.id.sound_image_selected)
+ private val mNameView: TextView = itemView.findViewById<View>(R.id.ringtone_name) as TextView
+ private val mImageView: ImageView =
+ itemView.findViewById<View>(R.id.ringtone_image) as ImageView
+
+ init {
+ itemView.setOnClickListener(this)
+ }
+
+ override fun onBindItemView(itemHolder: RingtoneHolder) {
+ mNameView.text = itemHolder.name
+ val opaque = itemHolder.isSelected || !itemHolder.hasPermissions()
+ mNameView.alpha = if (opaque) 1f else .63f
+ mImageView.alpha = if (opaque) 1f else .63f
+ mImageView.clearColorFilter()
+
+ val itemViewType: Int = getItemViewType()
+ if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
+ if (!itemHolder.hasPermissions()) {
+ mImageView.setImageResource(R.drawable.ic_ringtone_not_found)
+ val colorAccent = ThemeUtils.resolveColor(itemView.getContext(),
+ R.attr.colorAccent)
+ mImageView.setColorFilter(colorAccent, PorterDuff.Mode.SRC_ATOP)
+ } else {
+ mImageView.setImageResource(R.drawable.placeholder_album_artwork)
+ }
+ } else if (itemHolder.item == Utils.RINGTONE_SILENT) {
+ mImageView.setImageResource(R.drawable.ic_ringtone_silent)
+ } else if (itemHolder.isPlaying) {
+ mImageView.setImageResource(R.drawable.ic_ringtone_active)
+ } else {
+ mImageView.setImageResource(R.drawable.ic_ringtone)
+ }
+ AnimatorUtils.startDrawableAnimation(mImageView)
+
+ mSelectedView.visibility = if (itemHolder.isSelected) View.VISIBLE else View.GONE
+
+ val bgColorId = if (itemHolder.isSelected) R.color.white_08p else R.color.transparent
+ itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), bgColorId))
+
+ if (itemViewType == VIEW_TYPE_CUSTOM_SOUND) {
+ itemView.setOnCreateContextMenuListener(this)
+ }
+ }
+
+ override fun onClick(view: View) {
+ if (itemHolder!!.hasPermissions()) {
+ notifyItemClicked(CLICK_NORMAL)
+ } else {
+ notifyItemClicked(CLICK_NO_PERMISSIONS)
+ }
+ }
+
+ override fun onCreateContextMenu(
+ contextMenu: ContextMenu,
+ view: View,
+ contextMenuInfo: ContextMenuInfo
+ ) {
+ notifyItemClicked(CLICK_LONG_PRESS)
+ contextMenu.add(Menu.NONE, 0, Menu.NONE, R.string.remove_sound)
+ }
+
+ class Factory internal constructor(private val mInflater: LayoutInflater)
+ : ItemViewHolder.Factory {
+ override fun createViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder<*> {
+ val itemView = mInflater.inflate(R.layout.ringtone_item_sound, parent, false)
+ return RingtoneViewHolder(itemView)
+ }
+ }
+
+ companion object {
+ const val VIEW_TYPE_SYSTEM_SOUND = R.layout.ringtone_item_sound
+ const val VIEW_TYPE_CUSTOM_SOUND = -R.layout.ringtone_item_sound
+ const val CLICK_NORMAL = 0
+ const val CLICK_LONG_PRESS = -1
+ const val CLICK_NO_PERMISSIONS = -2
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityListener.java b/src/com/android/deskclock/ringtone/SystemRingtoneHolder.kt
similarity index 63%
copy from src/com/android/deskclock/data/CityListener.java
copy to src/com/android/deskclock/ringtone/SystemRingtoneHolder.kt
index 91f66b3..73be7d7 100644
--- a/src/com/android/deskclock/data/CityListener.java
+++ b/src/com/android/deskclock/ringtone/SystemRingtoneHolder.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.deskclock.data;
+package com.android.deskclock.ringtone
-import java.util.List;
+import android.net.Uri
-/**
- * The interface through which interested parties are notified of changes to the world cities list.
- */
-public interface CityListener {
- void citiesChanged(List<City> oldCities, List<City> newCities);
+internal class SystemRingtoneHolder(uri: Uri, name: String?) : RingtoneHolder(uri, name) {
+ override fun getItemViewType(): Int {
+ return RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND
+ }
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/AlarmVolumePreference.java b/src/com/android/deskclock/settings/AlarmVolumePreference.java
deleted file mode 100644
index b1eb8ce..0000000
--- a/src/com/android/deskclock/settings/AlarmVolumePreference.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.settings;
-
-import android.annotation.TargetApi;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.media.AudioManager;
-import android.os.Build;
-import android.provider.Settings;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.SeekBar;
-
-import com.android.deskclock.R;
-import com.android.deskclock.RingtonePreviewKlaxon;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-import static android.content.Context.AUDIO_SERVICE;
-import static android.content.Context.NOTIFICATION_SERVICE;
-import static android.media.AudioManager.STREAM_ALARM;
-
-public class AlarmVolumePreference extends Preference {
-
- private static final long ALARM_PREVIEW_DURATION_MS = 2000;
-
- private SeekBar mSeekbar;
- private ImageView mAlarmIcon;
- private boolean mPreviewPlaying;
-
- public AlarmVolumePreference(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public void onBindViewHolder(PreferenceViewHolder holder) {
- super.onBindViewHolder(holder);
-
- final Context context = getContext();
- final AudioManager audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
-
- // Disable click feedback for this preference.
- holder.itemView.setClickable(false);
-
- mSeekbar = (SeekBar) holder.findViewById(R.id.alarm_volume_slider);
- mSeekbar.setMax(audioManager.getStreamMaxVolume(STREAM_ALARM));
- mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM));
- mAlarmIcon = (ImageView) holder.findViewById(R.id.alarm_icon);
- onSeekbarChanged();
-
- final ContentObserver volumeObserver = new ContentObserver(mSeekbar.getHandler()) {
- @Override
- public void onChange(boolean selfChange) {
- // Volume was changed elsewhere, update our slider.
- mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM));
- }
- };
-
- mSeekbar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- context.getContentResolver().registerContentObserver(Settings.System.CONTENT_URI,
- true, volumeObserver);
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- context.getContentResolver().unregisterContentObserver(volumeObserver);
- }
- });
-
- mSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
- @Override
- public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
- if (fromUser) {
- audioManager.setStreamVolume(STREAM_ALARM, progress, 0);
- }
- onSeekbarChanged();
- }
-
- @Override
- public void onStartTrackingTouch(SeekBar seekBar) {
- }
-
- @Override
- public void onStopTrackingTouch(SeekBar seekBar) {
- if (!mPreviewPlaying && seekBar.getProgress() != 0) {
- // If we are not currently playing and progress is set to non-zero, start.
- RingtonePreviewKlaxon.start(
- context, DataModel.getDataModel().getDefaultAlarmRingtoneUri());
- mPreviewPlaying = true;
- seekBar.postDelayed(new Runnable() {
- @Override
- public void run() {
- RingtonePreviewKlaxon.stop(context);
- mPreviewPlaying = false;
- }
- }, ALARM_PREVIEW_DURATION_MS);
- }
- }
- });
- }
-
- private void onSeekbarChanged() {
- mSeekbar.setEnabled(doesDoNotDisturbAllowAlarmPlayback());
- mAlarmIcon.setImageResource(mSeekbar.getProgress() == 0 ?
- R.drawable.ic_alarm_off_24dp : R.drawable.ic_alarm_small);
- }
-
- private boolean doesDoNotDisturbAllowAlarmPlayback() {
- return !Utils.isNOrLater() || doesDoNotDisturbAllowAlarmPlaybackNPlus();
- }
-
- @TargetApi(Build.VERSION_CODES.N)
- private boolean doesDoNotDisturbAllowAlarmPlaybackNPlus() {
- final NotificationManager notificationManager = (NotificationManager)
- getContext().getSystemService(NOTIFICATION_SERVICE);
- return notificationManager.getCurrentInterruptionFilter() !=
- NotificationManager.INTERRUPTION_FILTER_NONE;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/AlarmVolumePreference.kt b/src/com/android/deskclock/settings/AlarmVolumePreference.kt
new file mode 100644
index 0000000..554c384
--- /dev/null
+++ b/src/com/android/deskclock/settings/AlarmVolumePreference.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.settings
+
+import android.annotation.TargetApi
+import android.app.NotificationManager
+import android.content.Context
+import android.content.Context.AUDIO_SERVICE
+import android.content.Context.NOTIFICATION_SERVICE
+import android.database.ContentObserver
+import android.media.AudioManager
+import android.media.AudioManager.STREAM_ALARM
+import android.os.Build
+import android.provider.Settings
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.SeekBar
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+
+import com.android.deskclock.R
+import com.android.deskclock.RingtonePreviewKlaxon
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+class AlarmVolumePreference(context: Context?, attrs: AttributeSet?) : Preference(context, attrs) {
+ private lateinit var mSeekbar: SeekBar
+ private lateinit var mAlarmIcon: ImageView
+
+ private var mPreviewPlaying = false
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ val context: Context = getContext()
+ val audioManager: AudioManager = context.getSystemService(AUDIO_SERVICE) as AudioManager
+
+ // Disable click feedback for this preference.
+ holder.itemView.setClickable(false)
+ mSeekbar = holder.findViewById(R.id.alarm_volume_slider) as SeekBar
+ mSeekbar.setMax(audioManager.getStreamMaxVolume(STREAM_ALARM))
+ mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM))
+ mAlarmIcon = holder.findViewById(R.id.alarm_icon) as ImageView
+ onSeekbarChanged()
+
+ val volumeObserver: ContentObserver = object : ContentObserver(mSeekbar.getHandler()) {
+ override fun onChange(selfChange: Boolean) {
+ // Volume was changed elsewhere, update our slider.
+ mSeekbar.setProgress(audioManager.getStreamVolume(STREAM_ALARM))
+ }
+ }
+
+ mSeekbar.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View?) {
+ context.getContentResolver().registerContentObserver(Settings.System.CONTENT_URI,
+ true, volumeObserver)
+ }
+
+ override fun onViewDetachedFromWindow(v: View?) {
+ context.getContentResolver().unregisterContentObserver(volumeObserver)
+ }
+ })
+
+ mSeekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ audioManager.setStreamVolume(STREAM_ALARM, progress, 0)
+ }
+ onSeekbarChanged()
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar?) {
+ }
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {
+ if (!mPreviewPlaying && seekBar.getProgress() != 0) {
+ // If we are not currently playing and progress is set to non-zero, start.
+ RingtonePreviewKlaxon
+ .start(context, DataModel.dataModel.defaultAlarmRingtoneUri)
+ mPreviewPlaying = true
+ seekBar.postDelayed(Runnable {
+ RingtonePreviewKlaxon.stop(context)
+ mPreviewPlaying = false
+ }, ALARM_PREVIEW_DURATION_MS)
+ }
+ }
+ })
+ }
+
+ private fun onSeekbarChanged() {
+ mSeekbar.setEnabled(doesDoNotDisturbAllowAlarmPlayback())
+ val imageRes = if (mSeekbar.getProgress() == 0) {
+ R.drawable.ic_alarm_off_24dp
+ } else {
+ R.drawable.ic_alarm_small
+ }
+ mAlarmIcon.setImageResource(imageRes)
+ }
+
+ private fun doesDoNotDisturbAllowAlarmPlayback(): Boolean {
+ return !Utils.isNOrLater || doesDoNotDisturbAllowAlarmPlaybackNPlus()
+ }
+
+ @TargetApi(Build.VERSION_CODES.N)
+ private fun doesDoNotDisturbAllowAlarmPlaybackNPlus(): Boolean {
+ val notificationManager =
+ getContext().getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ return notificationManager.getCurrentInterruptionFilter() !=
+ NotificationManager.INTERRUPTION_FILTER_NONE
+ }
+
+ companion object {
+ private const val ALARM_PREVIEW_DURATION_MS: Long = 2000
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/ScreensaverSettingsActivity.java b/src/com/android/deskclock/settings/ScreensaverSettingsActivity.java
deleted file mode 100644
index edc0554..0000000
--- a/src/com/android/deskclock/settings/ScreensaverSettingsActivity.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.settings;
-
-import android.annotation.TargetApi;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceFragment;
-import androidx.appcompat.app.AppCompatActivity;
-import android.view.MenuItem;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-/**
- * Settings for Clock screen saver
- */
-public final class ScreensaverSettingsActivity extends AppCompatActivity {
-
- public static final String KEY_CLOCK_STYLE = "screensaver_clock_style";
- public static final String KEY_NIGHT_MODE = "screensaver_night_mode";
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.screensaver_settings);
- }
-
- @Override
- public boolean onOptionsItemSelected (MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- finish();
- return true;
- default:
- break;
- }
- return super.onOptionsItemSelected(item);
- }
-
-
- public static class PrefsFragment extends PreferenceFragment
- implements Preference.OnPreferenceChangeListener {
-
- @Override
- @TargetApi(Build.VERSION_CODES.N)
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (Utils.isNOrLater()) {
- getPreferenceManager().setStorageDeviceProtected();
- }
- addPreferencesFromResource(R.xml.screensaver_settings);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- refresh();
- }
-
- @Override
- public boolean onPreferenceChange(Preference pref, Object newValue) {
- if (KEY_CLOCK_STYLE.equals(pref.getKey())) {
- final ListPreference clockStylePref = (ListPreference) pref;
- final int index = clockStylePref.findIndexOfValue((String) newValue);
- clockStylePref.setSummary(clockStylePref.getEntries()[index]);
- }
- return true;
- }
-
- private void refresh() {
- final ListPreference clockStylePref = (ListPreference) findPreference(KEY_CLOCK_STYLE);
- clockStylePref.setSummary(clockStylePref.getEntry());
- clockStylePref.setOnPreferenceChangeListener(this);
- }
- }
-}
diff --git a/src/com/android/deskclock/settings/ScreensaverSettingsActivity.kt b/src/com/android/deskclock/settings/ScreensaverSettingsActivity.kt
new file mode 100644
index 0000000..3199b7c
--- /dev/null
+++ b/src/com/android/deskclock/settings/ScreensaverSettingsActivity.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.settings
+
+import android.annotation.TargetApi
+import android.os.Build
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.appcompat.app.AppCompatActivity
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+/**
+ * Settings for Clock screen saver
+ */
+class ScreensaverSettingsActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.screensaver_settings)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.getItemId()) {
+ android.R.id.home -> {
+ finish()
+ return true
+ }
+ else -> {
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ class PrefsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
+
+ @TargetApi(Build.VERSION_CODES.N)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (Utils.isNOrLater) {
+ getPreferenceManager().setStorageDeviceProtected()
+ }
+ }
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String) {
+ addPreferencesFromResource(R.xml.screensaver_settings)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ refresh()
+ }
+
+ override fun onPreferenceChange(pref: Preference, newValue: Any?): Boolean {
+ if (KEY_CLOCK_STYLE == pref.getKey()) {
+ val clockStylePref: ListPreference = pref as ListPreference
+ val index: Int = clockStylePref.findIndexOfValue(newValue as String?)
+ clockStylePref.setSummary(clockStylePref.getEntries().get(index))
+ }
+ return true
+ }
+
+ private fun refresh() {
+ val clockStylePref = findPreference<ListPreference>(KEY_CLOCK_STYLE) as ListPreference
+ clockStylePref.setSummary(clockStylePref.getEntry())
+ clockStylePref.setOnPreferenceChangeListener(this)
+ }
+ }
+
+ companion object {
+ const val KEY_CLOCK_STYLE = "screensaver_clock_style"
+ const val KEY_NIGHT_MODE = "screensaver_night_mode"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/SettingsActivity.java b/src/com/android/deskclock/settings/SettingsActivity.java
deleted file mode 100644
index dcb5707..0000000
--- a/src/com/android/deskclock/settings/SettingsActivity.java
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.settings;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Vibrator;
-import android.provider.Settings;
-import androidx.preference.ListPreference;
-import androidx.preference.ListPreferenceDialogFragmentCompat;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceDialogFragmentCompat;
-import androidx.preference.PreferenceFragmentCompat;
-import androidx.preference.TwoStatePreference;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-
-import com.android.deskclock.BaseActivity;
-import com.android.deskclock.DropShadowController;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
-import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
-import com.android.deskclock.actionbarmenu.OptionsMenuManager;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.TimeZones;
-import com.android.deskclock.data.Weekdays;
-import com.android.deskclock.ringtone.RingtonePickerActivity;
-
-/**
- * Settings for the Alarm Clock.
- */
-public final class SettingsActivity extends BaseActivity {
-
- public static final String KEY_ALARM_SNOOZE = "snooze_duration";
- public static final String KEY_ALARM_CRESCENDO = "alarm_crescendo_duration";
- public static final String KEY_TIMER_CRESCENDO = "timer_crescendo_duration";
- public static final String KEY_TIMER_RINGTONE = "timer_ringtone";
- public static final String KEY_TIMER_VIBRATE = "timer_vibrate";
- public static final String KEY_AUTO_SILENCE = "auto_silence";
- public static final String KEY_CLOCK_STYLE = "clock_style";
- public static final String KEY_CLOCK_DISPLAY_SECONDS = "display_clock_seconds";
- public static final String KEY_HOME_TZ = "home_time_zone";
- public static final String KEY_AUTO_HOME_CLOCK = "automatic_home_clock";
- public static final String KEY_DATE_TIME = "date_time";
- public static final String KEY_VOLUME_BUTTONS = "volume_button_setting";
- public static final String KEY_WEEK_START = "week_start";
-
- public static final String DEFAULT_VOLUME_BEHAVIOR = "0";
- public static final String VOLUME_BEHAVIOR_SNOOZE = "1";
- public static final String VOLUME_BEHAVIOR_DISMISS = "2";
-
- public static final String PREFS_FRAGMENT_TAG = "prefs_fragment";
- public static final String PREFERENCE_DIALOG_FRAGMENT_TAG = "preference_dialog";
-
- private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
-
- /**
- * The controller that shows the drop shadow when content is not scrolled to the top.
- */
- private DropShadowController mDropShadowController;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.settings);
-
- mOptionsMenuManager.addMenuItemController(new NavUpMenuItemController(this))
- .addMenuItemController(MenuItemControllerFactory.getInstance()
- .buildMenuItemControllers(this));
-
- // Create the prefs fragment in code to ensure it's created before PreferenceDialogFragment
- if (savedInstanceState == null) {
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.main, new PrefsFragment(), PREFS_FRAGMENT_TAG)
- .disallowAddToBackStack()
- .commit();
- }
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- final View dropShadow = findViewById(R.id.drop_shadow);
- final PrefsFragment fragment =
- (PrefsFragment) getSupportFragmentManager().findFragmentById(R.id.main);
- mDropShadowController = new DropShadowController(dropShadow, fragment.getListView());
- }
-
- @Override
- protected void onPause() {
- mDropShadowController.stop();
- super.onPause();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- mOptionsMenuManager.onCreateOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- mOptionsMenuManager.onPrepareOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- return mOptionsMenuManager.onOptionsItemSelected(item)
- || super.onOptionsItemSelected(item);
- }
-
- public static class PrefsFragment extends PreferenceFragmentCompat implements
- Preference.OnPreferenceChangeListener,
- Preference.OnPreferenceClickListener {
-
- @Override
- public void onCreatePreferences(Bundle bundle, String rootKey) {
- getPreferenceManager().setStorageDeviceProtected();
- addPreferencesFromResource(R.xml.settings);
- final Preference timerVibrate = findPreference(KEY_TIMER_VIBRATE);
- final boolean hasVibrator = ((Vibrator) timerVibrate.getContext()
- .getSystemService(VIBRATOR_SERVICE)).hasVibrator();
- timerVibrate.setVisible(hasVibrator);
- loadTimeZoneList();
- }
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- // By default, do not recreate the DeskClock activity
- getActivity().setResult(RESULT_CANCELED);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- refresh();
- }
-
- @Override
- public boolean onPreferenceChange(Preference pref, Object newValue) {
- switch (pref.getKey()) {
- case KEY_ALARM_CRESCENDO:
- case KEY_HOME_TZ:
- case KEY_ALARM_SNOOZE:
- case KEY_TIMER_CRESCENDO:
- final ListPreference preference = (ListPreference) pref;
- final int index = preference.findIndexOfValue((String) newValue);
- preference.setSummary(preference.getEntries()[index]);
- break;
- case KEY_CLOCK_STYLE:
- case KEY_WEEK_START:
- case KEY_VOLUME_BUTTONS:
- final SimpleMenuPreference simpleMenuPreference = (SimpleMenuPreference) pref;
- final int i = simpleMenuPreference.findIndexOfValue((String) newValue);
- pref.setSummary(simpleMenuPreference.getEntries()[i]);
- break;
- case KEY_CLOCK_DISPLAY_SECONDS:
- DataModel.getDataModel().setDisplayClockSeconds((boolean) newValue);
- break;
- case KEY_AUTO_SILENCE:
- final String delay = (String) newValue;
- updateAutoSnoozeSummary((ListPreference) pref, delay);
- break;
- case KEY_AUTO_HOME_CLOCK:
- final boolean autoHomeClockEnabled = ((TwoStatePreference) pref).isChecked();
- final Preference homeTimeZonePref = findPreference(KEY_HOME_TZ);
- homeTimeZonePref.setEnabled(!autoHomeClockEnabled);
- break;
- case KEY_TIMER_VIBRATE:
- final TwoStatePreference timerVibratePref = (TwoStatePreference) pref;
- DataModel.getDataModel().setTimerVibrate(timerVibratePref.isChecked());
- break;
- case KEY_TIMER_RINGTONE:
- pref.setSummary(DataModel.getDataModel().getTimerRingtoneTitle());
- break;
- }
- // Set result so DeskClock knows to refresh itself
- getActivity().setResult(RESULT_OK);
- return true;
- }
-
- @Override
- public boolean onPreferenceClick(Preference pref) {
- final Context context = getActivity();
- if (context == null) {
- return false;
- }
-
- switch (pref.getKey()) {
- case KEY_DATE_TIME:
- final Intent dialogIntent = new Intent(Settings.ACTION_DATE_SETTINGS);
- dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(dialogIntent);
- return true;
- case KEY_TIMER_RINGTONE:
- startActivity(RingtonePickerActivity.createTimerRingtonePickerIntent(context));
- return true;
- }
-
- return false;
- }
-
- @Override
- public void onDisplayPreferenceDialog(Preference preference) {
- // Only single-selection lists are currently supported.
- final PreferenceDialogFragmentCompat f;
- if (preference instanceof ListPreference) {
- f = ListPreferenceDialogFragmentCompat.newInstance(preference.getKey());
- } else {
- throw new IllegalArgumentException("Unsupported DialogPreference type");
- }
- showDialog(f);
- }
-
- private void showDialog(PreferenceDialogFragmentCompat fragment) {
- // Don't show dialog if one is already shown.
- if (getFragmentManager().findFragmentByTag(PREFERENCE_DIALOG_FRAGMENT_TAG) != null) {
- return;
- }
- // Always set the target fragment, this is required by PreferenceDialogFragment
- // internally.
- fragment.setTargetFragment(this, 0);
- // Don't use getChildFragmentManager(), it causes issues on older platforms when the
- // target fragment is being restored after an orientation change.
- fragment.show(getFragmentManager(), PREFERENCE_DIALOG_FRAGMENT_TAG);
- }
-
- /**
- * Reconstruct the timezone list.
- */
- private void loadTimeZoneList() {
- final TimeZones timezones = DataModel.getDataModel().getTimeZones();
- final ListPreference homeTimezonePref = (ListPreference) findPreference(KEY_HOME_TZ);
- homeTimezonePref.setEntryValues(timezones.getTimeZoneIds());
- homeTimezonePref.setEntries(timezones.getTimeZoneNames());
- homeTimezonePref.setSummary(homeTimezonePref.getEntry());
- homeTimezonePref.setOnPreferenceChangeListener(this);
- }
-
- private void refresh() {
- final ListPreference autoSilencePref =
- (ListPreference) findPreference(KEY_AUTO_SILENCE);
- String delay = autoSilencePref.getValue();
- updateAutoSnoozeSummary(autoSilencePref, delay);
- autoSilencePref.setOnPreferenceChangeListener(this);
-
- final SimpleMenuPreference clockStylePref = (SimpleMenuPreference)
- findPreference(KEY_CLOCK_STYLE);
- clockStylePref.setSummary(clockStylePref.getEntry());
- clockStylePref.setOnPreferenceChangeListener(this);
-
- final SimpleMenuPreference volumeButtonsPref = (SimpleMenuPreference)
- findPreference(KEY_VOLUME_BUTTONS);
- volumeButtonsPref.setSummary(volumeButtonsPref.getEntry());
- volumeButtonsPref.setOnPreferenceChangeListener(this);
-
- final Preference clockSecondsPref = findPreference(KEY_CLOCK_DISPLAY_SECONDS);
- clockSecondsPref.setOnPreferenceChangeListener(this);
-
- final Preference autoHomeClockPref = findPreference(KEY_AUTO_HOME_CLOCK);
- final boolean autoHomeClockEnabled =
- ((TwoStatePreference) autoHomeClockPref).isChecked();
- autoHomeClockPref.setOnPreferenceChangeListener(this);
-
- final ListPreference homeTimezonePref = (ListPreference) findPreference(KEY_HOME_TZ);
- homeTimezonePref.setEnabled(autoHomeClockEnabled);
- refreshListPreference(homeTimezonePref);
-
- refreshListPreference((ListPreference) findPreference(KEY_ALARM_CRESCENDO));
- refreshListPreference((ListPreference) findPreference(KEY_TIMER_CRESCENDO));
- refreshListPreference((ListPreference) findPreference(KEY_ALARM_SNOOZE));
-
- final Preference dateAndTimeSetting = findPreference(KEY_DATE_TIME);
- dateAndTimeSetting.setOnPreferenceClickListener(this);
-
- final SimpleMenuPreference weekStartPref = (SimpleMenuPreference)
- findPreference(KEY_WEEK_START);
- // Set the default value programmatically
- final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
- final Integer firstDay = weekdayOrder.getCalendarDays().get(0);
- final String value = String.valueOf(firstDay);
- final int idx = weekStartPref.findIndexOfValue(value);
- weekStartPref.setValueIndex(idx);
- weekStartPref.setSummary(weekStartPref.getEntries()[idx]);
- weekStartPref.setOnPreferenceChangeListener(this);
-
- final Preference timerRingtonePref = findPreference(KEY_TIMER_RINGTONE);
- timerRingtonePref.setOnPreferenceClickListener(this);
- timerRingtonePref.setSummary(DataModel.getDataModel().getTimerRingtoneTitle());
- }
-
- private void refreshListPreference(ListPreference preference) {
- preference.setSummary(preference.getEntry());
- preference.setOnPreferenceChangeListener(this);
- }
-
- private void updateAutoSnoozeSummary(ListPreference listPref, String delay) {
- int i = Integer.parseInt(delay);
- if (i == -1) {
- listPref.setSummary(R.string.auto_silence_never);
- } else {
- listPref.setSummary(Utils.getNumberFormattedQuantityString(getActivity(),
- R.plurals.auto_silence_summary, i));
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/SettingsActivity.kt b/src/com/android/deskclock/settings/SettingsActivity.kt
new file mode 100644
index 0000000..c5bccb0
--- /dev/null
+++ b/src/com/android/deskclock/settings/SettingsActivity.kt
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.settings
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.Vibrator
+import android.provider.Settings
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import androidx.preference.ListPreference
+import androidx.preference.ListPreferenceDialogFragmentCompat
+import androidx.preference.Preference
+import androidx.preference.PreferenceDialogFragmentCompat
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.TwoStatePreference
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.DropShadowController
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory
+import com.android.deskclock.actionbarmenu.NavUpMenuItemController
+import com.android.deskclock.actionbarmenu.OptionsMenuManager
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.ringtone.RingtonePickerActivity
+
+/**
+ * Settings for the Alarm Clock.
+ */
+class SettingsActivity : BaseActivity() {
+ private val mOptionsMenuManager = OptionsMenuManager()
+
+ /**
+ * The controller that shows the drop shadow when content is not scrolled to the top.
+ */
+ private lateinit var mDropShadowController: DropShadowController
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.settings)
+
+ mOptionsMenuManager.addMenuItemController(NavUpMenuItemController(this))
+ .addMenuItemController(*MenuItemControllerFactory.buildMenuItemControllers(this))
+
+ // Create the prefs fragment in code to ensure it's created before PreferenceDialogFragment
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.main, PrefsFragment(), PREFS_FRAGMENT_TAG)
+ .disallowAddToBackStack()
+ .commit()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ val dropShadow: View = findViewById(R.id.drop_shadow)
+ val fragment = getSupportFragmentManager().findFragmentById(R.id.main) as PrefsFragment
+ mDropShadowController = DropShadowController(dropShadow, fragment.getListView())
+ }
+
+ override fun onPause() {
+ mDropShadowController.stop()
+ super.onPause()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ mOptionsMenuManager.onCreateOptionsMenu(menu)
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ mOptionsMenuManager.onPrepareOptionsMenu(menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return (mOptionsMenuManager.onOptionsItemSelected(item) ||
+ super.onOptionsItemSelected(item))
+ }
+
+ class PrefsFragment :
+ PreferenceFragmentCompat(),
+ Preference.OnPreferenceChangeListener,
+ Preference.OnPreferenceClickListener {
+
+ override fun onCreatePreferences(bundle: Bundle?, rootKey: String?) {
+ getPreferenceManager().setStorageDeviceProtected()
+ addPreferencesFromResource(R.xml.settings)
+ val timerVibrate: Preference? = findPreference(KEY_TIMER_VIBRATE)
+ timerVibrate?.let {
+ val hasVibrator: Boolean = (it.getContext()
+ .getSystemService(VIBRATOR_SERVICE) as Vibrator).hasVibrator()
+ it.setVisible(hasVibrator)
+ }
+ loadTimeZoneList()
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ // By default, do not recreate the DeskClock activity
+ getActivity()?.setResult(RESULT_CANCELED)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ refresh()
+ }
+
+ override fun onPreferenceChange(pref: Preference, newValue: Any): Boolean {
+ when (pref.getKey()) {
+ KEY_ALARM_CRESCENDO, KEY_HOME_TZ, KEY_ALARM_SNOOZE, KEY_TIMER_CRESCENDO -> {
+ val preference: ListPreference = pref as ListPreference
+ val index: Int = preference.findIndexOfValue(newValue as String)
+ preference.setSummary(preference.getEntries().get(index))
+ }
+ KEY_CLOCK_STYLE, KEY_WEEK_START, KEY_VOLUME_BUTTONS -> {
+ val simpleMenuPreference = pref as SimpleMenuPreference
+ val i: Int = simpleMenuPreference.findIndexOfValue(newValue as String)
+ pref.setSummary(simpleMenuPreference.getEntries().get(i))
+ }
+ KEY_CLOCK_DISPLAY_SECONDS -> {
+ DataModel.dataModel.displayClockSeconds = newValue as Boolean
+ }
+ KEY_AUTO_SILENCE -> {
+ val delay = newValue as String
+ updateAutoSnoozeSummary(pref as ListPreference, delay)
+ }
+ KEY_AUTO_HOME_CLOCK -> {
+ val autoHomeClockEnabled: Boolean = (pref as TwoStatePreference).isChecked()
+ val homeTimeZonePref: Preference? = findPreference(KEY_HOME_TZ)
+ homeTimeZonePref?.setEnabled(!autoHomeClockEnabled)
+ }
+ KEY_TIMER_VIBRATE -> {
+ val timerVibratePref: TwoStatePreference = pref as TwoStatePreference
+ DataModel.dataModel.timerVibrate = timerVibratePref.isChecked()
+ }
+ KEY_TIMER_RINGTONE -> pref.setSummary(DataModel.dataModel.timerRingtoneTitle)
+ }
+
+ // Set result so DeskClock knows to refresh itself
+ getActivity()?.setResult(RESULT_OK)
+ return true
+ }
+
+ override fun onPreferenceClick(pref: Preference): Boolean {
+ val context: Context = getActivity() ?: return false
+
+ when (pref.getKey()) {
+ KEY_DATE_TIME -> {
+ val dialogIntent = Intent(Settings.ACTION_DATE_SETTINGS)
+ dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(dialogIntent)
+ return true
+ }
+ KEY_TIMER_RINGTONE -> {
+ startActivity(RingtonePickerActivity.createTimerRingtonePickerIntent(context))
+ return true
+ }
+ else -> return false
+ }
+ }
+
+ override fun onDisplayPreferenceDialog(preference: Preference) {
+ // Only single-selection lists are currently supported.
+ val f: PreferenceDialogFragmentCompat
+ f = if (preference is ListPreference) {
+ ListPreferenceDialogFragmentCompat.newInstance(preference.getKey())
+ } else {
+ throw IllegalArgumentException("Unsupported DialogPreference type")
+ }
+ showDialog(f)
+ }
+
+ private fun showDialog(fragment: PreferenceDialogFragmentCompat) {
+ // Don't show dialog if one is already shown.
+ if (parentFragmentManager.findFragmentByTag(PREFERENCE_DIALOG_FRAGMENT_TAG) != null) {
+ return
+ }
+ // Always set the target fragment, this is required by PreferenceDialogFragment
+ // internally.
+ fragment.setTargetFragment(this, 0)
+ // Don't use getChildFragmentManager(), it causes issues on older platforms when the
+ // target fragment is being restored after an orientation change.
+ fragment.show(parentFragmentManager, PREFERENCE_DIALOG_FRAGMENT_TAG)
+ }
+
+ /**
+ * Reconstruct the timezone list.
+ */
+ private fun loadTimeZoneList() {
+ val timezones = DataModel.dataModel.timeZones
+ val homeTimezonePref: ListPreference? = findPreference(KEY_HOME_TZ)
+ homeTimezonePref?.let {
+ it.setEntryValues(timezones.timeZoneIds)
+ it.setEntries(timezones.timeZoneNames)
+ it.setSummary(homeTimezonePref.getEntry())
+ it.setOnPreferenceChangeListener(this)
+ }
+ }
+
+ private fun refresh() {
+ val autoSilencePref: ListPreference? = findPreference(KEY_AUTO_SILENCE)
+ autoSilencePref?.let {
+ val delay: String = it.getValue()
+ updateAutoSnoozeSummary(it, delay)
+ it.setOnPreferenceChangeListener(this)
+ }
+
+ val clockStylePref: SimpleMenuPreference? = findPreference(KEY_CLOCK_STYLE)
+ clockStylePref?.let {
+ it.setSummary(it.getEntry())
+ it.setOnPreferenceChangeListener(this)
+ }
+
+ val volumeButtonsPref: SimpleMenuPreference? = findPreference(KEY_VOLUME_BUTTONS)
+ volumeButtonsPref?.let {
+ it.setSummary(volumeButtonsPref.getEntry())
+ it.setOnPreferenceChangeListener(this)
+ }
+
+ val clockSecondsPref: Preference? = findPreference(KEY_CLOCK_DISPLAY_SECONDS)
+ clockSecondsPref?.setOnPreferenceChangeListener(this)
+
+ val autoHomeClockPref: Preference? = findPreference(KEY_AUTO_HOME_CLOCK)
+ val autoHomeClockEnabled: Boolean =
+ (autoHomeClockPref as TwoStatePreference).isChecked()
+ autoHomeClockPref.setOnPreferenceChangeListener(this)
+
+ val homeTimezonePref: ListPreference? = findPreference(KEY_HOME_TZ)
+ homeTimezonePref?.setEnabled(autoHomeClockEnabled)
+ refreshListPreference(homeTimezonePref!!)
+
+ refreshListPreference(findPreference(KEY_ALARM_CRESCENDO)!!)
+ refreshListPreference(findPreference(KEY_TIMER_CRESCENDO)!!)
+ refreshListPreference(findPreference(KEY_ALARM_SNOOZE)!!)
+
+ val dateAndTimeSetting: Preference? = findPreference(KEY_DATE_TIME)
+ dateAndTimeSetting?.setOnPreferenceClickListener(this)
+
+ val weekStartPref: SimpleMenuPreference? = findPreference(KEY_WEEK_START)
+ // Set the default value programmatically
+ val weekdayOrder = DataModel.dataModel.weekdayOrder
+ val firstDay = weekdayOrder.calendarDays[0]
+ val value = firstDay.toString()
+ weekStartPref?.let {
+ val idx: Int = it.findIndexOfValue(value)
+ it.setValueIndex(idx)
+ it.setSummary(weekStartPref.getEntries().get(idx))
+ it.setOnPreferenceChangeListener(this)
+ }
+
+ val timerRingtonePref: Preference? = findPreference(KEY_TIMER_RINGTONE)
+ timerRingtonePref?.let {
+ it.setOnPreferenceClickListener(this)
+ it.setSummary(DataModel.dataModel.timerRingtoneTitle)
+ }
+ }
+
+ private fun refreshListPreference(preference: ListPreference) {
+ preference.setSummary(preference.getEntry())
+ preference.setOnPreferenceChangeListener(this)
+ }
+
+ private fun updateAutoSnoozeSummary(listPref: ListPreference, delay: String) {
+ val i = delay.toInt()
+ if (i == -1) {
+ listPref.setSummary(R.string.auto_silence_never)
+ } else {
+ listPref.setSummary(Utils.getNumberFormattedQuantityString(getActivity()!!,
+ R.plurals.auto_silence_summary, i))
+ }
+ }
+ }
+
+ companion object {
+ const val KEY_ALARM_SNOOZE = "snooze_duration"
+ const val KEY_ALARM_CRESCENDO = "alarm_crescendo_duration"
+ const val KEY_TIMER_CRESCENDO = "timer_crescendo_duration"
+ const val KEY_TIMER_RINGTONE = "timer_ringtone"
+ const val KEY_TIMER_VIBRATE = "timer_vibrate"
+ const val KEY_AUTO_SILENCE = "auto_silence"
+ const val KEY_CLOCK_STYLE = "clock_style"
+ const val KEY_CLOCK_DISPLAY_SECONDS = "display_clock_seconds"
+ const val KEY_HOME_TZ = "home_time_zone"
+ const val KEY_AUTO_HOME_CLOCK = "automatic_home_clock"
+ const val KEY_DATE_TIME = "date_time"
+ const val KEY_VOLUME_BUTTONS = "volume_button_setting"
+ const val KEY_WEEK_START = "week_start"
+ const val DEFAULT_VOLUME_BEHAVIOR = "0"
+ const val VOLUME_BEHAVIOR_SNOOZE = "1"
+ const val VOLUME_BEHAVIOR_DISMISS = "2"
+ const val PREFS_FRAGMENT_TAG = "prefs_fragment"
+ const val PREFERENCE_DIALOG_FRAGMENT_TAG = "preference_dialog"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/settings/SimpleMenuPreference.java b/src/com/android/deskclock/settings/SimpleMenuPreference.java
deleted file mode 100644
index 579ad57..0000000
--- a/src/com/android/deskclock/settings/SimpleMenuPreference.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.settings;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.core.content.ContextCompat;
-import androidx.preference.DropDownPreference;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-
-/**
- * Bend {@link DropDownPreference} to support
- * <a href="https://material.google.com/components/menus.html#menus-behavior">Simple Menus</a>.
- */
-public class SimpleMenuPreference extends DropDownPreference {
-
- private SimpleMenuAdapter mAdapter;
-
- public SimpleMenuPreference(Context context) {
- this(context, null);
- }
-
- public SimpleMenuPreference(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.dropdownPreferenceStyle);
- }
-
- public SimpleMenuPreference(Context context, AttributeSet attrs, int defStyle) {
- this(context, attrs, defStyle, 0);
- }
-
- public SimpleMenuPreference(Context context, AttributeSet attrs, int defStyleAttr,
- int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- }
-
- @Override
- protected ArrayAdapter createAdapter() {
- mAdapter = new SimpleMenuAdapter(getContext(), R.layout.simple_menu_dropdown_item);
- return mAdapter;
- }
-
- private static void restoreOriginalOrder(CharSequence[] array,
- int lastSelectedOriginalPosition) {
- final CharSequence item = array[0];
- System.arraycopy(array, 1, array, 0, lastSelectedOriginalPosition);
- array[lastSelectedOriginalPosition] = item;
- }
-
- private static void swapSelectedToFront(CharSequence[] array, int position) {
- final CharSequence item = array[position];
- System.arraycopy(array, 0, array, 1, position);
- array[0] = item;
- }
-
- private static void setSelectedPosition(CharSequence[] array, int lastSelectedOriginalPosition,
- int position) {
- final CharSequence item = array[position];
- restoreOriginalOrder(array, lastSelectedOriginalPosition);
- final int originalPosition = Utils.indexOf(array, item);
- swapSelectedToFront(array, originalPosition);
- }
-
- @Override
- public void setSummary(CharSequence summary) {
- final CharSequence[] entries = getEntries();
- final int index = Utils.indexOf(entries, summary);
- if (index == -1) {
- throw new IllegalArgumentException("Illegal Summary");
- }
- final int lastSelectedOriginalPosition = mAdapter.getLastSelectedOriginalPosition();
- mAdapter.setSelectedPosition(index);
- setSelectedPosition(entries, lastSelectedOriginalPosition, index);
- setSelectedPosition(getEntryValues(), lastSelectedOriginalPosition, index);
- super.setSummary(summary);
- }
-
- private final static class SimpleMenuAdapter extends ArrayAdapter<CharSequence> {
-
- /** The original position of the last selected element */
- private int mLastSelectedOriginalPosition = 0;
-
- SimpleMenuAdapter(Context context, int resource) {
- super(context, resource);
- }
-
- private void restoreOriginalOrder() {
- final CharSequence item = getItem(0);
- remove(item);
- insert(item, mLastSelectedOriginalPosition);
- }
-
- private void swapSelectedToFront(int position) {
- final CharSequence item = getItem(position);
- remove(item);
- insert(item, 0);
- mLastSelectedOriginalPosition = position;
- }
-
- int getLastSelectedOriginalPosition() {
- return mLastSelectedOriginalPosition;
- }
-
- void setSelectedPosition(int position) {
- setNotifyOnChange(false);
- final CharSequence item = getItem(position);
- restoreOriginalOrder();
- final int originalPosition = getPosition(item);
- swapSelectedToFront(originalPosition);
- notifyDataSetChanged();
- }
-
- @Override
- public View getDropDownView(int position, View convertView, @NonNull ViewGroup parent) {
- final View view = super.getDropDownView(position, convertView, parent);
- if (position == 0) {
- view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white_08p));
- } else {
- view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.transparent));
- }
- return view;
- }
- }
-}
diff --git a/src/com/android/deskclock/settings/SimpleMenuPreference.kt b/src/com/android/deskclock/settings/SimpleMenuPreference.kt
new file mode 100644
index 0000000..9eee1b4
--- /dev/null
+++ b/src/com/android/deskclock/settings/SimpleMenuPreference.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.settings
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.core.content.ContextCompat
+import androidx.preference.DropDownPreference
+
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+/**
+ * Bend [DropDownPreference] to support
+ * [Simple Menus](https://material.google.com/components/menus.html#menus-behavior).
+ */
+class SimpleMenuPreference(
+ context: Context?,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int
+) : DropDownPreference(context, attrs, defStyleAttr, defStyleRes) {
+ private lateinit var mAdapter: SimpleMenuAdapter
+
+ constructor(context: Context?) : this(context, null) {
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?) :
+ this(context, attrs, R.attr.dropdownPreferenceStyle) {
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) :
+ this(context, attrs, defStyle, 0) {
+ }
+
+ override fun createAdapter(): ArrayAdapter<CharSequence?> {
+ mAdapter = SimpleMenuAdapter(getContext(), R.layout.simple_menu_dropdown_item)
+ return mAdapter
+ }
+
+ override fun setSummary(summary: CharSequence) {
+ val entries: Array<CharSequence> = getEntries()
+ val index = Utils.indexOf(entries, summary)
+ require(index != -1) { "Illegal Summary" }
+ val lastSelectedOriginalPosition = mAdapter.lastSelectedOriginalPosition
+ mAdapter.setSelectedPosition(index)
+ setSelectedPosition(entries, lastSelectedOriginalPosition, index)
+ setSelectedPosition(getEntryValues(), lastSelectedOriginalPosition, index)
+ super.setSummary(summary)
+ }
+
+ private class SimpleMenuAdapter internal constructor(context: Context, resource: Int) :
+ ArrayAdapter<CharSequence?>(context, resource) {
+
+ /** The original position of the last selected element */
+ var lastSelectedOriginalPosition = 0
+ private set
+
+ private fun restoreOriginalOrder() {
+ val item: CharSequence? = getItem(0)
+ remove(item)
+ insert(item, lastSelectedOriginalPosition)
+ }
+
+ private fun swapSelectedToFront(position: Int) {
+ val item: CharSequence? = getItem(position)
+ remove(item)
+ insert(item, 0)
+ lastSelectedOriginalPosition = position
+ }
+
+ fun setSelectedPosition(position: Int) {
+ setNotifyOnChange(false)
+ val item: CharSequence? = getItem(position)
+ restoreOriginalOrder()
+ val originalPosition: Int = getPosition(item)
+ swapSelectedToFront(originalPosition)
+ notifyDataSetChanged()
+ }
+
+ override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val view: View = super.getDropDownView(position, convertView, parent)
+ if (position == 0) {
+ view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.white_08p))
+ } else {
+ view.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.transparent))
+ }
+ return view
+ }
+ }
+
+ companion object {
+ private fun restoreOriginalOrder(
+ array: Array<CharSequence>,
+ lastSelectedOriginalPosition: Int
+ ) {
+ val item = array[0]
+ System.arraycopy(array, 1, array, 0, lastSelectedOriginalPosition)
+ array[lastSelectedOriginalPosition] = item
+ }
+
+ private fun swapSelectedToFront(array: Array<CharSequence>, position: Int) {
+ val item = array[position]
+ System.arraycopy(array, 0, array, 1, position)
+ array[0] = item
+ }
+
+ private fun setSelectedPosition(
+ array: Array<CharSequence>,
+ lastSelectedOriginalPosition: Int,
+ position: Int
+ ) {
+ val item = array[position]
+ restoreOriginalOrder(array, lastSelectedOriginalPosition)
+ val originalPosition = Utils.indexOf(array, item)
+ swapSelectedToFront(array, originalPosition)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/LapsAdapter.java b/src/com/android/deskclock/stopwatch/LapsAdapter.java
deleted file mode 100644
index e3afaf2..0000000
--- a/src/com/android/deskclock/stopwatch/LapsAdapter.java
+++ /dev/null
@@ -1,358 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.stopwatch;
-
-import android.content.Context;
-import androidx.annotation.VisibleForTesting;
-import androidx.recyclerview.widget.RecyclerView;
-import android.text.format.DateUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Lap;
-import com.android.deskclock.data.Stopwatch;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.text.DecimalFormatSymbols;
-import java.util.List;
-
-/**
- * Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
- * lap is at the bottom.
- */
-class LapsAdapter extends RecyclerView.Adapter<LapsAdapter.LapItemHolder> {
-
- private static final long TEN_MINUTES = 10 * DateUtils.MINUTE_IN_MILLIS;
- private static final long HOUR = DateUtils.HOUR_IN_MILLIS;
- private static final long TEN_HOURS = 10 * HOUR;
- private static final long HUNDRED_HOURS = 100 * HOUR;
-
- /** A single space preceded by a zero-width LRM; This groups adjacent chars left-to-right. */
- private static final String LRM_SPACE = "\u200E ";
-
- /** Reusable StringBuilder that assembles a formatted time; alleviates memory churn. */
- private static final StringBuilder sTimeBuilder = new StringBuilder(12);
-
- private final LayoutInflater mInflater;
- private final Context mContext;
-
- /** Used to determine when the time format for the lap time column has changed length. */
- private int mLastFormattedLapTimeLength;
-
- /** Used to determine when the time format for the total time column has changed length. */
- private int mLastFormattedAccumulatedTimeLength;
-
- LapsAdapter(Context context) {
- mContext = context;
- mInflater = LayoutInflater.from(context);
- setHasStableIds(true);
- }
-
- /**
- * After recording the first lap, there is always a "current lap" in progress.
- *
- * @return 0 if no laps are yet recorded; lap count + 1 if any laps exist
- */
- @Override
- public int getItemCount() {
- final int lapCount = getLaps().size();
- final int currentLapCount = lapCount == 0 ? 0 : 1;
- return currentLapCount + lapCount;
- }
-
- @Override
- public LapItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- final View v = mInflater.inflate(R.layout.lap_view, parent, false /* attachToRoot */);
- return new LapItemHolder(v);
- }
-
- @Override
- public void onBindViewHolder(LapItemHolder viewHolder, int position) {
- final long lapTime;
- final int lapNumber;
- final long totalTime;
-
- // Lap will be null for the current lap.
- final Lap lap = position == 0 ? null : getLaps().get(position - 1);
- if (lap != null) {
- // For a recorded lap, merely extract the values to format.
- lapTime = lap.getLapTime();
- lapNumber = lap.getLapNumber();
- totalTime = lap.getAccumulatedTime();
- } else {
- // For the current lap, compute times relative to the stopwatch.
- totalTime = getStopwatch().getTotalTime();
- lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
- lapNumber = getLaps().size() + 1;
- }
-
- // Bind data into the child views.
- viewHolder.lapTime.setText(formatLapTime(lapTime, true));
- viewHolder.accumulatedTime.setText(formatAccumulatedTime(totalTime, true));
- viewHolder.lapNumber.setText(formatLapNumber(getLaps().size() + 1, lapNumber));
- }
-
- @Override
- public long getItemId(int position) {
- final List<Lap> laps = getLaps();
- if (position == 0) {
- return laps.size() + 1;
- }
-
- return laps.get(position - 1).getLapNumber();
- }
-
- /**
- * @param rv the RecyclerView that contains the {@code childView}
- * @param totalTime time accumulated for the current lap and all prior laps
- */
- void updateCurrentLap(RecyclerView rv, long totalTime) {
- // If no laps exist there is nothing to do.
- if (getItemCount() == 0) {
- return;
- }
-
- final View currentLapView = rv.getChildAt(0);
- if (currentLapView != null) {
- // Compute the lap time using the total time.
- final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
-
- final LapItemHolder holder = (LapItemHolder) rv.getChildViewHolder(currentLapView);
- holder.lapTime.setText(formatLapTime(lapTime, false));
- holder.accumulatedTime.setText(formatAccumulatedTime(totalTime, false));
- }
- }
-
- /**
- * Record a new lap and update this adapter to include it.
- *
- * @return a newly cleared lap
- */
- Lap addLap() {
- final Lap lap = DataModel.getDataModel().addLap();
-
- if (getItemCount() == 10) {
- // 10 total laps indicates all items switch from 1 to 2 digit lap numbers.
- notifyDataSetChanged();
- } else {
- // New current lap now exists.
- notifyItemInserted(0);
-
- // Prior current lap must be refreshed once with the true values in place.
- notifyItemChanged(1);
- }
-
- return lap;
- }
-
- /**
- * Remove all recorded laps and update this adapter.
- */
- void clearLaps() {
- // Clear the computed time lengths related to the old recorded laps.
- mLastFormattedLapTimeLength = 0;
- mLastFormattedAccumulatedTimeLength = 0;
-
- notifyDataSetChanged();
- }
-
- /**
- * @return a formatted textual description of lap times and total time
- */
- String getShareText() {
- final Stopwatch stopwatch = getStopwatch();
- final long totalTime = stopwatch.getTotalTime();
- final String stopwatchTime = formatTime(totalTime, totalTime, ":");
-
- // Choose a size for the builder that is unlikely to be resized.
- final StringBuilder builder = new StringBuilder(1000);
-
- // Add the total elapsed time of the stopwatch.
- builder.append(mContext.getString(R.string.sw_share_main, stopwatchTime));
- builder.append("\n");
-
- final List<Lap> laps = getLaps();
- if (!laps.isEmpty()) {
- // Add a header for lap times.
- builder.append(mContext.getString(R.string.sw_share_laps));
- builder.append("\n");
-
- // Loop through the laps in the order they were recorded; reverse of display order.
- final String separator = DecimalFormatSymbols.getInstance().getDecimalSeparator() + " ";
- for (int i = laps.size() - 1; i >= 0; i--) {
- final Lap lap = laps.get(i);
- builder.append(lap.getLapNumber());
- builder.append(separator);
- final long lapTime = lap.getLapTime();
- builder.append(formatTime(lapTime, lapTime, " "));
- builder.append("\n");
- }
-
- // Append the final lap
- builder.append(laps.size() + 1);
- builder.append(separator);
- final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
- builder.append(formatTime(lapTime, lapTime, " "));
- builder.append("\n");
- }
-
- return builder.toString();
- }
-
- /**
- * @param lapCount the total number of recorded laps
- * @param lapNumber the number of the lap being formatted
- * @return e.g. "# 7" if {@code lapCount} less than 10; "# 07" if {@code lapCount} is 10 or more
- */
- @VisibleForTesting
- String formatLapNumber(int lapCount, int lapNumber) {
- if (lapCount < 10) {
- return mContext.getString(R.string.lap_number_single_digit, lapNumber);
- } else {
- return mContext.getString(R.string.lap_number_double_digit, lapNumber);
- }
- }
-
- /**
- * @param maxTime the maximum amount of time; used to choose a time format
- * @param time the time to format guaranteed not to exceed {@code maxTime}
- * @param separator displayed between hours and minutes as well as minutes and seconds
- * @return a formatted version of the time
- */
- @VisibleForTesting
- static String formatTime(long maxTime, long time, String separator) {
- final int hours, minutes, seconds, hundredths;
- if (time <= 0) {
- // A negative time should be impossible, but is tolerated to avoid crashing the app.
- hours = minutes = seconds = hundredths = 0;
- } else {
- hours = (int) (time / DateUtils.HOUR_IN_MILLIS);
- int remainder = (int) (time % DateUtils.HOUR_IN_MILLIS);
-
- minutes = (int) (remainder / DateUtils.MINUTE_IN_MILLIS);
- remainder = (int) (remainder % DateUtils.MINUTE_IN_MILLIS);
-
- seconds = (int) (remainder / DateUtils.SECOND_IN_MILLIS);
- remainder = (int) (remainder % DateUtils.SECOND_IN_MILLIS);
-
- hundredths = remainder / 10;
- }
-
- final char decimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
-
- sTimeBuilder.setLength(0);
-
- // The display of hours and minutes varies based on maxTime.
- if (maxTime < TEN_MINUTES) {
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 1));
- } else if (maxTime < HOUR) {
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
- } else if (maxTime < TEN_HOURS) {
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 1));
- sTimeBuilder.append(separator);
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
- } else if (maxTime < HUNDRED_HOURS) {
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 2));
- sTimeBuilder.append(separator);
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
- } else {
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hours, 3));
- sTimeBuilder.append(separator);
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(minutes, 2));
- }
-
- // The display of seconds and hundredths-of-a-second is constant.
- sTimeBuilder.append(separator);
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(seconds, 2));
- sTimeBuilder.append(decimalSeparator);
- sTimeBuilder.append(UiDataModel.getUiDataModel().getFormattedNumber(hundredths, 2));
-
- return sTimeBuilder.toString();
- }
-
- /**
- * @param lapTime the lap time to be formatted
- * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
- * set changes; they are not allowed to occur during bind
- * @return a formatted version of the lap time
- */
- private String formatLapTime(long lapTime, boolean isBinding) {
- // The longest lap dictates the way the given lapTime must be formatted.
- final long longestLapTime = Math.max(DataModel.getDataModel().getLongestLapTime(), lapTime);
- final String formattedTime = formatTime(longestLapTime, lapTime, LRM_SPACE);
-
- // If the newly formatted lap time has altered the format, refresh all laps.
- final int newLength = formattedTime.length();
- if (!isBinding && mLastFormattedLapTimeLength != newLength) {
- mLastFormattedLapTimeLength = newLength;
- notifyDataSetChanged();
- }
-
- return formattedTime;
- }
-
- /**
- * @param accumulatedTime the accumulated time to be formatted
- * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
- * set changes; they are not allowed to occur during bind
- * @return a formatted version of the accumulated time
- */
- private String formatAccumulatedTime(long accumulatedTime, boolean isBinding) {
- final long totalTime = getStopwatch().getTotalTime();
- final long longestAccumulatedTime = Math.max(totalTime, accumulatedTime);
- final String formattedTime = formatTime(longestAccumulatedTime, accumulatedTime, LRM_SPACE);
-
- // If the newly formatted accumulated time has altered the format, refresh all laps.
- final int newLength = formattedTime.length();
- if (!isBinding && mLastFormattedAccumulatedTimeLength != newLength) {
- mLastFormattedAccumulatedTimeLength = newLength;
- notifyDataSetChanged();
- }
-
- return formattedTime;
- }
-
- private Stopwatch getStopwatch() {
- return DataModel.getDataModel().getStopwatch();
- }
-
- private List<Lap> getLaps() {
- return DataModel.getDataModel().getLaps();
- }
-
- /**
- * Cache the child views of each lap item view.
- */
- static final class LapItemHolder extends RecyclerView.ViewHolder {
-
- private final TextView lapNumber;
- private final TextView lapTime;
- private final TextView accumulatedTime;
-
- LapItemHolder(View itemView) {
- super(itemView);
-
- lapTime = (TextView) itemView.findViewById(R.id.lap_time);
- lapNumber = (TextView) itemView.findViewById(R.id.lap_number);
- accumulatedTime = (TextView) itemView.findViewById(R.id.lap_total);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/LapsAdapter.kt b/src/com/android/deskclock/stopwatch/LapsAdapter.kt
new file mode 100644
index 0000000..b9264d4
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/LapsAdapter.kt
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.content.Context
+import android.text.format.DateUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.recyclerview.widget.RecyclerView
+
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Lap
+import com.android.deskclock.data.Stopwatch
+import com.android.deskclock.stopwatch.LapsAdapter.LapItemHolder
+import com.android.deskclock.uidata.UiDataModel
+
+import java.text.DecimalFormatSymbols
+
+import kotlin.math.max
+
+/**
+ * Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
+ * lap is at the bottom.
+ */
+internal class LapsAdapter(context: Context) : RecyclerView.Adapter<LapItemHolder?>() {
+ private val mInflater: LayoutInflater
+ private val mContext: Context
+
+ /** Used to determine when the time format for the lap time column has changed length. */
+ private var mLastFormattedLapTimeLength = 0
+
+ /** Used to determine when the time format for the total time column has changed length. */
+ private var mLastFormattedAccumulatedTimeLength = 0
+
+ init {
+ mContext = context
+ mInflater = LayoutInflater.from(context)
+ setHasStableIds(true)
+ }
+
+ /**
+ * After recording the first lap, there is always a "current lap" in progress.
+ *
+ * @return 0 if no laps are yet recorded; lap count + 1 if any laps exist
+ */
+ override fun getItemCount(): Int {
+ val lapCount = laps.size
+ val currentLapCount = if (lapCount == 0) 0 else 1
+ return currentLapCount + lapCount
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LapItemHolder {
+ val v: View = mInflater.inflate(R.layout.lap_view, parent, false /* attachToRoot */)
+ return LapItemHolder(v)
+ }
+
+ override fun onBindViewHolder(viewHolder: LapItemHolder, position: Int) {
+ val lapTime: Long
+ val lapNumber: Int
+ val totalTime: Long
+
+ // Lap will be null for the current lap.
+ val lap = if (position == 0) null else laps[position - 1]
+ if (lap != null) {
+ // For a recorded lap, merely extract the values to format.
+ lapTime = lap.lapTime
+ lapNumber = lap.lapNumber
+ totalTime = lap.accumulatedTime
+ } else {
+ // For the current lap, compute times relative to the stopwatch.
+ totalTime = stopwatch.totalTime
+ lapTime = DataModel.dataModel.getCurrentLapTime(totalTime)
+ lapNumber = laps.size + 1
+ }
+
+ // Bind data into the child views.
+ viewHolder.lapTime.setText(formatLapTime(lapTime, true))
+ viewHolder.accumulatedTime.setText(formatAccumulatedTime(totalTime, true))
+ viewHolder.lapNumber.setText(formatLapNumber(laps.size + 1, lapNumber))
+ }
+
+ override fun getItemId(position: Int): Long {
+ val laps = laps
+ return if (position == 0) {
+ (laps.size + 1).toLong()
+ } else {
+ laps[position - 1].lapNumber.toLong()
+ }
+ }
+
+ /**
+ * @param rv the RecyclerView that contains the `childView`
+ * @param totalTime time accumulated for the current lap and all prior laps
+ */
+ fun updateCurrentLap(rv: RecyclerView, totalTime: Long) {
+ // If no laps exist there is nothing to do.
+ if (itemCount == 0) {
+ return
+ }
+
+ val currentLapView: View? = rv.getChildAt(0)
+ if (currentLapView != null) {
+ // Compute the lap time using the total time.
+ val lapTime = DataModel.dataModel.getCurrentLapTime(totalTime)
+ val holder = rv.getChildViewHolder(currentLapView) as LapItemHolder
+ holder.lapTime.setText(formatLapTime(lapTime, false))
+ holder.accumulatedTime.setText(formatAccumulatedTime(totalTime, false))
+ }
+ }
+
+ /**
+ * Record a new lap and update this adapter to include it.
+ *
+ * @return a newly cleared lap
+ */
+ fun addLap(): Lap? {
+ val lap = DataModel.dataModel.addLap()
+
+ if (itemCount == 10) {
+ // 10 total laps indicates all items switch from 1 to 2 digit lap numbers.
+ notifyDataSetChanged()
+ } else {
+ // New current lap now exists.
+ notifyItemInserted(0)
+
+ // Prior current lap must be refreshed once with the true values in place.
+ notifyItemChanged(1)
+ }
+
+ return lap
+ }
+
+ /**
+ * Remove all recorded laps and update this adapter.
+ */
+ fun clearLaps() {
+ // Clear the computed time lengths related to the old recorded laps.
+ mLastFormattedLapTimeLength = 0
+ mLastFormattedAccumulatedTimeLength = 0
+
+ notifyDataSetChanged()
+ }
+
+ /**
+ * @return a formatted textual description of lap times and total time
+ */
+ val shareText: String
+ get() {
+ val stopwatch = stopwatch
+ val totalTime = stopwatch.totalTime
+ val stopwatchTime = formatTime(totalTime, totalTime, ":")
+
+ // Choose a size for the builder that is unlikely to be resized.
+ val builder = StringBuilder(1000)
+
+ // Add the total elapsed time of the stopwatch.
+ builder.append(mContext.getString(R.string.sw_share_main, stopwatchTime))
+ builder.append("\n")
+
+ val laps = laps
+ if (laps.isNotEmpty()) {
+ // Add a header for lap times.
+ builder.append(mContext.getString(R.string.sw_share_laps))
+ builder.append("\n")
+
+ // Loop through the laps in the order they were recorded; reverse of display order.
+ val separator = DecimalFormatSymbols.getInstance().decimalSeparator.toString() + " "
+ for (i in laps.indices.reversed()) {
+ val lap = laps[i]
+ builder.append(lap.lapNumber)
+ builder.append(separator)
+ val lapTime = lap.lapTime
+ builder.append(formatTime(lapTime, lapTime, " "))
+ builder.append("\n")
+ }
+
+ // Append the final lap
+ builder.append(laps.size + 1)
+ builder.append(separator)
+ val lapTime = DataModel.dataModel.getCurrentLapTime(totalTime)
+ builder.append(formatTime(lapTime, lapTime, " "))
+ builder.append("\n")
+ }
+ return builder.toString()
+ }
+
+ /**
+ * @param lapCount the total number of recorded laps
+ * @param lapNumber the number of the lap being formatted
+ * @return e.g. "# 7" if `lapCount` less than 10; "# 07" if `lapCount` is 10 or more
+ */
+ @VisibleForTesting
+ fun formatLapNumber(lapCount: Int, lapNumber: Int): String {
+ return if (lapCount < 10) {
+ mContext.getString(R.string.lap_number_single_digit, lapNumber)
+ } else {
+ mContext.getString(R.string.lap_number_double_digit, lapNumber)
+ }
+ }
+
+ /**
+ * @param lapTime the lap time to be formatted
+ * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
+ * set changes; they are not allowed to occur during bind
+ * @return a formatted version of the lap time
+ */
+ private fun formatLapTime(lapTime: Long, isBinding: Boolean): String {
+ // The longest lap dictates the way the given lapTime must be formatted.
+ val longestLapTime = max(DataModel.dataModel.longestLapTime, lapTime)
+ val formattedTime = formatTime(longestLapTime, lapTime, LRM_SPACE)
+
+ // If the newly formatted lap time has altered the format, refresh all laps.
+ val newLength = formattedTime.length
+ if (!isBinding && mLastFormattedLapTimeLength != newLength) {
+ mLastFormattedLapTimeLength = newLength
+ notifyDataSetChanged()
+ }
+
+ return formattedTime
+ }
+
+ /**
+ * @param accumulatedTime the accumulated time to be formatted
+ * @param isBinding if the lap time is requested so it can be bound avoid notifying of data
+ * set changes; they are not allowed to occur during bind
+ * @return a formatted version of the accumulated time
+ */
+ private fun formatAccumulatedTime(accumulatedTime: Long, isBinding: Boolean): String {
+ val totalTime = stopwatch.totalTime
+ val longestAccumulatedTime = max(totalTime, accumulatedTime)
+ val formattedTime = formatTime(longestAccumulatedTime, accumulatedTime, LRM_SPACE)
+
+ // If the newly formatted accumulated time has altered the format, refresh all laps.
+ val newLength = formattedTime.length
+ if (!isBinding && mLastFormattedAccumulatedTimeLength != newLength) {
+ mLastFormattedAccumulatedTimeLength = newLength
+ notifyDataSetChanged()
+ }
+
+ return formattedTime
+ }
+
+ private val stopwatch: Stopwatch
+ get() = DataModel.dataModel.stopwatch
+
+ private val laps: List<Lap>
+ get() = DataModel.dataModel.laps
+
+ /**
+ * Cache the child views of each lap item view.
+ */
+ internal class LapItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val lapNumber: TextView
+ val lapTime: TextView
+ val accumulatedTime: TextView
+
+ init {
+ lapTime = itemView.findViewById(R.id.lap_time) as TextView
+ lapNumber = itemView.findViewById(R.id.lap_number) as TextView
+ accumulatedTime = itemView.findViewById(R.id.lap_total) as TextView
+ }
+ }
+
+ companion object {
+ private val TEN_MINUTES: Long = 10 * DateUtils.MINUTE_IN_MILLIS
+ private val HOUR: Long = DateUtils.HOUR_IN_MILLIS
+ private val TEN_HOURS = 10 * HOUR
+ private val HUNDRED_HOURS = 100 * HOUR
+
+ /** A single space preceded by a zero-width LRM; This groups adjacent chars left-to-right. */
+ private const val LRM_SPACE = "\u200E "
+
+ /** Reusable StringBuilder that assembles a formatted time; alleviates memory churn. */
+ private val sTimeBuilder = StringBuilder(12)
+
+ /**
+ * @param maxTime the maximum amount of time; used to choose a time format
+ * @param time the time to format guaranteed not to exceed `maxTime`
+ * @param separator displayed between hours and minutes as well as minutes and seconds
+ * @return a formatted version of the time
+ */
+ @VisibleForTesting
+ fun formatTime(maxTime: Long, time: Long, separator: String?): String {
+ val hours: Int
+ val minutes: Int
+ val seconds: Int
+ val hundredths: Int
+ if (time <= 0) {
+ // A negative time should be impossible, but is tolerated to avoid crashing the app.
+ hundredths = 0
+ seconds = hundredths
+ minutes = seconds
+ hours = minutes
+ } else {
+ hours = (time / DateUtils.HOUR_IN_MILLIS).toInt()
+ var remainder = (time % DateUtils.HOUR_IN_MILLIS).toInt()
+ minutes = (remainder / DateUtils.MINUTE_IN_MILLIS).toInt()
+ remainder = (remainder % DateUtils.MINUTE_IN_MILLIS).toInt()
+ seconds = (remainder / DateUtils.SECOND_IN_MILLIS).toInt()
+ remainder = (remainder % DateUtils.SECOND_IN_MILLIS).toInt()
+ hundredths = remainder / 10
+ }
+
+ val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator
+
+ sTimeBuilder.setLength(0)
+
+ // The display of hours and minutes varies based on maxTime.
+ when {
+ maxTime < TEN_MINUTES -> {
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 1))
+ }
+ maxTime < HOUR -> {
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+ }
+ maxTime < TEN_HOURS -> {
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hours, 1))
+ sTimeBuilder.append(separator)
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+ }
+ maxTime < HUNDRED_HOURS -> {
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hours, 2))
+ sTimeBuilder.append(separator)
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+ }
+ else -> {
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hours, 3))
+ sTimeBuilder.append(separator)
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(minutes, 2))
+ }
+ }
+
+ // The display of seconds and hundredths-of-a-second is constant.
+ sTimeBuilder.append(separator)
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(seconds, 2))
+ sTimeBuilder.append(decimalSeparator)
+ sTimeBuilder.append(UiDataModel.uiDataModel.getFormattedNumber(hundredths, 2))
+
+ return sTimeBuilder.toString()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchCircleView.java b/src/com/android/deskclock/stopwatch/StopwatchCircleView.java
deleted file mode 100644
index 31ae20d..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchCircleView.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.stopwatch;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.view.View;
-
-import com.android.deskclock.R;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Lap;
-import com.android.deskclock.data.Stopwatch;
-
-import java.util.List;
-
-/**
- * Custom view that draws a reference lap as a circle when one exists.
- */
-public final class StopwatchCircleView extends View {
-
- /** The size of the dot indicating the user's position within the reference lap. */
- private final float mDotRadius;
-
- /** An amount to subtract from the true radius to account for drawing thicknesses. */
- private final float mRadiusOffset;
-
- /** Used to scale the width of the marker to make it similarly visible on all screens. */
- private final float mScreenDensity;
-
- /** The color indicating the remaining portion of the current lap. */
- private final int mRemainderColor;
-
- /** The color indicating the completed portion of the lap. */
- private final int mCompletedColor;
-
- /** The size of the stroke that paints the lap circle. */
- private final float mStrokeSize;
-
- /** The size of the stroke that paints the marker for the end of the prior lap. */
- private final float mMarkerStrokeSize;
-
- private final Paint mPaint = new Paint();
- private final Paint mFill = new Paint();
- private final RectF mArcRect = new RectF();
-
- @SuppressWarnings("unused")
- public StopwatchCircleView(Context context) {
- this(context, null);
- }
-
- public StopwatchCircleView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final Resources resources = context.getResources();
- final float dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size);
-
- mDotRadius = dotDiameter / 2f;
- mScreenDensity = resources.getDisplayMetrics().density;
- mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
- mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
- mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize);
-
- mRemainderColor = Color.WHITE;
- mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent);
-
- mPaint.setAntiAlias(true);
- mPaint.setStyle(Paint.Style.STROKE);
-
- mFill.setAntiAlias(true);
- mFill.setColor(mCompletedColor);
- mFill.setStyle(Paint.Style.FILL);
- }
-
- /**
- * Start the animation if it is not currently running.
- */
- void update() {
- postInvalidateOnAnimation();
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- // Compute the size and location of the circle to be drawn.
- final int xCenter = getWidth() / 2;
- final int yCenter = getHeight() / 2;
- final float radius = Math.min(xCenter, yCenter) - mRadiusOffset;
-
- // Reset old painting state.
- mPaint.setColor(mRemainderColor);
- mPaint.setStrokeWidth(mStrokeSize);
-
- final List<Lap> laps = getLaps();
-
- // If a reference lap does not exist or should not be drawn, draw a simple white circle.
- if (laps.isEmpty() || !DataModel.getDataModel().canAddMoreLaps()) {
- // Draw a complete white circle; no red arc required.
- canvas.drawCircle(xCenter, yCenter, radius, mPaint);
-
- // No need to continue animating the plain white circle.
- return;
- }
-
- // The first lap is the reference lap to which all future laps are compared.
- final Stopwatch stopwatch = getStopwatch();
- final int lapCount = laps.size();
- final Lap firstLap = laps.get(lapCount - 1);
- final Lap priorLap = laps.get(0);
- final long firstLapTime = firstLap.getLapTime();
- final long currentLapTime = stopwatch.getTotalTime() - priorLap.getAccumulatedTime();
-
- // Draw a combination of red and white arcs to create a circle.
- mArcRect.top = yCenter - radius;
- mArcRect.bottom = yCenter + radius;
- mArcRect.left = xCenter - radius;
- mArcRect.right = xCenter + radius;
- final float redPercent = (float) currentLapTime / (float) firstLapTime;
- final float whitePercent = 1 - (redPercent > 1 ? 1 : redPercent);
-
- // Draw a white arc to indicate the amount of reference lap that remains.
- canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, whitePercent * 360, false, mPaint);
-
- // Draw a red arc to indicate the amount of reference lap completed.
- mPaint.setColor(mCompletedColor);
- canvas.drawArc(mArcRect, 270, redPercent * 360 , false, mPaint);
-
- // Starting on lap 2, a marker can be drawn indicating where the prior lap ended.
- if (lapCount > 1) {
- mPaint.setColor(mRemainderColor);
- mPaint.setStrokeWidth(mMarkerStrokeSize);
- final float markerAngle = (float) priorLap.getLapTime() / (float) firstLapTime * 360;
- final float startAngle = 270 + markerAngle;
- final float sweepAngle = mScreenDensity * (float) (360 / (radius * Math.PI));
- canvas.drawArc(mArcRect, startAngle, sweepAngle, false, mPaint);
- }
-
- // Draw a red dot to indicate current position relative to reference lap.
- final float dotAngleDegrees = 270 + redPercent * 360;
- final double dotAngleRadians = Math.toRadians(dotAngleDegrees);
- final float dotX = xCenter + (float) (radius * Math.cos(dotAngleRadians));
- final float dotY = yCenter + (float) (radius * Math.sin(dotAngleRadians));
- canvas.drawCircle(dotX, dotY, mDotRadius, mFill);
-
- // If the stopwatch is not running it does not require continuous updates.
- if (stopwatch.isRunning()) {
- postInvalidateOnAnimation();
- }
- }
-
- private Stopwatch getStopwatch() {
- return DataModel.getDataModel().getStopwatch();
- }
-
- private List<Lap> getLaps() {
- return DataModel.getDataModel().getLaps();
- }
-}
diff --git a/src/com/android/deskclock/stopwatch/StopwatchCircleView.kt b/src/com/android/deskclock/stopwatch/StopwatchCircleView.kt
new file mode 100644
index 0000000..a7d8699
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchCircleView.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.view.View
+
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Lap
+import com.android.deskclock.data.Stopwatch
+
+import kotlin.math.cos
+import kotlin.math.min
+import kotlin.math.sin
+
+/**
+ * Custom view that draws a reference lap as a circle when one exists.
+ */
+class StopwatchCircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
+
+ /** The size of the dot indicating the user's position within the reference lap. */
+ private val mDotRadius: Float
+
+ /** An amount to subtract from the true radius to account for drawing thicknesses. */
+ private val mRadiusOffset: Float
+
+ /** Used to scale the width of the marker to make it similarly visible on all screens. */
+ private val mScreenDensity: Float
+
+ /** The color indicating the remaining portion of the current lap. */
+ private val mRemainderColor: Int
+
+ /** The color indicating the completed portion of the lap. */
+ private val mCompletedColor: Int
+
+ /** The size of the stroke that paints the lap circle. */
+ private val mStrokeSize: Float
+
+ /** The size of the stroke that paints the marker for the end of the prior lap. */
+ private val mMarkerStrokeSize: Float
+
+ private val mPaint: Paint = Paint()
+ private val mFill: Paint = Paint()
+ private val mArcRect: RectF = RectF()
+
+ constructor(context: Context) : this(context, null) {
+ }
+
+ init {
+ val resources: Resources = context.getResources()
+ val dotDiameter: Float = resources.getDimension(R.dimen.circletimer_dot_size)
+
+ mDotRadius = dotDiameter / 2f
+ mScreenDensity = resources.getDisplayMetrics().density
+ mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size)
+ mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size)
+ mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize)
+
+ mRemainderColor = Color.WHITE
+ mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent)
+
+ mPaint.setAntiAlias(true)
+ mPaint.setStyle(Paint.Style.STROKE)
+
+ mFill.setAntiAlias(true)
+ mFill.setColor(mCompletedColor)
+ mFill.setStyle(Paint.Style.FILL)
+ }
+
+ /**
+ * Start the animation if it is not currently running.
+ */
+ fun update() {
+ postInvalidateOnAnimation()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ // Compute the size and location of the circle to be drawn.
+ val xCenter: Int = getWidth() / 2
+ val yCenter: Int = getHeight() / 2
+ val radius = min(xCenter, yCenter) - mRadiusOffset
+
+ // Reset old painting state.
+ mPaint.setColor(mRemainderColor)
+ mPaint.setStrokeWidth(mStrokeSize)
+ val laps = laps
+
+ // If a reference lap does not exist or should not be drawn, draw a simple white circle.
+ if (laps.isEmpty() || !DataModel.dataModel.canAddMoreLaps()) {
+ // Draw a complete white circle; no red arc required.
+ canvas.drawCircle(xCenter.toFloat(), yCenter.toFloat(), radius, mPaint)
+
+ // No need to continue animating the plain white circle.
+ return
+ }
+
+ // The first lap is the reference lap to which all future laps are compared.
+ val stopwatch = stopwatch
+ val lapCount = laps.size
+ val firstLap = laps[lapCount - 1]
+ val priorLap = laps[0]
+ val firstLapTime = firstLap.lapTime
+ val currentLapTime = stopwatch.totalTime - priorLap.accumulatedTime
+
+ // Draw a combination of red and white arcs to create a circle.
+ mArcRect.top = yCenter - radius
+ mArcRect.bottom = yCenter + radius
+ mArcRect.left = xCenter - radius
+ mArcRect.right = xCenter + radius
+ val redPercent = currentLapTime.toFloat() / firstLapTime.toFloat()
+ val whitePercent: Float = 1f - if (redPercent > 1) 1f else redPercent
+
+ // Draw a white arc to indicate the amount of reference lap that remains.
+ canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, whitePercent * 360, false, mPaint)
+
+ // Draw a red arc to indicate the amount of reference lap completed.
+ mPaint.setColor(mCompletedColor)
+ canvas.drawArc(mArcRect, 270f, redPercent * 360, false, mPaint)
+
+ // Starting on lap 2, a marker can be drawn indicating where the prior lap ended.
+ if (lapCount > 1) {
+ mPaint.setColor(mRemainderColor)
+ mPaint.setStrokeWidth(mMarkerStrokeSize)
+ val markerAngle = priorLap.lapTime.toFloat() / firstLapTime.toFloat() * 360
+ val startAngle = 270 + markerAngle
+ val sweepAngle = mScreenDensity * (360 / (radius * Math.PI)).toFloat()
+ canvas.drawArc(mArcRect, startAngle, sweepAngle, false, mPaint)
+ }
+
+ // Draw a red dot to indicate current position relative to reference lap.
+ val dotAngleDegrees = 270 + redPercent * 360
+ val dotAngleRadians = Math.toRadians(dotAngleDegrees.toDouble())
+ val dotX = xCenter + (radius * cos(dotAngleRadians)).toFloat()
+ val dotY = yCenter + (radius * sin(dotAngleRadians)).toFloat()
+ canvas.drawCircle(dotX, dotY, mDotRadius, mFill)
+
+ // If the stopwatch is not running it does not require continuous updates.
+ if (stopwatch.isRunning) {
+ postInvalidateOnAnimation()
+ }
+ }
+
+ private val stopwatch: Stopwatch
+ get() = DataModel.dataModel.stopwatch
+
+ private val laps: List<Lap>
+ get() = DataModel.dataModel.laps
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchFragment.java b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
deleted file mode 100644
index fe91b37..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchFragment.java
+++ /dev/null
@@ -1,740 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.stopwatch;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.drawable.GradientDrawable;
-import android.os.Bundle;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.core.graphics.ColorUtils;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.SimpleItemAnimator;
-import android.transition.TransitionManager;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.deskclock.AnimatorUtils;
-import com.android.deskclock.DeskClockFragment;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.StopwatchTextController;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Lap;
-import com.android.deskclock.data.Stopwatch;
-import com.android.deskclock.data.StopwatchListener;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.TabListener;
-import com.android.deskclock.uidata.UiDataModel;
-import com.android.deskclock.uidata.UiDataModel.Tab;
-
-import static android.R.attr.state_activated;
-import static android.R.attr.state_pressed;
-import static android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM;
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
-
-/**
- * Fragment that shows the stopwatch and recorded laps.
- */
-public final class StopwatchFragment extends DeskClockFragment {
-
- /** Milliseconds between redraws while running. */
- private static final int REDRAW_PERIOD_RUNNING = 25;
-
- /** Milliseconds between redraws while paused. */
- private static final int REDRAW_PERIOD_PAUSED = 500;
-
- /** Keep the screen on when this tab is selected. */
- private final TabListener mTabWatcher = new TabWatcher();
-
- /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */
- private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
-
- /** Updates the user interface in response to stopwatch changes. */
- private final StopwatchListener mStopwatchWatcher = new StopwatchWatcher();
-
- /** Draws a gradient over the bottom of the {@link #mLapsList} to reduce clash with the fab. */
- private GradientItemDecoration mGradientItemDecoration;
-
- /** The data source for {@link #mLapsList}. */
- private LapsAdapter mLapsAdapter;
-
- /** The layout manager for the {@link #mLapsAdapter}. */
- private LinearLayoutManager mLapsLayoutManager;
-
- /** Draws the reference lap while the stopwatch is running. */
- private StopwatchCircleView mTime;
-
- /** The View containing both TextViews of the stopwatch. */
- private View mStopwatchWrapper;
-
- /** Displays the recorded lap times. */
- private RecyclerView mLapsList;
-
- /** Displays the current stopwatch time (seconds and above only). */
- private TextView mMainTimeText;
-
- /** Displays the current stopwatch time (hundredths only). */
- private TextView mHundredthsTimeText;
-
- /** Formats and displays the text in the stopwatch. */
- private StopwatchTextController mStopwatchTextController;
-
- /** The public no-arg constructor required by all fragments. */
- public StopwatchFragment() {
- super(STOPWATCH);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
- mLapsAdapter = new LapsAdapter(getActivity());
- mLapsLayoutManager = new LinearLayoutManager(getActivity());
- mGradientItemDecoration = new GradientItemDecoration(getActivity());
-
- final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false);
- mTime = (StopwatchCircleView) v.findViewById(R.id.stopwatch_circle);
- mLapsList = (RecyclerView) v.findViewById(R.id.laps_list);
- ((SimpleItemAnimator) mLapsList.getItemAnimator()).setSupportsChangeAnimations(false);
- mLapsList.setLayoutManager(mLapsLayoutManager);
- mLapsList.addItemDecoration(mGradientItemDecoration);
-
- // In landscape layouts, the laps list can reach the top of the screen and thus can cause
- // a drop shadow to appear. The same is not true for portrait landscapes.
- if (Utils.isLandscape(getActivity())) {
- final ScrollPositionWatcher scrollPositionWatcher = new ScrollPositionWatcher();
- mLapsList.addOnLayoutChangeListener(scrollPositionWatcher);
- mLapsList.addOnScrollListener(scrollPositionWatcher);
- } else {
- setTabScrolledToTop(true);
- }
- mLapsList.setAdapter(mLapsAdapter);
-
- // Timer text serves as a virtual start/stop button.
- mMainTimeText = (TextView) v.findViewById(R.id.stopwatch_time_text);
- mHundredthsTimeText = (TextView) v.findViewById(R.id.stopwatch_hundredths_text);
- mStopwatchTextController = new StopwatchTextController(mMainTimeText, mHundredthsTimeText);
- mStopwatchWrapper = v.findViewById(R.id.stopwatch_time_wrapper);
-
- DataModel.getDataModel().addStopwatchListener(mStopwatchWatcher);
-
- mStopwatchWrapper.setOnClickListener(new TimeClickListener());
- if (mTime != null) {
- mStopwatchWrapper.setOnTouchListener(new CircleTouchListener());
- }
-
- final Context c = mMainTimeText.getContext();
- final int colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent);
- final int textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary);
- final ColorStateList timeTextColor = new ColorStateList(
- new int[][] { { -state_activated, -state_pressed }, {} },
- new int[] { textColorPrimary, colorAccent });
- mMainTimeText.setTextColor(timeTextColor);
- mHundredthsTimeText.setTextColor(timeTextColor);
-
- return v;
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- final Activity activity = getActivity();
- final Intent intent = activity.getIntent();
- if (intent != null) {
- final String action = intent.getAction();
- if (StopwatchService.ACTION_START_STOPWATCH.equals(action)) {
- DataModel.getDataModel().startStopwatch();
- // Consume the intent
- activity.setIntent(null);
- } else if (StopwatchService.ACTION_PAUSE_STOPWATCH.equals(action)) {
- DataModel.getDataModel().pauseStopwatch();
- // Consume the intent
- activity.setIntent(null);
- }
- }
-
- // Conservatively assume the data in the adapter has changed while the fragment was paused.
- mLapsAdapter.notifyDataSetChanged();
-
- // Synchronize the user interface with the data model.
- updateUI(FAB_AND_BUTTONS_IMMEDIATE);
-
- // Start watching for page changes away from this fragment.
- UiDataModel.getUiDataModel().addTabListener(mTabWatcher);
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // Stop all updates while the fragment is not visible.
- stopUpdatingTime();
-
- // Stop watching for page changes away from this fragment.
- UiDataModel.getUiDataModel().removeTabListener(mTabWatcher);
-
- // Release the wake lock if it is currently held.
- releaseWakeLock();
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- DataModel.getDataModel().removeStopwatchListener(mStopwatchWatcher);
- }
-
- @Override
- public void onFabClick(@NonNull ImageView fab) {
- toggleStopwatchState();
- }
-
- @Override
- public void onLeftButtonClick(@NonNull Button left) {
- doReset();
- }
-
- @Override
- public void onRightButtonClick(@NonNull Button right) {
- switch (getStopwatch().getState()) {
- case RUNNING:
- doAddLap();
- break;
- case PAUSED:
- doShare();
- break;
- }
- }
-
- private void updateFab(@NonNull ImageView fab, boolean animate) {
- if (getStopwatch().isRunning()) {
- if (animate) {
- fab.setImageResource(R.drawable.ic_play_pause_animation);
- } else {
- fab.setImageResource(R.drawable.ic_play_pause);
- }
- fab.setContentDescription(fab.getResources().getString(R.string.sw_pause_button));
- } else {
- if (animate) {
- fab.setImageResource(R.drawable.ic_pause_play_animation);
- } else {
- fab.setImageResource(R.drawable.ic_pause_play);
- }
- fab.setContentDescription(fab.getResources().getString(R.string.sw_start_button));
- }
- fab.setVisibility(VISIBLE);
- }
-
- public void onUpdateFab(@NonNull ImageView fab) {
- updateFab(fab, false);
- }
-
- @Override
- public void onMorphFab(@NonNull ImageView fab) {
- // Update the fab's drawable to match the current timer state.
- updateFab(fab, Utils.isNOrLater());
- // Animate the drawable.
- AnimatorUtils.startDrawableAnimation(fab);
- }
-
- @Override
- public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
- final Resources resources = getResources();
- left.setClickable(true);
- left.setText(R.string.sw_reset_button);
- left.setContentDescription(resources.getString(R.string.sw_reset_button));
-
- switch (getStopwatch().getState()) {
- case RESET:
- left.setVisibility(INVISIBLE);
- right.setClickable(true);
- right.setVisibility(INVISIBLE);
- break;
- case RUNNING:
- left.setVisibility(VISIBLE);
- final boolean canRecordLaps = canRecordMoreLaps();
- right.setText(R.string.sw_lap_button);
- right.setContentDescription(resources.getString(R.string.sw_lap_button));
- right.setClickable(canRecordLaps);
- right.setVisibility(canRecordLaps ? VISIBLE : INVISIBLE);
- break;
- case PAUSED:
- left.setVisibility(VISIBLE);
- right.setClickable(true);
- right.setVisibility(VISIBLE);
- right.setText(R.string.sw_share_button);
- right.setContentDescription(resources.getString(R.string.sw_share_button));
- break;
- }
- }
-
- /**
- * @param color the newly installed app window color
- */
- protected void onAppColorChanged(@ColorInt int color) {
- if (mGradientItemDecoration != null) {
- mGradientItemDecoration.updateGradientColors(color);
- }
- if (mLapsList != null) {
- mLapsList.invalidateItemDecorations();
- }
- }
-
- /**
- * Start the stopwatch.
- */
- private void doStart() {
- Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock);
- DataModel.getDataModel().startStopwatch();
- }
-
- /**
- * Pause the stopwatch.
- */
- private void doPause() {
- Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock);
- DataModel.getDataModel().pauseStopwatch();
- }
-
- /**
- * Reset the stopwatch.
- */
- private void doReset() {
- final Stopwatch.State priorState = getStopwatch().getState();
- Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock);
- DataModel.getDataModel().resetStopwatch();
- mMainTimeText.setAlpha(1f);
- mHundredthsTimeText.setAlpha(1f);
- if (priorState == Stopwatch.State.RUNNING) {
- updateFab(FAB_MORPH);
- }
- }
-
- /**
- * Send stopwatch time and lap times to an external sharing application.
- */
- private void doShare() {
- // Disable the fab buttons to avoid double-taps on the share button.
- updateFab(BUTTONS_DISABLE);
-
- final String[] subjects = getResources().getStringArray(R.array.sw_share_strings);
- final String subject = subjects[(int) (Math.random() * subjects.length)];
- final String text = mLapsAdapter.getShareText();
-
- @SuppressLint("InlinedApi")
- @SuppressWarnings("deprecation")
- final Intent shareIntent = new Intent(Intent.ACTION_SEND)
- .addFlags(Utils.isLOrLater() ? Intent.FLAG_ACTIVITY_NEW_DOCUMENT
- : Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
- .putExtra(Intent.EXTRA_SUBJECT, subject)
- .putExtra(Intent.EXTRA_TEXT, text)
- .setType("text/plain");
-
- final Context context = getActivity();
- final String title = context.getString(R.string.sw_share_button);
- final Intent shareChooserIntent = Intent.createChooser(shareIntent, title);
- try {
- context.startActivity(shareChooserIntent);
- } catch (ActivityNotFoundException anfe) {
- LogUtils.e("Cannot share lap data because no suitable receiving Activity exists");
- updateFab(BUTTONS_IMMEDIATE);
- }
- }
-
- /**
- * Record and add a new lap ending now.
- */
- private void doAddLap() {
- Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock);
-
- // Record a new lap.
- final Lap lap = mLapsAdapter.addLap();
- if (lap == null) {
- return;
- }
-
- // Update button states.
- updateFab(BUTTONS_IMMEDIATE);
-
- if (lap.getLapNumber() == 1) {
- // Child views from prior lap sets hang around and blit to the screen when adding the
- // first lap of the subsequent lap set. Remove those superfluous children here manually
- // to ensure they aren't seen as the first lap is drawn.
- mLapsList.removeAllViewsInLayout();
-
- if (mTime != null) {
- // Start animating the reference lap.
- mTime.update();
- }
-
- // Recording the first lap transitions the UI to display the laps list.
- showOrHideLaps(false);
- }
-
- // Ensure the newly added lap is visible on screen.
- mLapsList.scrollToPosition(0);
- }
-
- /**
- * Show or hide the list of laps.
- */
- private void showOrHideLaps(boolean clearLaps) {
- final ViewGroup sceneRoot = (ViewGroup) getView();
- if (sceneRoot == null) {
- return;
- }
-
- TransitionManager.beginDelayedTransition(sceneRoot);
-
- if (clearLaps) {
- mLapsAdapter.clearLaps();
- }
-
- final boolean lapsVisible = mLapsAdapter.getItemCount() > 0;
- mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE);
-
- if (Utils.isPortrait(getActivity())) {
- // When the lap list is visible, it includes the bottom padding. When it is absent the
- // appropriate bottom padding must be applied to the container.
- final Resources res = getResources();
- final int bottom = lapsVisible ? 0 : res.getDimensionPixelSize(R.dimen.fab_height);
- final int top = sceneRoot.getPaddingTop();
- final int left = sceneRoot.getPaddingLeft();
- final int right = sceneRoot.getPaddingRight();
- sceneRoot.setPadding(left, top, right, bottom);
- }
- }
-
- private void adjustWakeLock() {
- final boolean appInForeground = DataModel.getDataModel().isApplicationInForeground();
- if (getStopwatch().isRunning() && isTabSelected() && appInForeground) {
- getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- } else {
- releaseWakeLock();
- }
- }
-
- private void releaseWakeLock() {
- getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- }
-
- /**
- * Either pause or start the stopwatch based on its current state.
- */
- private void toggleStopwatchState() {
- if (getStopwatch().isRunning()) {
- doPause();
- } else {
- doStart();
- }
- }
-
- private Stopwatch getStopwatch() {
- return DataModel.getDataModel().getStopwatch();
- }
-
- private boolean canRecordMoreLaps() {
- return DataModel.getDataModel().canAddMoreLaps();
- }
-
- /**
- * Post the first runnable to update times within the UI. It will reschedule itself as needed.
- */
- private void startUpdatingTime() {
- // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
- stopUpdatingTime();
- mMainTimeText.post(mTimeUpdateRunnable);
- }
-
- /**
- * Remove the runnable that updates times within the UI.
- */
- private void stopUpdatingTime() {
- mMainTimeText.removeCallbacks(mTimeUpdateRunnable);
- }
-
- /**
- * Update all time displays based on a single snapshot of the stopwatch progress. This includes
- * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in
- * the list of laps.
- */
- private void updateTime() {
- // Compute the total time of the stopwatch.
- final Stopwatch stopwatch = getStopwatch();
- final long totalTime = stopwatch.getTotalTime();
- mStopwatchTextController.setTimeString(totalTime);
-
- // Update the current lap.
- final boolean currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0;
- if (!stopwatch.isReset() && currentLapIsVisible) {
- mLapsAdapter.updateCurrentLap(mLapsList, totalTime);
- }
- }
-
- /**
- * Synchronize the UI state with the model data.
- */
- private void updateUI(@UpdateFabFlag int updateTypes) {
- adjustWakeLock();
-
- // Draw the latest stopwatch and current lap times.
- updateTime();
-
- if (mTime != null) {
- mTime.update();
- }
-
- final Stopwatch stopwatch = getStopwatch();
- if (!stopwatch.isReset()) {
- startUpdatingTime();
- }
-
- // Adjust the visibility of the list of laps.
- showOrHideLaps(stopwatch.isReset());
-
- // Update button states.
- updateFab(updateTypes);
- }
-
- /**
- * This runnable periodically updates times throughout the UI. It stops these updates when the
- * stopwatch is no longer running.
- */
- private final class TimeUpdateRunnable implements Runnable {
- @Override
- public void run() {
- final long startTime = Utils.now();
-
- updateTime();
-
- // Blink text iff the stopwatch is paused and not pressed.
- final View touchTarget = mTime != null ? mTime : mStopwatchWrapper;
- final Stopwatch stopwatch = getStopwatch();
- final boolean blink = stopwatch.isPaused()
- && startTime % 1000 < 500
- && !touchTarget.isPressed();
-
- if (blink) {
- mMainTimeText.setAlpha(0f);
- mHundredthsTimeText.setAlpha(0f);
- } else {
- mMainTimeText.setAlpha(1f);
- mHundredthsTimeText.setAlpha(1f);
- }
-
- if (!stopwatch.isReset()) {
- final long period = stopwatch.isPaused()
- ? REDRAW_PERIOD_PAUSED
- : REDRAW_PERIOD_RUNNING;
- final long endTime = Utils.now();
- final long delay = Math.max(0, startTime + period - endTime);
- mMainTimeText.postDelayed(this, delay);
- }
- }
- }
-
- /**
- * Acquire or release the wake lock based on the tab state.
- */
- private final class TabWatcher implements TabListener {
- @Override
- public void selectedTabChanged(Tab oldSelectedTab, Tab newSelectedTab) {
- adjustWakeLock();
- }
- }
-
- /**
- * Update the user interface in response to a stopwatch change.
- */
- private class StopwatchWatcher implements StopwatchListener {
- @Override
- public void stopwatchUpdated(Stopwatch before, Stopwatch after) {
- if (after.isReset()) {
- // Ensure the drop shadow is hidden when the stopwatch is reset.
- setTabScrolledToTop(true);
- if (DataModel.getDataModel().isApplicationInForeground()) {
- updateUI(BUTTONS_IMMEDIATE);
- }
- return;
- }
- if (DataModel.getDataModel().isApplicationInForeground()) {
- updateUI(FAB_MORPH | BUTTONS_IMMEDIATE);
- }
- }
-
- @Override
- public void lapAdded(Lap lap) {
- }
- }
-
- /**
- * Toggles stopwatch state when user taps stopwatch.
- */
- private final class TimeClickListener implements View.OnClickListener {
- @Override
- public void onClick(View view) {
- if (getStopwatch().isRunning()) {
- DataModel.getDataModel().pauseStopwatch();
- } else {
- DataModel.getDataModel().startStopwatch();
- }
- }
- }
-
- /**
- * Checks if the user is pressing inside of the stopwatch circle.
- */
- private final class CircleTouchListener implements View.OnTouchListener {
- @Override
- public boolean onTouch(View view, MotionEvent event) {
- final int actionMasked = event.getActionMasked();
- if (actionMasked != MotionEvent.ACTION_DOWN) {
- return false;
- }
- final float rX = view.getWidth() / 2f;
- final float rY = (view.getHeight() - view.getPaddingBottom()) / 2f;
- final float r = Math.min(rX, rY);
-
- final float x = event.getX() - rX;
- final float y = event.getY() - rY;
-
- final boolean inCircle = Math.pow(x / r, 2.0) + Math.pow(y / r, 2.0) <= 1.0;
-
- // Consume the event if it is outside the circle
- return !inCircle;
- }
- }
-
- /**
- * Updates the vertical scroll state of this tab in the {@link UiDataModel} as the user scrolls
- * the recyclerview or when the size/position of elements within the recyclerview changes.
- */
- private final class ScrollPositionWatcher extends RecyclerView.OnScrollListener
- implements View.OnLayoutChangeListener {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- setTabScrolledToTop(Utils.isScrolledToTop(mLapsList));
- }
-
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- setTabScrolledToTop(Utils.isScrolledToTop(mLapsList));
- }
- }
-
- /**
- * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the
- * contrast between floating buttons and the laps list content.
- */
- private static final class GradientItemDecoration extends RecyclerView.ItemDecoration {
-
- // 0% - 25% of gradient length -> opacity changes from 0% to 50%
- // 25% - 90% of gradient length -> opacity changes from 50% to 100%
- // 90% - 100% of gradient length -> opacity remains at 100%
- private static final int[] ALPHAS = {
- 0x00, // 0%
- 0x1A, // 10%
- 0x33, // 20%
- 0x4D, // 30%
- 0x66, // 40%
- 0x80, // 50%
- 0x89, // 53.8%
- 0x93, // 57.6%
- 0x9D, // 61.5%
- 0xA7, // 65.3%
- 0xB1, // 69.2%
- 0xBA, // 73.0%
- 0xC4, // 76.9%
- 0xCE, // 80.7%
- 0xD8, // 84.6%
- 0xE2, // 88.4%
- 0xEB, // 92.3%
- 0xF5, // 96.1%
- 0xFF, // 100%
- 0xFF, // 100%
- 0xFF, // 100%
- };
-
- /**
- * A reusable array of control point colors that define the gradient. It is based on the
- * background color of the window and thus recomputed each time that color is changed.
- */
- private final int[] mGradientColors = new int[ALPHAS.length];
-
- /** The drawable that produces the tinting gradient effect of this decoration. */
- private final GradientDrawable mGradient = new GradientDrawable();
-
- /** The height of the gradient; sized relative to the fab height. */
- private final int mGradientHeight;
-
- GradientItemDecoration(Context context) {
- mGradient.setOrientation(TOP_BOTTOM);
- updateGradientColors(ThemeUtils.resolveColor(context, android.R.attr.windowBackground));
-
- final Resources resources = context.getResources();
- final float fabHeight = resources.getDimensionPixelSize(R.dimen.fab_height);
- mGradientHeight = Math.round(fabHeight * 1.2f);
- }
-
- @Override
- public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
- super.onDrawOver(c, parent, state);
-
- final int w = parent.getWidth();
- final int h = parent.getHeight();
-
- mGradient.setBounds(0, h - mGradientHeight, w, h);
- mGradient.draw(c);
- }
-
- /**
- * Given a {@code baseColor}, compute a gradient of tinted colors that define the fade
- * effect to apply to the bottom of the lap list.
- *
- * @param baseColor a base color to which the gradient tint should be applied
- */
- void updateGradientColors(@ColorInt int baseColor) {
- // Compute the tinted colors that form the gradient.
- for (int i = 0; i < mGradientColors.length; i++) {
- mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i]);
- }
-
- // Set the gradient colors into the drawable.
- mGradient.setColors(mGradientColors);
- }
- }
-}
diff --git a/src/com/android/deskclock/stopwatch/StopwatchFragment.kt b/src/com/android/deskclock/stopwatch/StopwatchFragment.kt
new file mode 100644
index 0000000..02a7259
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchFragment.kt
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.R.attr.state_activated
+import android.R.attr.state_pressed
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.content.res.Resources
+import android.graphics.Canvas
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM
+import android.os.Bundle
+import android.transition.TransitionManager
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.core.graphics.ColorUtils
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.SimpleItemAnimator
+
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.DeskClockFragment
+import com.android.deskclock.FabContainer
+import com.android.deskclock.FabContainer.UpdateFabFlag
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Lap
+import com.android.deskclock.data.Stopwatch
+import com.android.deskclock.data.StopwatchListener
+import com.android.deskclock.events.Events
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.StopwatchTextController
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.uidata.TabListener
+import com.android.deskclock.uidata.UiDataModel
+
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.pow
+import kotlin.math.roundToInt
+
+/**
+ * Fragment that shows the stopwatch and recorded laps.
+ */
+class StopwatchFragment : DeskClockFragment(UiDataModel.Tab.STOPWATCH) {
+
+ /** Keep the screen on when this tab is selected. */
+ private val mTabWatcher: TabListener = TabWatcher()
+
+ /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */
+ private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
+
+ /** Updates the user interface in response to stopwatch changes. */
+ private val mStopwatchWatcher: StopwatchListener = StopwatchWatcher()
+
+ /** Draws a gradient over the bottom of the [.mLapsList] to reduce clash with the fab. */
+ private var mGradientItemDecoration: GradientItemDecoration? = null
+
+ /** The data source for [.mLapsList]. */
+ private lateinit var mLapsAdapter: LapsAdapter
+
+ /** The layout manager for the [.mLapsAdapter]. */
+ private lateinit var mLapsLayoutManager: LinearLayoutManager
+
+ /** Draws the reference lap while the stopwatch is running. */
+ private var mTime: StopwatchCircleView? = null
+
+ /** The View containing both TextViews of the stopwatch. */
+ private lateinit var mStopwatchWrapper: View
+
+ /** Displays the recorded lap times. */
+ private lateinit var mLapsList: RecyclerView
+
+ /** Displays the current stopwatch time (seconds and above only). */
+ private lateinit var mMainTimeText: TextView
+
+ /** Displays the current stopwatch time (hundredths only). */
+ private lateinit var mHundredthsTimeText: TextView
+
+ /** Formats and displays the text in the stopwatch. */
+ private lateinit var mStopwatchTextController: StopwatchTextController
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ state: Bundle?
+ ): View {
+ mLapsAdapter = LapsAdapter(requireActivity())
+ mLapsLayoutManager = LinearLayoutManager(requireActivity())
+ mGradientItemDecoration = GradientItemDecoration(requireActivity())
+
+ val v: View = inflater.inflate(R.layout.stopwatch_fragment, container, false)
+ mTime = v.findViewById(R.id.stopwatch_circle)
+ mLapsList = v.findViewById(R.id.laps_list) as RecyclerView
+ (mLapsList.getItemAnimator() as SimpleItemAnimator).setSupportsChangeAnimations(false)
+ mLapsList.setLayoutManager(mLapsLayoutManager)
+ mLapsList.addItemDecoration(mGradientItemDecoration!!)
+
+ // In landscape layouts, the laps list can reach the top of the screen and thus can cause
+ // a drop shadow to appear. The same is not true for portrait landscapes.
+ if (Utils.isLandscape(requireActivity())) {
+ val scrollPositionWatcher = ScrollPositionWatcher()
+ mLapsList.addOnLayoutChangeListener(scrollPositionWatcher)
+ mLapsList.addOnScrollListener(scrollPositionWatcher)
+ } else {
+ setTabScrolledToTop(true)
+ }
+ mLapsList.setAdapter(mLapsAdapter)
+
+ // Timer text serves as a virtual start/stop button.
+ mMainTimeText = v.findViewById(R.id.stopwatch_time_text) as TextView
+ mHundredthsTimeText = v.findViewById(R.id.stopwatch_hundredths_text) as TextView
+ mStopwatchTextController = StopwatchTextController(mMainTimeText, mHundredthsTimeText)
+ mStopwatchWrapper = v.findViewById(R.id.stopwatch_time_wrapper)
+
+ DataModel.dataModel.addStopwatchListener(mStopwatchWatcher)
+
+ mStopwatchWrapper.setOnClickListener(TimeClickListener())
+ if (mTime != null) {
+ mStopwatchWrapper.setOnTouchListener(CircleTouchListener())
+ }
+
+ val c: Context = mMainTimeText.getContext()
+ val colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent)
+ val textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary)
+ val timeTextColor =
+ ColorStateList(
+ arrayOf(intArrayOf(-state_activated, -state_pressed), intArrayOf()),
+ intArrayOf(textColorPrimary, colorAccent)
+ )
+ mMainTimeText.setTextColor(timeTextColor)
+ mHundredthsTimeText.setTextColor(timeTextColor)
+
+ return v
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ val activity: Activity = requireActivity()
+ val intent: Intent? = activity.getIntent()
+ if (intent != null) {
+ val action: String? = intent.getAction()
+ if (StopwatchService.Companion.ACTION_START_STOPWATCH == action) {
+ DataModel.dataModel.startStopwatch()
+ // Consume the intent
+ activity.setIntent(null)
+ } else if (StopwatchService.Companion.ACTION_PAUSE_STOPWATCH == action) {
+ DataModel.dataModel.pauseStopwatch()
+ // Consume the intent
+ activity.setIntent(null)
+ }
+ }
+
+ // Conservatively assume the data in the adapter has changed while the fragment was paused.
+ mLapsAdapter.notifyDataSetChanged()
+
+ // Synchronize the user interface with the data model.
+ updateUI(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+ // Start watching for page changes away from this fragment.
+ UiDataModel.uiDataModel.addTabListener(mTabWatcher)
+ }
+
+ override fun onStop() {
+ super.onStop()
+
+ // Stop all updates while the fragment is not visible.
+ stopUpdatingTime()
+
+ // Stop watching for page changes away from this fragment.
+ UiDataModel.uiDataModel.removeTabListener(mTabWatcher)
+
+ // Release the wake lock if it is currently held.
+ releaseWakeLock()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+
+ DataModel.dataModel.removeStopwatchListener(mStopwatchWatcher)
+ }
+
+ override fun onFabClick(fab: ImageView) {
+ toggleStopwatchState()
+ }
+
+ override fun onLeftButtonClick(left: Button) {
+ doReset()
+ }
+
+ override fun onRightButtonClick(right: Button) {
+ when (stopwatch.state) {
+ Stopwatch.State.RUNNING -> doAddLap()
+ Stopwatch.State.PAUSED -> doShare()
+ Stopwatch.State.RESET -> {
+ }
+ null -> {
+ }
+ }
+ }
+
+ private fun updateFab(fab: ImageView, animate: Boolean) {
+ if (stopwatch.isRunning) {
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_play_pause_animation)
+ } else {
+ fab.setImageResource(R.drawable.ic_play_pause)
+ }
+ fab.setContentDescription(fab.getResources().getString(R.string.sw_pause_button))
+ } else {
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_pause_play_animation)
+ } else {
+ fab.setImageResource(R.drawable.ic_pause_play)
+ }
+ fab.setContentDescription(fab.getResources().getString(R.string.sw_start_button))
+ }
+ fab.setVisibility(VISIBLE)
+ }
+
+ override fun onUpdateFab(fab: ImageView) {
+ updateFab(fab, false)
+ }
+
+ override fun onMorphFab(fab: ImageView) {
+ // Update the fab's drawable to match the current timer state.
+ updateFab(fab, Utils.isNOrLater)
+ // Animate the drawable.
+ AnimatorUtils.startDrawableAnimation(fab)
+ }
+
+ override fun onUpdateFabButtons(left: Button, right: Button) {
+ val resources: Resources = getResources()
+ left.setClickable(true)
+ left.setText(R.string.sw_reset_button)
+ left.setContentDescription(resources.getString(R.string.sw_reset_button))
+
+ when (stopwatch.state) {
+ Stopwatch.State.RESET -> {
+ left.setVisibility(INVISIBLE)
+ right.setClickable(true)
+ right.setVisibility(INVISIBLE)
+ }
+ Stopwatch.State.RUNNING -> {
+ left.setVisibility(VISIBLE)
+ val canRecordLaps = canRecordMoreLaps()
+ right.setText(R.string.sw_lap_button)
+ right.setContentDescription(resources.getString(R.string.sw_lap_button))
+ right.setClickable(canRecordLaps)
+ right.setVisibility(if (canRecordLaps) VISIBLE else INVISIBLE)
+ }
+ Stopwatch.State.PAUSED -> {
+ left.setVisibility(VISIBLE)
+ right.setClickable(true)
+ right.setVisibility(VISIBLE)
+ right.setText(R.string.sw_share_button)
+ right.setContentDescription(resources.getString(R.string.sw_share_button))
+ }
+ null -> {
+ }
+ }
+ }
+
+ /**
+ * @param color the newly installed app window color
+ */
+ override fun onAppColorChanged(@ColorInt color: Int) {
+ mGradientItemDecoration?.updateGradientColors(color)
+ mLapsList.invalidateItemDecorations()
+ }
+
+ /**
+ * Start the stopwatch.
+ */
+ private fun doStart() {
+ Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock)
+ DataModel.dataModel.startStopwatch()
+ }
+
+ /**
+ * Pause the stopwatch.
+ */
+ private fun doPause() {
+ Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock)
+ DataModel.dataModel.pauseStopwatch()
+ }
+
+ /**
+ * Reset the stopwatch.
+ */
+ private fun doReset() {
+ val priorState = stopwatch.state
+ Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock)
+ DataModel.dataModel.resetStopwatch()
+ mMainTimeText.setAlpha(1f)
+ mHundredthsTimeText.setAlpha(1f)
+ if (priorState == Stopwatch.State.RUNNING) {
+ updateFab(FabContainer.FAB_MORPH)
+ }
+ }
+
+ /**
+ * Send stopwatch time and lap times to an external sharing application.
+ */
+ private fun doShare() {
+ // Disable the fab buttons to avoid double-taps on the share button.
+ updateFab(FabContainer.BUTTONS_DISABLE)
+
+ val subjects: Array<String> = getResources().getStringArray(R.array.sw_share_strings)
+ val subject = subjects[(Math.random() * subjects.size).toInt()]
+ val text = mLapsAdapter.shareText
+
+ @SuppressLint("InlinedApi")
+ val shareIntent: Intent = Intent(Intent.ACTION_SEND)
+ .addFlags(if (Utils.isLOrLater) {
+ Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+ } else {
+ Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
+ })
+ .putExtra(Intent.EXTRA_SUBJECT, subject)
+ .putExtra(Intent.EXTRA_TEXT, text)
+ .setType("text/plain")
+
+ val context: Context = requireActivity()
+ val title: String = context.getString(R.string.sw_share_button)
+ val shareChooserIntent: Intent = Intent.createChooser(shareIntent, title)
+ try {
+ context.startActivity(shareChooserIntent)
+ } catch (anfe: ActivityNotFoundException) {
+ LogUtils.e("Cannot share lap data because no suitable receiving Activity exists")
+ updateFab(FabContainer.BUTTONS_IMMEDIATE)
+ }
+ }
+
+ /**
+ * Record and add a new lap ending now.
+ */
+ private fun doAddLap() {
+ Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock)
+
+ // Record a new lap.
+ val lap = mLapsAdapter.addLap() ?: return
+
+ // Update button states.
+ updateFab(FabContainer.BUTTONS_IMMEDIATE)
+ if (lap.lapNumber == 1) {
+ // Child views from prior lap sets hang around and blit to the screen when adding the
+ // first lap of the subsequent lap set. Remove those superfluous children here manually
+ // to ensure they aren't seen as the first lap is drawn.
+ mLapsList.removeAllViewsInLayout()
+ if (mTime != null) {
+ // Start animating the reference lap.
+ mTime!!.update()
+ }
+
+ // Recording the first lap transitions the UI to display the laps list.
+ showOrHideLaps(false)
+ }
+
+ // Ensure the newly added lap is visible on screen.
+ mLapsList.scrollToPosition(0)
+ }
+
+ /**
+ * Show or hide the list of laps.
+ */
+ private fun showOrHideLaps(clearLaps: Boolean) {
+ val sceneRoot: ViewGroup = getView() as ViewGroup? ?: return
+
+ TransitionManager.beginDelayedTransition(sceneRoot)
+
+ if (clearLaps) {
+ mLapsAdapter.clearLaps()
+ }
+
+ val lapsVisible = mLapsAdapter.getItemCount() > 0
+ mLapsList.setVisibility(if (lapsVisible) VISIBLE else GONE)
+
+ if (Utils.isPortrait(requireActivity())) {
+ // When the lap list is visible, it includes the bottom padding. When it is absent the
+ // appropriate bottom padding must be applied to the container.
+ val res: Resources = getResources()
+ val bottom = if (lapsVisible) 0 else res.getDimensionPixelSize(R.dimen.fab_height)
+ val top: Int = sceneRoot.getPaddingTop()
+ val left: Int = sceneRoot.getPaddingLeft()
+ val right: Int = sceneRoot.getPaddingRight()
+ sceneRoot.setPadding(left, top, right, bottom)
+ }
+ }
+
+ private fun adjustWakeLock() {
+ val appInForeground = DataModel.dataModel.isApplicationInForeground
+ if (stopwatch.isRunning && isTabSelected && appInForeground) {
+ requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ } else {
+ releaseWakeLock()
+ }
+ }
+
+ private fun releaseWakeLock() {
+ requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+ }
+
+ /**
+ * Either pause or start the stopwatch based on its current state.
+ */
+ private fun toggleStopwatchState() {
+ if (stopwatch.isRunning) {
+ doPause()
+ } else {
+ doStart()
+ }
+ }
+
+ private val stopwatch: Stopwatch
+ get() = DataModel.dataModel.stopwatch
+
+ private fun canRecordMoreLaps(): Boolean = DataModel.dataModel.canAddMoreLaps()
+
+ /**
+ * Post the first runnable to update times within the UI. It will reschedule itself as needed.
+ */
+ private fun startUpdatingTime() {
+ // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
+ stopUpdatingTime()
+ mMainTimeText.post(mTimeUpdateRunnable)
+ }
+
+ /**
+ * Remove the runnable that updates times within the UI.
+ */
+ private fun stopUpdatingTime() {
+ mMainTimeText.removeCallbacks(mTimeUpdateRunnable)
+ }
+
+ /**
+ * Update all time displays based on a single snapshot of the stopwatch progress. This includes
+ * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in
+ * the list of laps.
+ */
+ private fun updateTime() {
+ // Compute the total time of the stopwatch.
+ val stopwatch = stopwatch
+ val totalTime = stopwatch.totalTime
+ mStopwatchTextController.setTimeString(totalTime)
+
+ // Update the current lap.
+ val currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0
+ if (!stopwatch.isReset && currentLapIsVisible) {
+ mLapsAdapter.updateCurrentLap(mLapsList, totalTime)
+ }
+ }
+
+ /**
+ * Synchronize the UI state with the model data.
+ */
+ private fun updateUI(@UpdateFabFlag updateTypes: Int) {
+ adjustWakeLock()
+
+ // Draw the latest stopwatch and current lap times.
+ updateTime()
+ if (mTime != null) {
+ mTime!!.update()
+ }
+ val stopwatch = stopwatch
+ if (!stopwatch.isReset) {
+ startUpdatingTime()
+ }
+
+ // Adjust the visibility of the list of laps.
+ showOrHideLaps(stopwatch.isReset)
+
+ // Update button states.
+ updateFab(updateTypes)
+ }
+
+ /**
+ * This runnable periodically updates times throughout the UI. It stops these updates when the
+ * stopwatch is no longer running.
+ */
+ private inner class TimeUpdateRunnable : Runnable {
+ override fun run() {
+ val startTime = Utils.now()
+ updateTime()
+
+ // Blink text iff the stopwatch is paused and not pressed.
+ val touchTarget: View = if (mTime != null) mTime!! else mStopwatchWrapper
+ val stopwatch = stopwatch
+ val blink = (stopwatch.isPaused && startTime % 1000 < 500 && !touchTarget.isPressed())
+
+ if (blink) {
+ mMainTimeText.setAlpha(0f)
+ mHundredthsTimeText.setAlpha(0f)
+ } else {
+ mMainTimeText.setAlpha(1f)
+ mHundredthsTimeText.setAlpha(1f)
+ }
+
+ if (!stopwatch.isReset) {
+ val period = (if (stopwatch.isPaused) {
+ REDRAW_PERIOD_PAUSED
+ } else {
+ REDRAW_PERIOD_RUNNING
+ }).toLong()
+ val endTime = Utils.now()
+ val delay: Long = max(0, startTime + period - endTime).toLong()
+ mMainTimeText.postDelayed(this, delay)
+ }
+ }
+ }
+
+ /**
+ * Acquire or release the wake lock based on the tab state.
+ */
+ private inner class TabWatcher : TabListener {
+ override fun selectedTabChanged(
+ oldSelectedTab: UiDataModel.Tab,
+ newSelectedTab: UiDataModel.Tab
+ ) {
+ adjustWakeLock()
+ }
+ }
+
+ /**
+ * Update the user interface in response to a stopwatch change.
+ */
+ private inner class StopwatchWatcher : StopwatchListener {
+ override fun stopwatchUpdated(before: Stopwatch, after: Stopwatch) {
+ if (after.isReset) {
+ // Ensure the drop shadow is hidden when the stopwatch is reset.
+ setTabScrolledToTop(true)
+ if (DataModel.dataModel.isApplicationInForeground) {
+ updateUI(FabContainer.BUTTONS_IMMEDIATE)
+ }
+ return
+ }
+ if (DataModel.dataModel.isApplicationInForeground) {
+ updateUI(FabContainer.FAB_MORPH or FabContainer.BUTTONS_IMMEDIATE)
+ }
+ }
+
+ override fun lapAdded(lap: Lap) {
+ }
+ }
+
+ /**
+ * Toggles stopwatch state when user taps stopwatch.
+ */
+ private inner class TimeClickListener : View.OnClickListener {
+
+ override fun onClick(view: View?) {
+ if (stopwatch.isRunning) {
+ DataModel.dataModel.pauseStopwatch()
+ } else {
+ DataModel.dataModel.startStopwatch()
+ }
+ }
+ }
+
+ /**
+ * Checks if the user is pressing inside of the stopwatch circle.
+ */
+ private inner class CircleTouchListener : View.OnTouchListener {
+
+ override fun onTouch(view: View, event: MotionEvent): Boolean {
+ val actionMasked: Int = event.getActionMasked()
+ if (actionMasked != MotionEvent.ACTION_DOWN) {
+ return false
+ }
+ val rX: Float = view.getWidth() / 2f
+ val rY: Float = (view.getHeight() - view.getPaddingBottom()) / 2f
+ val r = min(rX, rY)
+
+ val x: Float = event.getX() - rX
+ val y: Float = event.getY() - rY
+
+ val inCircle = (x / r.toDouble()).pow(2.0) + (y / r.toDouble()).pow(2.0) <= 1.0
+
+ // Consume the event if it is outside the circle
+ return !inCircle
+ }
+ }
+
+ /**
+ * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls
+ * the recyclerview or when the size/position of elements within the recyclerview changes.
+ */
+ private inner class ScrollPositionWatcher :
+ RecyclerView.OnScrollListener(), View.OnLayoutChangeListener {
+
+ override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+ setTabScrolledToTop(Utils.isScrolledToTop(mLapsList))
+ }
+
+ override fun onLayoutChange(
+ v: View?,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
+ ) {
+ setTabScrolledToTop(Utils.isScrolledToTop(mLapsList))
+ }
+ }
+
+ /**
+ * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the
+ * contrast between floating buttons and the laps list content.
+ */
+ private class GradientItemDecoration internal constructor(context: Context)
+ : RecyclerView.ItemDecoration() {
+
+ /**
+ * A reusable array of control point colors that define the gradient. It is based on the
+ * background color of the window and thus recomputed each time that color is changed.
+ */
+ private val mGradientColors = IntArray(ALPHAS.size)
+
+ /** The drawable that produces the tinting gradient effect of this decoration. */
+ private val mGradient: GradientDrawable = GradientDrawable()
+
+ /** The height of the gradient; sized relative to the fab height. */
+ private val mGradientHeight: Int
+
+ init {
+ mGradient.setOrientation(TOP_BOTTOM)
+ updateGradientColors(ThemeUtils.resolveColor(context, android.R.attr.windowBackground))
+
+ val resources: Resources = context.getResources()
+ val fabHeight: Int = resources.getDimensionPixelSize(R.dimen.fab_height)
+ mGradientHeight = (fabHeight * 1.2f).roundToInt()
+ }
+
+ override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+ super.onDrawOver(c, parent, state)
+
+ val w: Int = parent.getWidth()
+ val h: Int = parent.getHeight()
+
+ mGradient.setBounds(0, h - mGradientHeight, w, h)
+ mGradient.draw(c)
+ }
+
+ /**
+ * Given a `baseColor`, compute a gradient of tinted colors that define the fade
+ * effect to apply to the bottom of the lap list.
+ *
+ * @param baseColor a base color to which the gradient tint should be applied
+ */
+ fun updateGradientColors(@ColorInt baseColor: Int) {
+ // Compute the tinted colors that form the gradient.
+ mGradientColors.indices.forEach { i ->
+ mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i])
+ }
+
+ // Set the gradient colors into the drawable.
+ mGradient.setColors(mGradientColors)
+ }
+
+ companion object {
+ // 0% - 25% of gradient length -> opacity changes from 0% to 50%
+ // 25% - 90% of gradient length -> opacity changes from 50% to 100%
+ // 90% - 100% of gradient length -> opacity remains at 100%
+ private val ALPHAS = intArrayOf(
+ 0x00, // 0%
+ 0x1A, // 10%
+ 0x33, // 20%
+ 0x4D, // 30%
+ 0x66, // 40%
+ 0x80, // 50%
+ 0x89, // 53.8%
+ 0x93, // 57.6%
+ 0x9D, // 61.5%
+ 0xA7, // 65.3%
+ 0xB1, // 69.2%
+ 0xBA, // 73.0%
+ 0xC4, // 76.9%
+ 0xCE, // 80.7%
+ 0xD8, // 84.6%
+ 0xE2, // 88.4%
+ 0xEB, // 92.3%
+ 0xF5, // 96.1%
+ 0xFF, // 100%
+ 0xFF, // 100%
+ 0xFF)
+ }
+ }
+
+ companion object {
+ /** Milliseconds between redraws while running. */
+ private const val REDRAW_PERIOD_RUNNING = 25
+
+ /** Milliseconds between redraws while paused. */
+ private const val REDRAW_PERIOD_PAUSED = 500
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java
deleted file mode 100644
index 44113b0..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.stopwatch;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.R;
-
-import static android.view.View.MeasureSpec.AT_MOST;
-import static android.view.View.MeasureSpec.EXACTLY;
-import static android.view.View.MeasureSpec.UNSPECIFIED;
-
-/**
- * Dynamically apportions size the stopwatch circle depending on the preferred width of the laps
- * list and the container size. Layouts fall into two different buckets:
- *
- * When the width of the laps list is less than half the container width, the laps list and
- * stopwatch display are each centered within half the container.
- * <pre>
- * ---------------------------------------------------------------------------
- * | | Lap 5 |
- * | | Lap 4 |
- * | 21:45.67 | Lap 3 |
- * | | Lap 2 |
- * | | Lap 1 |
- * ---------------------------------------------------------------------------
- * </pre>
- *
- * When the width of the laps list is greater than half the container width, the laps list is
- * granted all of the space it requires and the stopwatch display is centered within the remaining
- * container width.
- * <pre>
- * ---------------------------------------------------------------------------
- * | | Lap 5 |
- * | | Lap 4 |
- * | 21:45.67 | Lap 3 |
- * | | Lap 2 |
- * | | Lap 1 |
- * ---------------------------------------------------------------------------
- * </pre>
- */
-public class StopwatchLandscapeLayout extends ViewGroup {
-
- private View mLapsListView;
- private View mStopwatchView;
-
- public StopwatchLandscapeLayout(Context context) {
- super(context);
- }
-
- public StopwatchLandscapeLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public StopwatchLandscapeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- mLapsListView = findViewById(R.id.laps_list);
- mStopwatchView = findViewById(R.id.stopwatch_time_wrapper);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int height = MeasureSpec.getSize(heightMeasureSpec);
- final int width = MeasureSpec.getSize(widthMeasureSpec);
- final int halfWidth = width / 2;
-
- final int minWidthSpec = MeasureSpec.makeMeasureSpec(width, UNSPECIFIED);
- final int maxHeightSpec = MeasureSpec.makeMeasureSpec(height, AT_MOST);
-
- // First determine the width of the laps list.
- final int lapsListWidth;
- if (mLapsListView != null && mLapsListView.getVisibility() != GONE) {
- // Measure the intrinsic size of the laps list.
- mLapsListView.measure(minWidthSpec, maxHeightSpec);
-
- // Actual laps list width is the larger of half the container and its intrinsic width.
- lapsListWidth = Math.max(mLapsListView.getMeasuredWidth(), halfWidth);
- final int lapsListWidthSpec = MeasureSpec.makeMeasureSpec(lapsListWidth, EXACTLY);
- mLapsListView.measure(lapsListWidthSpec, maxHeightSpec);
- } else {
- lapsListWidth = 0;
- }
-
- // Stopwatch timer consumes the remaining width of container not granted to laps list.
- final int stopwatchWidth = width - lapsListWidth;
- final int stopwatchWidthSpec = MeasureSpec.makeMeasureSpec(stopwatchWidth, EXACTLY);
- mStopwatchView.measure(stopwatchWidthSpec, maxHeightSpec);
-
- // Record the measured size of this container.
- setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- // Compute the space available for layout.
- final int left = getPaddingLeft();
- final int top = getPaddingTop();
- final int right = getWidth() - getPaddingRight();
- final int bottom = getHeight() - getPaddingBottom();
- final int width = right - left;
- final int height = bottom - top;
- final int halfHeight = height / 2;
- final boolean isLTR = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
-
- final int lapsListWidth;
- if (mLapsListView != null && mLapsListView.getVisibility() != GONE) {
- // Layout the laps list, centering it vertically.
- lapsListWidth = mLapsListView.getMeasuredWidth();
- final int lapsListHeight = mLapsListView.getMeasuredHeight();
- final int lapsListTop = top + halfHeight - (lapsListHeight / 2);
- final int lapsListBottom = lapsListTop + lapsListHeight;
- final int lapsListLeft;
- final int lapsListRight;
- if (isLTR) {
- lapsListLeft = right - lapsListWidth;
- lapsListRight = right;
- } else {
- lapsListLeft = left;
- lapsListRight = left + lapsListWidth;
- }
-
- mLapsListView.layout(lapsListLeft, lapsListTop, lapsListRight, lapsListBottom);
- } else {
- lapsListWidth = 0;
- }
-
- // Layout the stopwatch, centering it horizontally and vertically.
- final int stopwatchWidth = mStopwatchView.getMeasuredWidth();
- final int stopwatchHeight = mStopwatchView.getMeasuredHeight();
- final int stopwatchTop = top + halfHeight - (stopwatchHeight / 2);
- final int stopwatchBottom = stopwatchTop + stopwatchHeight;
- final int stopwatchLeft;
- final int stopwatchRight;
- if (isLTR) {
- stopwatchLeft = left + ((width - lapsListWidth - stopwatchWidth) / 2);
- stopwatchRight = stopwatchLeft + stopwatchWidth;
- } else {
- stopwatchRight = right - ((width - lapsListWidth - stopwatchWidth) / 2);
- stopwatchLeft = stopwatchRight - stopwatchWidth;
- }
-
- mStopwatchView.layout(stopwatchLeft, stopwatchTop, stopwatchRight, stopwatchBottom);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.kt b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.kt
new file mode 100644
index 0000000..1bc2f7e
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchLandscapeLayout.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.view.View.MeasureSpec.AT_MOST
+import android.view.View.MeasureSpec.EXACTLY
+import android.view.View.MeasureSpec.UNSPECIFIED
+import android.view.ViewGroup
+
+import com.android.deskclock.R
+
+import kotlin.math.max
+
+/**
+ * Dynamically apportions size the stopwatch circle depending on the preferred width of the laps
+ * list and the container size. Layouts fall into two different buckets:
+ *
+ * When the width of the laps list is less than half the container width, the laps list and
+ * stopwatch display are each centered within half the container.
+ * <pre>
+ * ---------------------------------------------------------------------------
+ * | | Lap 5 |
+ * | | Lap 4 |
+ * | 21:45.67 | Lap 3 |
+ * | | Lap 2 |
+ * | | Lap 1 |
+ * ---------------------------------------------------------------------------
+</pre> *
+ *
+ * When the width of the laps list is greater than half the container width, the laps list is
+ * granted all of the space it requires and the stopwatch display is centered within the remaining
+ * container width.
+ * <pre>
+ * ---------------------------------------------------------------------------
+ * | | Lap 5 |
+ * | | Lap 4 |
+ * | 21:45.67 | Lap 3 |
+ * | | Lap 2 |
+ * | | Lap 1 |
+ * ---------------------------------------------------------------------------
+</pre> *
+ */
+class StopwatchLandscapeLayout : ViewGroup {
+ private var mLapsListView: View? = null
+ private lateinit var mStopwatchView: View
+
+ constructor(context: Context?) : super(context) {
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int)
+ : super(context, attrs, defStyleAttr) {
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ mLapsListView = findViewById(R.id.laps_list)
+ mStopwatchView = findViewById(R.id.stopwatch_time_wrapper)
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val height: Int = MeasureSpec.getSize(heightMeasureSpec)
+ val width: Int = MeasureSpec.getSize(widthMeasureSpec)
+ val halfWidth = width / 2
+
+ val minWidthSpec: Int = MeasureSpec.makeMeasureSpec(width, UNSPECIFIED)
+ val maxHeightSpec: Int = MeasureSpec.makeMeasureSpec(height, AT_MOST)
+
+ // First determine the width of the laps list.
+ val lapsListWidth: Int
+ val lapsListView = mLapsListView
+ if (lapsListView != null && lapsListView.getVisibility() != GONE) {
+ // Measure the intrinsic size of the laps list.
+ lapsListView.measure(minWidthSpec, maxHeightSpec)
+
+ // Actual laps list width is the larger of half the container and its intrinsic width.
+ lapsListWidth = max(lapsListView.getMeasuredWidth(), halfWidth)
+ val lapsListWidthSpec: Int = MeasureSpec.makeMeasureSpec(lapsListWidth, EXACTLY)
+ lapsListView.measure(lapsListWidthSpec, maxHeightSpec)
+ } else {
+ lapsListWidth = 0
+ }
+
+ // Stopwatch timer consumes the remaining width of container not granted to laps list.
+ val stopwatchWidth = width - lapsListWidth
+ val stopwatchWidthSpec: Int = MeasureSpec.makeMeasureSpec(stopwatchWidth, EXACTLY)
+ mStopwatchView.measure(stopwatchWidthSpec, maxHeightSpec)
+
+ // Record the measured size of this container.
+ setMeasuredDimension(widthMeasureSpec, heightMeasureSpec)
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ // Compute the space available for layout.
+ val left: Int = getPaddingLeft()
+ val top: Int = getPaddingTop()
+ val right: Int = getWidth() - getPaddingRight()
+ val bottom: Int = getHeight() - getPaddingBottom()
+ val width = right - left
+ val height = bottom - top
+ val halfHeight = height / 2
+ val isLTR = getLayoutDirection() == LAYOUT_DIRECTION_LTR
+
+ val lapsListWidth: Int
+ val lapsListView = mLapsListView
+ if (lapsListView != null && lapsListView.getVisibility() != GONE) {
+ // Layout the laps list, centering it vertically.
+ lapsListWidth = lapsListView.getMeasuredWidth()
+ val lapsListHeight: Int = lapsListView.getMeasuredHeight()
+ val lapsListTop = top + halfHeight - lapsListHeight / 2
+ val lapsListBottom = lapsListTop + lapsListHeight
+ val lapsListLeft: Int
+ val lapsListRight: Int
+ if (isLTR) {
+ lapsListLeft = right - lapsListWidth
+ lapsListRight = right
+ } else {
+ lapsListLeft = left
+ lapsListRight = left + lapsListWidth
+ }
+ lapsListView.layout(lapsListLeft, lapsListTop, lapsListRight, lapsListBottom)
+ } else {
+ lapsListWidth = 0
+ }
+
+ // Layout the stopwatch, centering it horizontally and vertically.
+ val stopwatchWidth: Int = mStopwatchView.getMeasuredWidth()
+ val stopwatchHeight: Int = mStopwatchView.getMeasuredHeight()
+ val stopwatchTop = top + halfHeight - stopwatchHeight / 2
+ val stopwatchBottom = stopwatchTop + stopwatchHeight
+ val stopwatchLeft: Int
+ val stopwatchRight: Int
+ if (isLTR) {
+ stopwatchLeft = left + (width - lapsListWidth - stopwatchWidth) / 2
+ stopwatchRight = stopwatchLeft + stopwatchWidth
+ } else {
+ stopwatchRight = right - (width - lapsListWidth - stopwatchWidth) / 2
+ stopwatchLeft = stopwatchRight - stopwatchWidth
+ }
+
+ mStopwatchView.layout(stopwatchLeft, stopwatchTop, stopwatchRight, stopwatchBottom)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchService.java b/src/com/android/deskclock/stopwatch/StopwatchService.java
deleted file mode 100644
index f7f9425..0000000
--- a/src/com/android/deskclock/stopwatch/StopwatchService.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.stopwatch;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
-
-/**
- * This service exists solely to allow the stopwatch notification to alter the state of the
- * stopwatch without disturbing the notification shade. If an activity were used instead (even one
- * that is not displayed) the notification manager implicitly closes the notification shade which
- * clashes with the use case of starting/pausing/lapping/resetting the stopwatch without
- * disturbing the notification shade.
- */
-public final class StopwatchService extends Service {
-
- private static final String ACTION_PREFIX = "com.android.deskclock.action.";
-
- // shows the tab with the stopwatch
- public static final String ACTION_SHOW_STOPWATCH = ACTION_PREFIX + "SHOW_STOPWATCH";
- // starts the current stopwatch
- public static final String ACTION_START_STOPWATCH = ACTION_PREFIX + "START_STOPWATCH";
- // pauses the current stopwatch that's currently running
- public static final String ACTION_PAUSE_STOPWATCH = ACTION_PREFIX + "PAUSE_STOPWATCH";
- // laps the stopwatch that's currently running
- public static final String ACTION_LAP_STOPWATCH = ACTION_PREFIX + "LAP_STOPWATCH";
- // resets the stopwatch if it's stopped
- public static final String ACTION_RESET_STOPWATCH = ACTION_PREFIX + "RESET_STOPWATCH";
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- final String action = intent.getAction();
- final int label = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent);
- switch (action) {
- case ACTION_SHOW_STOPWATCH: {
- Events.sendStopwatchEvent(R.string.action_show, label);
-
- // Open DeskClock positioned on the stopwatch tab.
- UiDataModel.getUiDataModel().setSelectedTab(STOPWATCH);
- final Intent showStopwatch = new Intent(this, DeskClock.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(showStopwatch);
- break;
- }
- case ACTION_START_STOPWATCH: {
- Events.sendStopwatchEvent(R.string.action_start, label);
- DataModel.getDataModel().startStopwatch();
- break;
- }
- case ACTION_PAUSE_STOPWATCH: {
- Events.sendStopwatchEvent(R.string.action_pause, label);
- DataModel.getDataModel().pauseStopwatch();
- break;
- }
- case ACTION_RESET_STOPWATCH: {
- Events.sendStopwatchEvent(R.string.action_reset, label);
- DataModel.getDataModel().resetStopwatch();
- break;
- }
- case ACTION_LAP_STOPWATCH: {
- Events.sendStopwatchEvent(R.string.action_lap, label);
- DataModel.getDataModel().addLap();
- break;
- }
- }
-
- return START_NOT_STICKY;
- }
-}
diff --git a/src/com/android/deskclock/stopwatch/StopwatchService.kt b/src/com/android/deskclock/stopwatch/StopwatchService.kt
new file mode 100644
index 0000000..19ac00a
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchService.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ * This service exists solely to allow the stopwatch notification to alter the state of the
+ * stopwatch without disturbing the notification shade. If an activity were used instead (even one
+ * that is not displayed) the notification manager implicitly closes the notification shade which
+ * clashes with the use case of starting/pausing/lapping/resetting the stopwatch without
+ * disturbing the notification shade.
+ */
+class StopwatchService : Service() {
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ val action: String? = intent.getAction()
+ val label: Int = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent)
+ when (action) {
+ ACTION_SHOW_STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_show, label)
+
+ // Open DeskClock positioned on the stopwatch tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.STOPWATCH
+ val showStopwatch: Intent = Intent(this, DeskClock::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(showStopwatch)
+ }
+ ACTION_START_STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_start, label)
+ DataModel.dataModel.startStopwatch()
+ }
+ ACTION_PAUSE_STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_pause, label)
+ DataModel.dataModel.pauseStopwatch()
+ }
+ ACTION_RESET_STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_reset, label)
+ DataModel.dataModel.resetStopwatch()
+ }
+ ACTION_LAP_STOPWATCH -> {
+ Events.sendStopwatchEvent(R.string.action_lap, label)
+ DataModel.dataModel.addLap()
+ }
+ }
+
+ return START_NOT_STICKY
+ }
+
+ companion object {
+ private const val ACTION_PREFIX = "com.android.deskclock.action."
+
+ // shows the tab with the stopwatch
+ const val ACTION_SHOW_STOPWATCH = ACTION_PREFIX + "SHOW_STOPWATCH"
+
+ // starts the current stopwatch
+ const val ACTION_START_STOPWATCH = ACTION_PREFIX + "START_STOPWATCH"
+
+ // pauses the current stopwatch that's currently running
+ const val ACTION_PAUSE_STOPWATCH = ACTION_PREFIX + "PAUSE_STOPWATCH"
+
+ // laps the stopwatch that's currently running
+ const val ACTION_LAP_STOPWATCH = ACTION_PREFIX + "LAP_STOPWATCH"
+
+ // resets the stopwatch if it's stopped
+ const val ACTION_RESET_STOPWATCH = ACTION_PREFIX + "RESET_STOPWATCH"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/ExpiredTimersActivity.java b/src/com/android/deskclock/timer/ExpiredTimersActivity.java
deleted file mode 100644
index 30ed121..0000000
--- a/src/com/android/deskclock/timer/ExpiredTimersActivity.java
+++ /dev/null
@@ -1,309 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.os.Bundle;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-import android.transition.AutoTransition;
-import android.transition.TransitionManager;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import com.android.deskclock.BaseActivity;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.data.TimerListener;
-
-import java.util.List;
-
-/**
- * This activity is designed to be shown over the lock screen. As such, it displays the expired
- * timers and a single button to reset them all. Each expired timer can also be reset to one minute
- * with a button in the user interface. All other timer operations are disabled in this activity.
- */
-public class ExpiredTimersActivity extends BaseActivity {
-
- /** Scheduled to update the timers while at least one is expired. */
- private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
-
- /** Updates the timers displayed in this activity as the backing data changes. */
- private final TimerListener mTimerChangeWatcher = new TimerChangeWatcher();
-
- /** The scene root for transitions when expired timers are added/removed from this container. */
- private ViewGroup mExpiredTimersScrollView;
-
- /** Displays the expired timers. */
- private ViewGroup mExpiredTimersView;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- final List<Timer> expiredTimers = getExpiredTimers();
-
- // If no expired timers, finish
- if (expiredTimers.size() == 0) {
- LogUtils.i("No expired timers, skipping display.");
- finish();
- return;
- }
-
- setContentView(R.layout.expired_timers_activity);
-
- mExpiredTimersView = (ViewGroup) findViewById(R.id.expired_timers_list);
- mExpiredTimersScrollView = (ViewGroup) findViewById(R.id.expired_timers_scroll);
-
- findViewById(R.id.fab).setOnClickListener(new FabClickListener());
-
- final View view = findViewById(R.id.expired_timers_activity);
- view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
-
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
- | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
- | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON);
-
- // Close dialogs and window shade, so this is fully visible
- sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-
- // Honor rotation on tablets; fix the orientation on phones.
- if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
- }
-
- // Create views for each of the expired timers.
- for (Timer timer : expiredTimers) {
- addTimer(timer);
- }
-
- // Update views in response to timer data changes.
- DataModel.getDataModel().addTimerListener(mTimerChangeWatcher);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- startUpdatingTime();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- stopUpdatingTime();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- DataModel.getDataModel().removeTimerListener(mTimerChangeWatcher);
- }
-
- @Override
- public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_UP) {
- switch (event.getKeyCode()) {
- case KeyEvent.KEYCODE_VOLUME_UP:
- case KeyEvent.KEYCODE_VOLUME_DOWN:
- case KeyEvent.KEYCODE_VOLUME_MUTE:
- case KeyEvent.KEYCODE_CAMERA:
- case KeyEvent.KEYCODE_FOCUS:
- DataModel.getDataModel().resetOrDeleteExpiredTimers(
- R.string.label_hardware_button);
- return true;
- }
- }
- return super.dispatchKeyEvent(event);
- }
-
- /**
- * Post the first runnable to update times within the UI. It will reschedule itself as needed.
- */
- private void startUpdatingTime() {
- // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
- stopUpdatingTime();
- mExpiredTimersView.post(mTimeUpdateRunnable);
- }
-
- /**
- * Remove the runnable that updates times within the UI.
- */
- private void stopUpdatingTime() {
- mExpiredTimersView.removeCallbacks(mTimeUpdateRunnable);
- }
-
- /**
- * Create and add a new view that corresponds with the given {@code timer}.
- */
- private void addTimer(Timer timer) {
- TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition());
-
- final int timerId = timer.getId();
- final TimerItem timerItem = (TimerItem)
- getLayoutInflater().inflate(R.layout.timer_item, mExpiredTimersView, false);
- // Store the timer id as a tag on the view so it can be located on delete.
- timerItem.setId(timerId);
- mExpiredTimersView.addView(timerItem);
-
- // Hide the label hint for expired timers.
- final TextView labelView = (TextView) timerItem.findViewById(R.id.timer_label);
- labelView.setHint(null);
- labelView.setVisibility(TextUtils.isEmpty(timer.getLabel()) ? View.GONE : View.VISIBLE);
-
- // Add logic to the "Add 1 Minute" button.
- final View addMinuteButton = timerItem.findViewById(R.id.reset_add);
- addMinuteButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- final Timer timer = DataModel.getDataModel().getTimer(timerId);
- DataModel.getDataModel().addTimerMinute(timer);
- }
- });
-
- // If the first timer was just added, center it.
- final List<Timer> expiredTimers = getExpiredTimers();
- if (expiredTimers.size() == 1) {
- centerFirstTimer();
- } else if (expiredTimers.size() == 2) {
- uncenterFirstTimer();
- }
- }
-
- /**
- * Remove an existing view that corresponds with the given {@code timer}.
- */
- private void removeTimer(Timer timer) {
- TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, new AutoTransition());
-
- final int timerId = timer.getId();
- final int count = mExpiredTimersView.getChildCount();
- for (int i = 0; i < count; ++i) {
- final View timerView = mExpiredTimersView.getChildAt(i);
- if (timerView.getId() == timerId) {
- mExpiredTimersView.removeView(timerView);
- break;
- }
- }
-
- // If the second last timer was just removed, center the last timer.
- final List<Timer> expiredTimers = getExpiredTimers();
- if (expiredTimers.isEmpty()) {
- finish();
- } else if (expiredTimers.size() == 1) {
- centerFirstTimer();
- }
- }
-
- /**
- * Center the single timer.
- */
- private void centerFirstTimer() {
- final FrameLayout.LayoutParams lp =
- (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams();
- lp.gravity = Gravity.CENTER;
- mExpiredTimersView.requestLayout();
- }
-
- /**
- * Display the multiple timers as a scrollable list.
- */
- private void uncenterFirstTimer() {
- final FrameLayout.LayoutParams lp =
- (FrameLayout.LayoutParams) mExpiredTimersView.getLayoutParams();
- lp.gravity = Gravity.NO_GRAVITY;
- mExpiredTimersView.requestLayout();
- }
-
- private List<Timer> getExpiredTimers() {
- return DataModel.getDataModel().getExpiredTimers();
- }
-
- /**
- * Periodically refreshes the state of each timer.
- */
- private class TimeUpdateRunnable implements Runnable {
- @Override
- public void run() {
- final long startTime = SystemClock.elapsedRealtime();
-
- final int count = mExpiredTimersView.getChildCount();
- for (int i = 0; i < count; ++i) {
- final TimerItem timerItem = (TimerItem) mExpiredTimersView.getChildAt(i);
- final Timer timer = DataModel.getDataModel().getTimer(timerItem.getId());
- if (timer != null) {
- timerItem.update(timer);
- }
- }
-
- final long endTime = SystemClock.elapsedRealtime();
-
- // Try to maintain a consistent period of time between redraws.
- final long delay = Math.max(0L, startTime + 20L - endTime);
- mExpiredTimersView.postDelayed(this, delay);
- }
- }
-
- /**
- * Clicking the fab resets all expired timers.
- */
- private class FabClickListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- stopUpdatingTime();
- DataModel.getDataModel().removeTimerListener(mTimerChangeWatcher);
- DataModel.getDataModel().resetOrDeleteExpiredTimers(R.string.label_deskclock);
- finish();
- }
- }
-
- /**
- * Adds and removes expired timers from this activity based on their state changes.
- */
- private class TimerChangeWatcher implements TimerListener {
- @Override
- public void timerAdded(Timer timer) {
- if (timer.isExpired()) {
- addTimer(timer);
- }
- }
-
- @Override
- public void timerUpdated(Timer before, Timer after) {
- if (!before.isExpired() && after.isExpired()) {
- addTimer(after);
- } else if (before.isExpired() && !after.isExpired()) {
- removeTimer(before);
- }
- }
-
- @Override
- public void timerRemoved(Timer timer) {
- if (timer.isExpired()) {
- removeTimer(timer);
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/timer/ExpiredTimersActivity.kt b/src/com/android/deskclock/timer/ExpiredTimersActivity.kt
new file mode 100644
index 0000000..09c4ee1
--- /dev/null
+++ b/src/com/android/deskclock/timer/ExpiredTimersActivity.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.os.Bundle
+import android.os.SystemClock
+import android.text.TextUtils
+import android.transition.AutoTransition
+import android.transition.TransitionManager
+import android.widget.FrameLayout
+import android.widget.TextView
+import android.view.Gravity
+import android.view.KeyEvent
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.LogUtils
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.data.TimerListener
+
+/**
+ * This activity is designed to be shown over the lock screen. As such, it displays the expired
+ * timers and a single button to reset them all. Each expired timer can also be reset to one minute
+ * with a button in the user interface. All other timer operations are disabled in this activity.
+ */
+class ExpiredTimersActivity : BaseActivity() {
+ /** Scheduled to update the timers while at least one is expired. */
+ private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
+
+ /** Updates the timers displayed in this activity as the backing data changes. */
+ private val mTimerChangeWatcher: TimerListener = TimerChangeWatcher()
+
+ /** The scene root for transitions when expired timers are added/removed from this container. */
+ private lateinit var mExpiredTimersScrollView: ViewGroup
+
+ /** Displays the expired timers. */
+ private lateinit var mExpiredTimersView: ViewGroup
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val expiredTimers = expiredTimers
+
+ // If no expired timers, finish
+ if (expiredTimers.size == 0) {
+ LogUtils.i("No expired timers, skipping display.")
+ finish()
+ return
+ }
+
+ setContentView(R.layout.expired_timers_activity)
+
+ mExpiredTimersView = findViewById(R.id.expired_timers_list) as ViewGroup
+ mExpiredTimersScrollView = findViewById(R.id.expired_timers_scroll) as ViewGroup
+
+ (findViewById(R.id.fab) as View).setOnClickListener(FabClickListener())
+
+ val view: View = findViewById(R.id.expired_timers_activity)
+ view.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON)
+
+ setTurnScreenOn(true)
+ setShowWhenLocked(true)
+
+ // Close dialogs and window shade, so this is fully visible
+ sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
+
+ // Honor rotation on tablets; fix the orientation on phones.
+ if (!getResources().getBoolean(R.bool.rotateAlarmAlert)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR)
+ }
+
+ // Create views for each of the expired timers.
+ for (timer in expiredTimers) {
+ addTimer(timer)
+ }
+
+ // Update views in response to timer data changes.
+ DataModel.dataModel.addTimerListener(mTimerChangeWatcher)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ startUpdatingTime()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ stopUpdatingTime()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ DataModel.dataModel.removeTimerListener(mTimerChangeWatcher)
+ }
+
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ if (event.action == KeyEvent.ACTION_UP) {
+ when (event.keyCode) {
+ KeyEvent.KEYCODE_VOLUME_UP,
+ KeyEvent.KEYCODE_VOLUME_DOWN,
+ KeyEvent.KEYCODE_VOLUME_MUTE,
+ KeyEvent.KEYCODE_CAMERA,
+ KeyEvent.KEYCODE_FOCUS -> {
+ DataModel.dataModel.resetOrDeleteExpiredTimers(R.string.label_hardware_button)
+ return true
+ }
+ }
+ }
+ return super.dispatchKeyEvent(event)
+ }
+
+ /**
+ * Post the first runnable to update times within the UI. It will reschedule itself as needed.
+ */
+ private fun startUpdatingTime() {
+ // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
+ stopUpdatingTime()
+ mExpiredTimersView.post(mTimeUpdateRunnable)
+ }
+
+ /**
+ * Remove the runnable that updates times within the UI.
+ */
+ private fun stopUpdatingTime() {
+ mExpiredTimersView.removeCallbacks(mTimeUpdateRunnable)
+ }
+
+ /**
+ * Create and add a new view that corresponds with the given `timer`.
+ */
+ private fun addTimer(timer: Timer) {
+ TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, AutoTransition())
+
+ val timerId: Int = timer.id
+ val timerItem = getLayoutInflater()
+ .inflate(R.layout.timer_item, mExpiredTimersView, false) as TimerItem
+ // Store the timer id as a tag on the view so it can be located on delete.
+ timerItem.id = timerId
+ mExpiredTimersView.addView(timerItem)
+
+ // Hide the label hint for expired timers.
+ val labelView = timerItem.findViewById<View>(R.id.timer_label) as TextView
+ labelView.hint = null
+ labelView.visibility = if (TextUtils.isEmpty(timer.label)) View.GONE else View.VISIBLE
+
+ // Add logic to the "Add 1 Minute" button.
+ val addMinuteButton = timerItem.findViewById<View>(R.id.reset_add)
+ addMinuteButton.setOnClickListener {
+ val timer: Timer = DataModel.dataModel.getTimer(timerId)!!
+ DataModel.dataModel.addTimerMinute(timer)
+ }
+
+ // If the first timer was just added, center it.
+ val expiredTimers = expiredTimers
+ if (expiredTimers.size == 1) {
+ centerFirstTimer()
+ } else if (expiredTimers.size == 2) {
+ uncenterFirstTimer()
+ }
+ }
+
+ /**
+ * Remove an existing view that corresponds with the given `timer`.
+ */
+ private fun removeTimer(timer: Timer) {
+ TransitionManager.beginDelayedTransition(mExpiredTimersScrollView, AutoTransition())
+
+ val timerId: Int = timer.id
+ val count = mExpiredTimersView.childCount
+ for (i in 0 until count) {
+ val timerView = mExpiredTimersView.getChildAt(i)
+ if (timerView.id == timerId) {
+ mExpiredTimersView.removeView(timerView)
+ break
+ }
+ }
+
+ // If the second last timer was just removed, center the last timer.
+ val expiredTimers = expiredTimers
+ if (expiredTimers.isEmpty()) {
+ finish()
+ } else if (expiredTimers.size == 1) {
+ centerFirstTimer()
+ }
+ }
+
+ /**
+ * Center the single timer.
+ */
+ private fun centerFirstTimer() {
+ val lp = mExpiredTimersView.layoutParams as FrameLayout.LayoutParams
+ lp.gravity = Gravity.CENTER
+ mExpiredTimersView.requestLayout()
+ }
+
+ /**
+ * Display the multiple timers as a scrollable list.
+ */
+ private fun uncenterFirstTimer() {
+ val lp = mExpiredTimersView.layoutParams as FrameLayout.LayoutParams
+ lp.gravity = Gravity.NO_GRAVITY
+ mExpiredTimersView.requestLayout()
+ }
+
+ private val expiredTimers: List<Timer>
+ get() = DataModel.dataModel.expiredTimers
+
+ /**
+ * Periodically refreshes the state of each timer.
+ */
+ private inner class TimeUpdateRunnable : Runnable {
+ override fun run() {
+ val startTime = SystemClock.elapsedRealtime()
+
+ val count = mExpiredTimersView.childCount
+ for (i in 0 until count) {
+ val timerItem = mExpiredTimersView.getChildAt(i) as TimerItem
+ val timer: Timer? = DataModel.dataModel.getTimer(timerItem.id)
+ if (timer != null) {
+ timerItem.update(timer)
+ }
+ }
+
+ val endTime = SystemClock.elapsedRealtime()
+
+ // Try to maintain a consistent period of time between redraws.
+ val delay = Math.max(0L, startTime + 20L - endTime)
+ mExpiredTimersView.postDelayed(this, delay)
+ }
+ }
+
+ /**
+ * Clicking the fab resets all expired timers.
+ */
+ private inner class FabClickListener : View.OnClickListener {
+ override fun onClick(v: View) {
+ stopUpdatingTime()
+ DataModel.dataModel.removeTimerListener(mTimerChangeWatcher)
+ DataModel.dataModel.resetOrDeleteExpiredTimers(R.string.label_deskclock)
+ finish()
+ }
+ }
+
+ /**
+ * Adds and removes expired timers from this activity based on their state changes.
+ */
+ private inner class TimerChangeWatcher : TimerListener {
+ override fun timerAdded(timer: Timer) {
+ if (timer.isExpired) {
+ addTimer(timer)
+ }
+ }
+
+ override fun timerUpdated(before: Timer, after: Timer) {
+ if (!before.isExpired && after.isExpired) {
+ addTimer(after)
+ } else if (before.isExpired && !after.isExpired) {
+ removeTimer(before)
+ }
+ }
+
+ override fun timerRemoved(timer: Timer) {
+ if (timer.isExpired) {
+ removeTimer(timer)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerCircleView.java b/src/com/android/deskclock/timer/TimerCircleView.java
deleted file mode 100644
index f605f91..0000000
--- a/src/com/android/deskclock/timer/TimerCircleView.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.RectF;
-import android.util.AttributeSet;
-import android.view.View;
-
-import com.android.deskclock.R;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.Timer;
-
-/**
- * Custom view that draws timer progress as a circle.
- */
-public final class TimerCircleView extends View {
-
- /** The size of the dot indicating the progress through the timer. */
- private final float mDotRadius;
-
- /** An amount to subtract from the true radius to account for drawing thicknesses. */
- private final float mRadiusOffset;
-
- /** The color indicating the remaining portion of the timer. */
- private final int mRemainderColor;
-
- /** The color indicating the completed portion of the timer. */
- private final int mCompletedColor;
-
- /** The size of the stroke that paints the timer circle. */
- private final float mStrokeSize;
-
- private final Paint mPaint = new Paint();
- private final Paint mFill = new Paint();
- private final RectF mArcRect = new RectF();
-
- private Timer mTimer;
-
- @SuppressWarnings("unused")
- public TimerCircleView(Context context) {
- this(context, null);
- }
-
- public TimerCircleView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final Resources resources = context.getResources();
- final float dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size);
-
- mDotRadius = dotDiameter / 2f;
- mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
- mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, 0);
-
- mRemainderColor = Color.WHITE;
- mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent);
-
- mPaint.setAntiAlias(true);
- mPaint.setStyle(Paint.Style.STROKE);
-
- mFill.setAntiAlias(true);
- mFill.setColor(mCompletedColor);
- mFill.setStyle(Paint.Style.FILL);
- }
-
- void update(Timer timer) {
- if (mTimer != timer) {
- mTimer = timer;
- postInvalidateOnAnimation();
- }
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- if (mTimer == null) {
- return;
- }
-
- // Compute the size and location of the circle to be drawn.
- final int xCenter = getWidth() / 2;
- final int yCenter = getHeight() / 2;
- final float radius = Math.min(xCenter, yCenter) - mRadiusOffset;
-
- // Reset old painting state.
- mPaint.setColor(mRemainderColor);
- mPaint.setStrokeWidth(mStrokeSize);
-
- // If the timer is reset, draw a simple white circle.
- final float redPercent;
- if (mTimer.isReset()) {
- // Draw a complete white circle; no red arc required.
- canvas.drawCircle(xCenter, yCenter, radius, mPaint);
-
- // Red percent is 0 since no timer progress has been made.
- redPercent = 0;
- } else if (mTimer.isExpired()) {
- mPaint.setColor(mCompletedColor);
-
- // Draw a complete white circle; no red arc required.
- canvas.drawCircle(xCenter, yCenter, radius, mPaint);
-
- // Red percent is 1 since the timer has expired.
- redPercent = 1;
- } else {
- // Draw a combination of red and white arcs to create a circle.
- mArcRect.top = yCenter - radius;
- mArcRect.bottom = yCenter + radius;
- mArcRect.left = xCenter - radius;
- mArcRect.right = xCenter + radius;
- redPercent = Math.min(1, (float) mTimer.getElapsedTime() / (float) mTimer.getTotalLength());
- final float whitePercent = 1 - redPercent;
-
- // Draw a white arc to indicate the amount of timer that remains.
- canvas.drawArc(mArcRect, 270, whitePercent * 360, false, mPaint);
-
- // Draw a red arc to indicate the amount of timer completed.
- mPaint.setColor(mCompletedColor);
- canvas.drawArc(mArcRect, 270, -redPercent * 360 , false, mPaint);
- }
-
- // Draw a red dot to indicate current progress through the timer.
- final float dotAngleDegrees = 270 - redPercent * 360;
- final double dotAngleRadians = Math.toRadians(dotAngleDegrees);
- final float dotX = xCenter + (float) (radius * Math.cos(dotAngleRadians));
- final float dotY = yCenter + (float) (radius * Math.sin(dotAngleRadians));
- canvas.drawCircle(dotX, dotY, mDotRadius, mFill);
-
- if (mTimer.isRunning()) {
- postInvalidateOnAnimation();
- }
- }
-}
diff --git a/src/com/android/deskclock/timer/TimerCircleView.kt b/src/com/android/deskclock/timer/TimerCircleView.kt
new file mode 100644
index 0000000..af45501
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerCircleView.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.view.View
+
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.data.Timer
+
+import kotlin.math.cos
+import kotlin.math.min
+import kotlin.math.sin
+
+/**
+ * Custom view that draws timer progress as a circle.
+ */
+class TimerCircleView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : View(context, attrs) {
+ /** The size of the dot indicating the progress through the timer. */
+ private val mDotRadius: Float
+
+ /** An amount to subtract from the true radius to account for drawing thicknesses. */
+ private val mRadiusOffset: Float
+
+ /** The color indicating the remaining portion of the timer. */
+ private val mRemainderColor: Int
+
+ /** The color indicating the completed portion of the timer. */
+ private val mCompletedColor: Int
+
+ /** The size of the stroke that paints the timer circle. */
+ private val mStrokeSize: Float
+
+ private val mPaint = Paint()
+ private val mFill = Paint()
+ private val mArcRect = RectF()
+
+ private var mTimer: Timer? = null
+
+ init {
+ val resources = context.resources
+ val dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size)
+
+ mDotRadius = dotDiameter / 2f
+ mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size)
+ mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, 0f)
+
+ mRemainderColor = Color.WHITE
+ mCompletedColor = ThemeUtils.resolveColor(context, R.attr.colorAccent)
+
+ mPaint.isAntiAlias = true
+ mPaint.style = Paint.Style.STROKE
+
+ mFill.isAntiAlias = true
+ mFill.color = mCompletedColor
+ mFill.style = Paint.Style.FILL
+ }
+
+ fun update(timer: Timer) {
+ if (mTimer !== timer) {
+ mTimer = timer
+ postInvalidateOnAnimation()
+ }
+ }
+
+ public override fun onDraw(canvas: Canvas) {
+ if (mTimer == null) {
+ return
+ }
+
+ // Compute the size and location of the circle to be drawn.
+ val xCenter = width / 2
+ val yCenter = height / 2
+ val radius = min(xCenter, yCenter) - mRadiusOffset
+
+ // Reset old painting state.
+ mPaint.color = mRemainderColor
+ mPaint.strokeWidth = mStrokeSize
+
+ // If the timer is reset, draw a simple white circle.
+ val redPercent: Float
+ when {
+ mTimer!!.isReset -> {
+ // Draw a complete white circle; no red arc required.
+ canvas.drawCircle(xCenter.toFloat(), yCenter.toFloat(), radius, mPaint)
+
+ // Red percent is 0 since no timer progress has been made.
+ redPercent = 0f
+ }
+ mTimer!!.isExpired -> {
+ mPaint.color = mCompletedColor
+
+ // Draw a complete white circle; no red arc required.
+ canvas.drawCircle(xCenter.toFloat(), yCenter.toFloat(), radius, mPaint)
+
+ // Red percent is 1 since the timer has expired.
+ redPercent = 1f
+ }
+ else -> {
+ // Draw a combination of red and white arcs to create a circle.
+ mArcRect.top = yCenter - radius
+ mArcRect.bottom = yCenter + radius
+ mArcRect.left = xCenter - radius
+ mArcRect.right = xCenter + radius
+ redPercent = min(1f,
+ mTimer!!.elapsedTime.toFloat() / mTimer!!.totalLength.toFloat())
+ val whitePercent = 1 - redPercent
+
+ // Draw a white arc to indicate the amount of timer that remains.
+ canvas.drawArc(mArcRect, 270f, whitePercent * 360, false, mPaint)
+
+ // Draw a red arc to indicate the amount of timer completed.
+ mPaint.color = mCompletedColor
+ canvas.drawArc(mArcRect, 270f, -redPercent * 360, false, mPaint)
+ }
+ }
+
+ // Draw a red dot to indicate current progress through the timer.
+ val dotAngleDegrees = 270 - redPercent * 360
+ val dotAngleRadians = Math.toRadians(dotAngleDegrees.toDouble())
+ val dotX = xCenter + (radius * cos(dotAngleRadians)).toFloat()
+ val dotY = yCenter + (radius * sin(dotAngleRadians)).toFloat()
+ canvas.drawCircle(dotX, dotY, mDotRadius, mFill)
+
+ if (mTimer!!.isRunning) {
+ postInvalidateOnAnimation()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerFragment.java b/src/com/android/deskclock/timer/TimerFragment.java
deleted file mode 100644
index 8b30105..0000000
--- a/src/com/android/deskclock/timer/TimerFragment.java
+++ /dev/null
@@ -1,789 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.SystemClock;
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.viewpager.widget.ViewPager;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.Button;
-import android.widget.ImageView;
-
-import com.android.deskclock.AnimatorUtils;
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.DeskClockFragment;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.data.TimerListener;
-import com.android.deskclock.data.TimerStringFormatter;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.io.Serializable;
-import java.util.Arrays;
-
-import static android.view.View.ALPHA;
-import static android.view.View.GONE;
-import static android.view.View.INVISIBLE;
-import static android.view.View.TRANSLATION_Y;
-import static android.view.View.VISIBLE;
-import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
-
-/**
- * Displays a vertical list of timers in all states.
- */
-public final class TimerFragment extends DeskClockFragment {
-
- private static final String EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP";
-
- private static final String KEY_TIMER_SETUP_STATE = "timer_setup_input";
-
- /** Notified when the user swipes vertically to change the visible timer. */
- private final TimerPageChangeListener mTimerPageChangeListener = new TimerPageChangeListener();
-
- /** Scheduled to update the timers while at least one is running. */
- private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
-
- /** Updates the {@link #mPageIndicators} in response to timers being added or removed. */
- private final TimerListener mTimerWatcher = new TimerWatcher();
-
- private TimerSetupView mCreateTimerView;
- private ViewPager mViewPager;
- private TimerPagerAdapter mAdapter;
- private View mTimersView;
- private View mCurrentView;
- private ImageView[] mPageIndicators;
-
- private Serializable mTimerSetupState;
-
- /** {@code true} while this fragment is creating a new timer; {@code false} otherwise. */
- private boolean mCreatingTimer;
-
- /**
- * @return an Intent that selects the timers tab with the setup screen for a new timer in place.
- */
- public static Intent createTimerSetupIntent(Context context) {
- return new Intent(context, DeskClock.class).putExtra(EXTRA_TIMER_SETUP, true);
- }
-
- /** The public no-arg constructor required by all fragments. */
- public TimerFragment() {
- super(TIMERS);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- final View view = inflater.inflate(R.layout.timer_fragment, container, false);
-
- mAdapter = new TimerPagerAdapter(getFragmentManager());
- mViewPager = (ViewPager) view.findViewById(R.id.vertical_view_pager);
- mViewPager.setAdapter(mAdapter);
- mViewPager.addOnPageChangeListener(mTimerPageChangeListener);
-
- mTimersView = view.findViewById(R.id.timer_view);
- mCreateTimerView = (TimerSetupView) view.findViewById(R.id.timer_setup);
- mCreateTimerView.setFabContainer(this);
- mPageIndicators = new ImageView[] {
- (ImageView) view.findViewById(R.id.page_indicator0),
- (ImageView) view.findViewById(R.id.page_indicator1),
- (ImageView) view.findViewById(R.id.page_indicator2),
- (ImageView) view.findViewById(R.id.page_indicator3)
- };
-
- DataModel.getDataModel().addTimerListener(mAdapter);
- DataModel.getDataModel().addTimerListener(mTimerWatcher);
-
- // If timer setup state is present, retrieve it to be later honored.
- if (savedInstanceState != null) {
- mTimerSetupState = savedInstanceState.getSerializable(KEY_TIMER_SETUP_STATE);
- }
-
- return view;
- }
-
- @Override
- public void onStart() {
- super.onStart();
-
- // Initialize the page indicators.
- updatePageIndicators();
-
- boolean createTimer = false;
- int showTimerId = -1;
-
- // Examine the intent of the parent activity to determine which view to display.
- final Intent intent = getActivity().getIntent();
- if (intent != null) {
- // These extras are single-use; remove them after honoring them.
- createTimer = intent.getBooleanExtra(EXTRA_TIMER_SETUP, false);
- intent.removeExtra(EXTRA_TIMER_SETUP);
-
- showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
- intent.removeExtra(TimerService.EXTRA_TIMER_ID);
- }
-
- // Choose the view to display in this fragment.
- if (showTimerId != -1) {
- // A specific timer must be shown; show the list of timers.
- showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
- } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
- // No timers exist, a timer is being created, or the last view was timer setup;
- // show the timer setup view.
- showCreateTimerView(FAB_AND_BUTTONS_IMMEDIATE);
-
- if (mTimerSetupState != null) {
- mCreateTimerView.setState(mTimerSetupState);
- mTimerSetupState = null;
- }
- } else {
- // Otherwise, default to showing the list of timers.
- showTimersView(FAB_AND_BUTTONS_IMMEDIATE);
- }
-
- // If the intent did not specify a timer to show, show the last timer that expired.
- if (showTimerId == -1) {
- final Timer timer = DataModel.getDataModel().getMostRecentExpiredTimer();
- showTimerId = timer == null ? -1 : timer.getId();
- }
-
- // If a specific timer should be displayed, display the corresponding timer tab.
- if (showTimerId != -1) {
- final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
- if (timer != null) {
- final int index = DataModel.getDataModel().getTimers().indexOf(timer);
- mViewPager.setCurrentItem(index);
- }
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- // We may have received a new intent while paused.
- final Intent intent = getActivity().getIntent();
- if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
- // This extra is single-use; remove after honoring it.
- final int showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1);
- intent.removeExtra(TimerService.EXTRA_TIMER_ID);
-
- final Timer timer = DataModel.getDataModel().getTimer(showTimerId);
- if (timer != null) {
- // A specific timer must be shown; show the list of timers.
- final int index = DataModel.getDataModel().getTimers().indexOf(timer);
- mViewPager.setCurrentItem(index);
-
- animateToView(mTimersView, null, false);
- }
- }
- }
-
- @Override
- public void onStop() {
- super.onStop();
-
- // Stop updating the timers when this fragment is no longer visible.
- stopUpdatingTime();
- }
-
- @Override
- public void onDestroyView() {
- super.onDestroyView();
-
- DataModel.getDataModel().removeTimerListener(mAdapter);
- DataModel.getDataModel().removeTimerListener(mTimerWatcher);
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
-
- // If the timer creation view is visible, store the input for later restoration.
- if (mCurrentView == mCreateTimerView) {
- mTimerSetupState = mCreateTimerView.getState();
- outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState);
- }
- }
-
- private void updateFab(@NonNull ImageView fab, boolean animate) {
- if (mCurrentView == mTimersView) {
- final Timer timer = getTimer();
- if (timer == null) {
- fab.setVisibility(INVISIBLE);
- return;
- }
-
- fab.setVisibility(VISIBLE);
- switch (timer.getState()) {
- case RUNNING:
- if (animate) {
- fab.setImageResource(R.drawable.ic_play_pause_animation);
- } else {
- fab.setImageResource(R.drawable.ic_play_pause);
- }
- fab.setContentDescription(fab.getResources().getString(R.string.timer_stop));
- break;
- case RESET:
- if (animate) {
- fab.setImageResource(R.drawable.ic_stop_play_animation);
- } else {
- fab.setImageResource(R.drawable.ic_pause_play);
- }
- fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
- break;
- case PAUSED:
- if (animate) {
- fab.setImageResource(R.drawable.ic_pause_play_animation);
- } else {
- fab.setImageResource(R.drawable.ic_pause_play);
- }
- fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
- break;
- case MISSED:
- case EXPIRED:
- fab.setImageResource(R.drawable.ic_stop_white_24dp);
- fab.setContentDescription(fab.getResources().getString(R.string.timer_stop));
- break;
- }
- } else if (mCurrentView == mCreateTimerView) {
- if (mCreateTimerView.hasValidInput()) {
- fab.setImageResource(R.drawable.ic_start_white_24dp);
- fab.setContentDescription(fab.getResources().getString(R.string.timer_start));
- fab.setVisibility(VISIBLE);
- } else {
- fab.setContentDescription(null);
- fab.setVisibility(INVISIBLE);
- }
- }
- }
-
- @Override
- public void onUpdateFab(@NonNull ImageView fab) {
- updateFab(fab, false);
- }
-
- @Override
- public void onMorphFab(@NonNull ImageView fab) {
- // Update the fab's drawable to match the current timer state.
- updateFab(fab, Utils.isNOrLater());
- // Animate the drawable.
- AnimatorUtils.startDrawableAnimation(fab);
- }
-
- @Override
- public void onUpdateFabButtons(@NonNull Button left, @NonNull Button right) {
- if (mCurrentView == mTimersView) {
- left.setClickable(true);
- left.setText(R.string.timer_delete);
- left.setContentDescription(left.getResources().getString(R.string.timer_delete));
- left.setVisibility(VISIBLE);
-
- right.setClickable(true);
- right.setText(R.string.timer_add_timer);
- right.setContentDescription(right.getResources().getString(R.string.timer_add_timer));
- right.setVisibility(VISIBLE);
-
- } else if (mCurrentView == mCreateTimerView) {
- left.setClickable(true);
- left.setText(R.string.timer_cancel);
- left.setContentDescription(left.getResources().getString(R.string.timer_cancel));
- // If no timers yet exist, the user is forced to create the first one.
- left.setVisibility(hasTimers() ? VISIBLE : INVISIBLE);
-
- right.setVisibility(INVISIBLE);
- }
- }
-
- @Override
- public void onFabClick(@NonNull ImageView fab) {
- if (mCurrentView == mTimersView) {
- final Timer timer = getTimer();
-
- // If no timer is currently showing a fab action is meaningless.
- if (timer == null) {
- return;
- }
-
- final Context context = fab.getContext();
- final long currentTime = timer.getRemainingTime();
-
- switch (timer.getState()) {
- case RUNNING:
- DataModel.getDataModel().pauseTimer(timer);
- Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock);
- if (currentTime > 0) {
- mTimersView.announceForAccessibility(TimerStringFormatter.formatString(
- context, R.string.timer_accessibility_stopped, currentTime, true));
- }
- break;
- case PAUSED:
- case RESET:
- DataModel.getDataModel().startTimer(timer);
- Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock);
- if (currentTime > 0) {
- mTimersView.announceForAccessibility(TimerStringFormatter.formatString(
- context, R.string.timer_accessibility_started, currentTime, true));
- }
- break;
- case MISSED:
- case EXPIRED:
- DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_deskclock);
- break;
- }
-
- } else if (mCurrentView == mCreateTimerView) {
- mCreatingTimer = true;
- try {
- // Create the new timer.
- final long timerLength = mCreateTimerView.getTimeInMillis();
- final Timer timer = DataModel.getDataModel().addTimer(timerLength, "", false);
- Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock);
-
- // Start the new timer.
- DataModel.getDataModel().startTimer(timer);
- Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock);
-
- // Display the freshly created timer view.
- mViewPager.setCurrentItem(0);
- } finally {
- mCreatingTimer = false;
- }
-
- // Return to the list of timers.
- animateToView(mTimersView, null, true);
- }
- }
-
- @Override
- public void onLeftButtonClick(@NonNull Button left) {
- if (mCurrentView == mTimersView) {
- // Clicking the "delete" button.
- final Timer timer = getTimer();
- if (timer == null) {
- return;
- }
-
- if (mAdapter.getCount() > 1) {
- animateTimerRemove(timer);
- } else {
- animateToView(mCreateTimerView, timer, false);
- }
-
- left.announceForAccessibility(getActivity().getString(R.string.timer_deleted));
- } else if (mCurrentView == mCreateTimerView) {
- // Clicking the "cancel" button on the timer creation page returns to the timers list.
- mCreateTimerView.reset();
-
- animateToView(mTimersView, null, false);
-
- left.announceForAccessibility(getActivity().getString(R.string.timer_canceled));
- }
- }
-
- @Override
- public void onRightButtonClick(@NonNull Button right) {
- if (mCurrentView != mCreateTimerView) {
- animateToView(mCreateTimerView, null, true);
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (mCurrentView == mCreateTimerView) {
- return mCreateTimerView.onKeyDown(keyCode, event);
- }
- return super.onKeyDown(keyCode, event);
- }
-
- /**
- * Updates the state of the page indicators so they reflect the selected page in the context of
- * all pages.
- */
- private void updatePageIndicators() {
- final int page = mViewPager.getCurrentItem();
- final int pageIndicatorCount = mPageIndicators.length;
- final int pageCount = mAdapter.getCount();
-
- final int[] states = computePageIndicatorStates(page, pageIndicatorCount, pageCount);
- for (int i = 0; i < states.length; i++) {
- final int state = states[i];
- final ImageView pageIndicator = mPageIndicators[i];
- if (state == 0) {
- pageIndicator.setVisibility(GONE);
- } else {
- pageIndicator.setVisibility(VISIBLE);
- pageIndicator.setImageResource(state);
- }
- }
- }
-
- /**
- * @param page the selected page; value between 0 and {@code pageCount}
- * @param pageIndicatorCount the number of indicators displaying the {@code page} location
- * @param pageCount the number of pages that exist
- * @return an array of length {@code pageIndicatorCount} specifying which image to display for
- * each page indicator or 0 if the page indicator should be hidden
- */
- @VisibleForTesting
- static int[] computePageIndicatorStates(int page, int pageIndicatorCount, int pageCount) {
- // Compute the number of page indicators that will be visible.
- final int rangeSize = Math.min(pageIndicatorCount, pageCount);
-
- // Compute the inclusive range of pages to indicate centered around the selected page.
- int rangeStart = page - (rangeSize / 2);
- int rangeEnd = rangeStart + rangeSize - 1;
-
- // Clamp the range of pages if they extend beyond the last page.
- if (rangeEnd >= pageCount) {
- rangeEnd = pageCount - 1;
- rangeStart = rangeEnd - rangeSize + 1;
- }
-
- // Clamp the range of pages if they extend beyond the first page.
- if (rangeStart < 0) {
- rangeStart = 0;
- rangeEnd = rangeSize - 1;
- }
-
- // Build the result with all page indicators initially hidden.
- final int[] states = new int[pageIndicatorCount];
- Arrays.fill(states, 0);
-
- // If 0 or 1 total pages exist, all page indicators must remain hidden.
- if (rangeSize < 2) {
- return states;
- }
-
- // Initialize the visible page indicators to be dark.
- Arrays.fill(states, 0, rangeSize, R.drawable.ic_swipe_circle_dark);
-
- // If more pages exist before the first page indicator, make it a fade-in gradient.
- if (rangeStart > 0) {
- states[0] = R.drawable.ic_swipe_circle_top;
- }
-
- // If more pages exist after the last page indicator, make it a fade-out gradient.
- if (rangeEnd < pageCount - 1) {
- states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom;
- }
-
- // Set the indicator of the selected page to be light.
- states[page - rangeStart] = R.drawable.ic_swipe_circle_light;
-
- return states;
- }
-
- /**
- * Display the view that creates a new timer.
- */
- private void showCreateTimerView(int updateTypes) {
- // Stop animating the timers.
- stopUpdatingTime();
-
- // Show the creation view; hide the timer view.
- mTimersView.setVisibility(GONE);
- mCreateTimerView.setVisibility(VISIBLE);
-
- // Record the fact that the create view is visible.
- mCurrentView = mCreateTimerView;
-
- // Update the fab and buttons.
- updateFab(updateTypes);
- }
-
- /**
- * Display the view that lists all existing timers.
- */
- private void showTimersView(int updateTypes) {
- // Clear any defunct timer creation state; the next timer creation starts fresh.
- mTimerSetupState = null;
-
- // Show the timer view; hide the creation view.
- mTimersView.setVisibility(VISIBLE);
- mCreateTimerView.setVisibility(GONE);
-
- // Record the fact that the create view is visible.
- mCurrentView = mTimersView;
-
- // Update the fab and buttons.
- updateFab(updateTypes);
-
- // Start animating the timers.
- startUpdatingTime();
- }
-
- /**
- * @param timerToRemove the timer to be removed during the animation
- */
- private void animateTimerRemove(final Timer timerToRemove) {
- final long duration = UiDataModel.getUiDataModel().getShortAnimationDuration();
-
- final Animator fadeOut = ObjectAnimator.ofFloat(mViewPager, ALPHA, 1, 0);
- fadeOut.setDuration(duration);
- fadeOut.setInterpolator(new DecelerateInterpolator());
- fadeOut.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- DataModel.getDataModel().removeTimer(timerToRemove);
- Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
- }
- });
-
- final Animator fadeIn = ObjectAnimator.ofFloat(mViewPager, ALPHA, 0, 1);
- fadeIn.setDuration(duration);
- fadeIn.setInterpolator(new AccelerateInterpolator());
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.play(fadeOut).before(fadeIn);
- animatorSet.start();
- }
-
- /**
- * @param toView one of {@link #mTimersView} or {@link #mCreateTimerView}
- * @param timerToRemove the timer to be removed during the animation; {@code null} if no timer
- * should be removed
- * @param animateDown {@code true} if the views should animate upwards, otherwise downwards
- */
- private void animateToView(final View toView, final Timer timerToRemove,
- final boolean animateDown) {
- if (mCurrentView == toView) {
- return;
- }
-
- final boolean toTimers = toView == mTimersView;
- if (toTimers) {
- mTimersView.setVisibility(VISIBLE);
- } else {
- mCreateTimerView.setVisibility(VISIBLE);
- }
- // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
- updateFab(BUTTONS_DISABLE);
-
- final long animationDuration = UiDataModel.getUiDataModel().getLongAnimationDuration();
-
- final ViewTreeObserver viewTreeObserver = toView.getViewTreeObserver();
- viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- if (viewTreeObserver.isAlive()) {
- viewTreeObserver.removeOnPreDrawListener(this);
- }
-
- final View view = mTimersView.findViewById(R.id.timer_time);
- final float distanceY = view != null ? view.getHeight() + view.getY() : 0;
- final float translationDistance = animateDown ? distanceY : -distanceY;
-
- toView.setTranslationY(-translationDistance);
- mCurrentView.setTranslationY(0f);
- toView.setAlpha(0f);
- mCurrentView.setAlpha(1f);
-
- final Animator translateCurrent = ObjectAnimator.ofFloat(mCurrentView,
- TRANSLATION_Y, translationDistance);
- final Animator translateNew = ObjectAnimator.ofFloat(toView, TRANSLATION_Y, 0f);
- final AnimatorSet translationAnimatorSet = new AnimatorSet();
- translationAnimatorSet.playTogether(translateCurrent, translateNew);
- translationAnimatorSet.setDuration(animationDuration);
- translationAnimatorSet.setInterpolator(AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN);
-
- final Animator fadeOutAnimator = ObjectAnimator.ofFloat(mCurrentView, ALPHA, 0f);
- fadeOutAnimator.setDuration(animationDuration / 2);
- fadeOutAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- super.onAnimationStart(animation);
-
- // The fade-out animation and fab-shrinking animation should run together.
- updateFab(FAB_AND_BUTTONS_SHRINK);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- if (toTimers) {
- showTimersView(FAB_AND_BUTTONS_EXPAND);
-
- // Reset the state of the create view.
- mCreateTimerView.reset();
- } else {
- showCreateTimerView(FAB_AND_BUTTONS_EXPAND);
- }
-
- if (timerToRemove != null) {
- DataModel.getDataModel().removeTimer(timerToRemove);
- Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock);
- }
-
- // Update the fab and button states now that the correct view is visible and
- // before the animation to expand the fab and buttons starts.
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
- }
- });
-
- final Animator fadeInAnimator = ObjectAnimator.ofFloat(toView, ALPHA, 1f);
- fadeInAnimator.setDuration(animationDuration / 2);
- fadeInAnimator.setStartDelay(animationDuration / 2);
-
- final AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet);
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- mTimersView.setTranslationY(0f);
- mCreateTimerView.setTranslationY(0f);
- mTimersView.setAlpha(1f);
- mCreateTimerView.setAlpha(1f);
- }
- });
- animatorSet.start();
-
- return true;
- }
- });
- }
-
- private boolean hasTimers() {
- return mAdapter.getCount() > 0;
- }
-
- private Timer getTimer() {
- if (mViewPager == null) {
- return null;
- }
-
- return mAdapter.getCount() == 0 ? null : mAdapter.getTimer(mViewPager.getCurrentItem());
- }
-
- private void startUpdatingTime() {
- // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
- stopUpdatingTime();
- mViewPager.post(mTimeUpdateRunnable);
- }
-
- private void stopUpdatingTime() {
- mViewPager.removeCallbacks(mTimeUpdateRunnable);
- }
-
- /**
- * Periodically refreshes the state of each timer.
- */
- private class TimeUpdateRunnable implements Runnable {
- @Override
- public void run() {
- final long startTime = SystemClock.elapsedRealtime();
- // If no timers require continuous updates, avoid scheduling the next update.
- if (!mAdapter.updateTime()) {
- return;
- }
- final long endTime = SystemClock.elapsedRealtime();
-
- // Try to maintain a consistent period of time between redraws.
- final long delay = Math.max(0, startTime + 20 - endTime);
- mTimersView.postDelayed(this, delay);
- }
- }
-
- /**
- * Update the page indicators and fab in response to a new timer becoming visible.
- */
- private class TimerPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
- @Override
- public void onPageSelected(int position) {
- updatePageIndicators();
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-
- // Showing a new timer page may introduce a timer requiring continuous updates.
- startUpdatingTime();
- }
-
- @Override
- public void onPageScrollStateChanged(int state) {
- // Teasing a neighboring timer may introduce a timer requiring continuous updates.
- if (state == ViewPager.SCROLL_STATE_DRAGGING) {
- startUpdatingTime();
- }
- }
- }
-
- /**
- * Update the page indicators in response to timers being added or removed.
- * Update the fab in response to the visible timer changing.
- */
- private class TimerWatcher implements TimerListener {
- @Override
- public void timerAdded(Timer timer) {
- updatePageIndicators();
- // If the timer is being created via this fragment avoid adjusting the fab.
- // Timer setup view is about to be animated away in response to this timer creation.
- // Changes to the fab immediately preceding that animation are jarring.
- if (!mCreatingTimer) {
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
- }
- }
-
- @Override
- public void timerUpdated(Timer before, Timer after) {
- // If the timer started, animate the timers.
- if (before.isReset() && !after.isReset()) {
- startUpdatingTime();
- }
-
- // Fetch the index of the change.
- final int index = DataModel.getDataModel().getTimers().indexOf(after);
-
- // If the timer just expired but is not displayed, display it now.
- if (!before.isExpired() && after.isExpired() && index != mViewPager.getCurrentItem()) {
- mViewPager.setCurrentItem(index, true);
-
- } else if (mCurrentView == mTimersView && index == mViewPager.getCurrentItem()) {
- // Morph the fab from its old state to new state if necessary.
- if (before.getState() != after.getState()
- && !(before.isPaused() && after.isReset())) {
- updateFab(FAB_MORPH);
- }
- }
- }
-
- @Override
- public void timerRemoved(Timer timer) {
- updatePageIndicators();
- updateFab(FAB_AND_BUTTONS_IMMEDIATE);
-
- if (mCurrentView == mTimersView && mAdapter.getCount() == 0) {
- animateToView(mCreateTimerView, null, false);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerFragment.kt b/src/com/android/deskclock/timer/TimerFragment.kt
new file mode 100644
index 0000000..883c901
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerFragment.kt
@@ -0,0 +1,757 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnPreDrawListener
+import android.view.animation.AccelerateInterpolator
+import android.view.animation.DecelerateInterpolator
+import android.widget.Button
+import android.widget.ImageView
+import androidx.annotation.VisibleForTesting
+import androidx.viewpager.widget.ViewPager
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.data.TimerListener
+import com.android.deskclock.data.TimerStringFormatter
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+import com.android.deskclock.AnimatorUtils
+import com.android.deskclock.DeskClock
+import com.android.deskclock.DeskClockFragment
+import com.android.deskclock.FabContainer
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+
+import java.io.Serializable
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Displays a vertical list of timers in all states.
+ */
+class TimerFragment : DeskClockFragment(UiDataModel.Tab.TIMERS) {
+ /** Notified when the user swipes vertically to change the visible timer. */
+ private val mTimerPageChangeListener = TimerPageChangeListener()
+
+ /** Scheduled to update the timers while at least one is running. */
+ private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable()
+
+ /** Updates the [.mPageIndicators] in response to timers being added or removed. */
+ private val mTimerWatcher: TimerListener = TimerWatcher()
+
+ private lateinit var mCreateTimerView: TimerSetupView
+ private lateinit var mViewPager: ViewPager
+ private lateinit var mAdapter: TimerPagerAdapter
+ private var mTimersView: View? = null
+ private var mCurrentView: View? = null
+ private lateinit var mPageIndicators: Array<ImageView>
+
+ private var mTimerSetupState: Serializable? = null
+
+ /** `true` while this fragment is creating a new timer; `false` otherwise. */
+ private var mCreatingTimer = false
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val view = inflater.inflate(R.layout.timer_fragment, container, false)
+
+ mAdapter = TimerPagerAdapter(parentFragmentManager)
+ mViewPager = view.findViewById<View>(R.id.vertical_view_pager) as ViewPager
+ mViewPager.setAdapter(mAdapter)
+ mViewPager.addOnPageChangeListener(mTimerPageChangeListener)
+
+ mTimersView = view.findViewById(R.id.timer_view)
+ mCreateTimerView = view.findViewById<View>(R.id.timer_setup) as TimerSetupView
+ mCreateTimerView.setFabContainer(this)
+ mPageIndicators = arrayOf(
+ view.findViewById<View>(R.id.page_indicator0) as ImageView,
+ view.findViewById<View>(R.id.page_indicator1) as ImageView,
+ view.findViewById<View>(R.id.page_indicator2) as ImageView,
+ view.findViewById<View>(R.id.page_indicator3) as ImageView
+ )
+
+ DataModel.dataModel.addTimerListener(mAdapter)
+ DataModel.dataModel.addTimerListener(mTimerWatcher)
+
+ // If timer setup state is present, retrieve it to be later honored.
+ savedInstanceState?.let {
+ mTimerSetupState = it.getSerializable(KEY_TIMER_SETUP_STATE)
+ }
+
+ return view
+ }
+
+ override fun onStart() {
+ super.onStart()
+
+ // Initialize the page indicators.
+ updatePageIndicators()
+ var createTimer = false
+ var showTimerId = -1
+
+ // Examine the intent of the parent activity to determine which view to display.
+ val intent = requireActivity().intent
+ intent?.let {
+ // These extras are single-use; remove them after honoring them.
+ createTimer = it.getBooleanExtra(EXTRA_TIMER_SETUP, false)
+ it.removeExtra(EXTRA_TIMER_SETUP)
+
+ showTimerId = it.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
+ it.removeExtra(TimerService.EXTRA_TIMER_ID)
+ }
+
+ // Choose the view to display in this fragment.
+ if (showTimerId != -1) {
+ // A specific timer must be shown; show the list of timers.
+ showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+ } else if (!hasTimers() || createTimer || mTimerSetupState != null) {
+ // No timers exist, a timer is being created, or the last view was timer setup;
+ // show the timer setup view.
+ showCreateTimerView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+ if (mTimerSetupState != null) {
+ mCreateTimerView.state = mTimerSetupState
+ mTimerSetupState = null
+ }
+ } else {
+ // Otherwise, default to showing the list of timers.
+ showTimersView(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+ }
+
+ // If the intent did not specify a timer to show, show the last timer that expired.
+ if (showTimerId == -1) {
+ val timer: Timer? = DataModel.dataModel.mostRecentExpiredTimer
+ showTimerId = timer?.id ?: -1
+ }
+
+ // If a specific timer should be displayed, display the corresponding timer tab.
+ if (showTimerId != -1) {
+ val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
+ timer?.let {
+ val index: Int = DataModel.dataModel.timers.indexOf(it)
+ mViewPager.setCurrentItem(index)
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // We may have received a new intent while paused.
+ val intent = requireActivity().intent
+ if (intent != null && intent.hasExtra(TimerService.EXTRA_TIMER_ID)) {
+ // This extra is single-use; remove after honoring it.
+ val showTimerId = intent.getIntExtra(TimerService.EXTRA_TIMER_ID, -1)
+ intent.removeExtra(TimerService.EXTRA_TIMER_ID)
+
+ val timer: Timer? = DataModel.dataModel.getTimer(showTimerId)
+ timer?.let {
+ // A specific timer must be shown; show the list of timers.
+ val index: Int = DataModel.dataModel.timers.indexOf(it)
+ mViewPager.setCurrentItem(index)
+
+ animateToView(mTimersView, null, false)
+ }
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+
+ // Stop updating the timers when this fragment is no longer visible.
+ stopUpdatingTime()
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+
+ DataModel.dataModel.removeTimerListener(mAdapter)
+ DataModel.dataModel.removeTimerListener(mTimerWatcher)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+
+ // If the timer creation view is visible, store the input for later restoration.
+ if (mCurrentView === mCreateTimerView) {
+ mTimerSetupState = mCreateTimerView.state
+ outState.putSerializable(KEY_TIMER_SETUP_STATE, mTimerSetupState)
+ }
+ }
+
+ private fun updateFab(fab: ImageView, animate: Boolean) {
+ if (mCurrentView === mTimersView) {
+ val timer = timer
+ if (timer == null) {
+ fab.visibility = View.INVISIBLE
+ return
+ }
+
+ fab.visibility = View.VISIBLE
+ when (timer.state) {
+ Timer.State.RUNNING -> {
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_play_pause_animation)
+ } else {
+ fab.setImageResource(R.drawable.ic_play_pause)
+ }
+ fab.contentDescription = fab.resources.getString(R.string.timer_stop)
+ }
+ Timer.State.RESET -> {
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_stop_play_animation)
+ } else {
+ fab.setImageResource(R.drawable.ic_pause_play)
+ }
+ fab.contentDescription = fab.resources.getString(R.string.timer_start)
+ }
+ Timer.State.PAUSED -> {
+ if (animate) {
+ fab.setImageResource(R.drawable.ic_pause_play_animation)
+ } else {
+ fab.setImageResource(R.drawable.ic_pause_play)
+ }
+ fab.contentDescription = fab.resources.getString(R.string.timer_start)
+ }
+ Timer.State.MISSED, Timer.State.EXPIRED -> {
+ fab.setImageResource(R.drawable.ic_stop_white_24dp)
+ fab.contentDescription = fab.resources.getString(R.string.timer_stop)
+ }
+ }
+ } else if (mCurrentView === mCreateTimerView) {
+ if (mCreateTimerView.hasValidInput()) {
+ fab.setImageResource(R.drawable.ic_start_white_24dp)
+ fab.contentDescription = fab.resources.getString(R.string.timer_start)
+ fab.visibility = View.VISIBLE
+ } else {
+ fab.contentDescription = null
+ fab.visibility = View.INVISIBLE
+ }
+ }
+ }
+
+ override fun onUpdateFab(fab: ImageView) {
+ updateFab(fab, false)
+ }
+
+ override fun onMorphFab(fab: ImageView) {
+ // Update the fab's drawable to match the current timer state.
+ updateFab(fab, Utils.isNOrLater)
+ // Animate the drawable.
+ AnimatorUtils.startDrawableAnimation(fab)
+ }
+
+ override fun onUpdateFabButtons(left: Button, right: Button) {
+ if (mCurrentView === mTimersView) {
+ left.isClickable = true
+ left.setText(R.string.timer_delete)
+ left.contentDescription = left.resources.getString(R.string.timer_delete)
+ left.visibility = View.VISIBLE
+
+ right.isClickable = true
+ right.setText(R.string.timer_add_timer)
+ right.contentDescription = right.resources.getString(R.string.timer_add_timer)
+ right.visibility = View.VISIBLE
+ } else if (mCurrentView === mCreateTimerView) {
+ left.isClickable = true
+ left.setText(R.string.timer_cancel)
+ left.contentDescription = left.resources.getString(R.string.timer_cancel)
+ // If no timers yet exist, the user is forced to create the first one.
+ left.visibility = if (hasTimers()) View.VISIBLE else View.INVISIBLE
+
+ right.visibility = View.INVISIBLE
+ }
+ }
+
+ override fun onFabClick(fab: ImageView) {
+ if (mCurrentView === mTimersView) {
+ // If no timer is currently showing a fab action is meaningless.
+ val timer = timer ?: return
+
+ val context = fab.context
+ val currentTime: Long = timer.remainingTime
+
+ when (timer.state) {
+ Timer.State.RUNNING -> {
+ DataModel.dataModel.pauseTimer(timer)
+ Events.sendTimerEvent(R.string.action_stop, R.string.label_deskclock)
+ if (currentTime > 0) {
+ mTimersView?.announceForAccessibility(TimerStringFormatter.formatString(
+ context, R.string.timer_accessibility_stopped, currentTime, true))
+ }
+ }
+ Timer.State.PAUSED, Timer.State.RESET -> {
+ DataModel.dataModel.startTimer(timer)
+ Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock)
+ if (currentTime > 0) {
+ mTimersView?.announceForAccessibility(TimerStringFormatter.formatString(
+ context, R.string.timer_accessibility_started, currentTime, true))
+ }
+ }
+ Timer.State.MISSED, Timer.State.EXPIRED -> {
+ DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock)
+ }
+ }
+ } else if (mCurrentView === mCreateTimerView) {
+ mCreatingTimer = true
+ try {
+ // Create the new timer.
+ val timerLength: Long = mCreateTimerView.timeInMillis
+ val timer: Timer = DataModel.dataModel.addTimer(timerLength, "", false)
+ Events.sendTimerEvent(R.string.action_create, R.string.label_deskclock)
+
+ // Start the new timer.
+ DataModel.dataModel.startTimer(timer)
+ Events.sendTimerEvent(R.string.action_start, R.string.label_deskclock)
+
+ // Display the freshly created timer view.
+ mViewPager.setCurrentItem(0)
+ } finally {
+ mCreatingTimer = false
+ }
+
+ // Return to the list of timers.
+ animateToView(mTimersView, null, true)
+ }
+ }
+
+ override fun onLeftButtonClick(left: Button) {
+ if (mCurrentView === mTimersView) {
+ // Clicking the "delete" button.
+ val timer = timer ?: return
+
+ if (mAdapter.getCount() > 1) {
+ animateTimerRemove(timer)
+ } else {
+ animateToView(mCreateTimerView, timer, false)
+ }
+
+ left.announceForAccessibility(requireActivity().getString(R.string.timer_deleted))
+ } else if (mCurrentView === mCreateTimerView) {
+ // Clicking the "cancel" button on the timer creation page returns to the timers list.
+ mCreateTimerView.reset()
+
+ animateToView(mTimersView, null, false)
+
+ left.announceForAccessibility(requireActivity().getString(R.string.timer_canceled))
+ }
+ }
+
+ override fun onRightButtonClick(right: Button) {
+ if (mCurrentView !== mCreateTimerView) {
+ animateToView(mCreateTimerView, null, true)
+ }
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ return if (mCurrentView === mCreateTimerView) {
+ mCreateTimerView.onKeyDown(keyCode, event)
+ } else super.onKeyDown(keyCode, event)
+ }
+
+ /**
+ * Updates the state of the page indicators so they reflect the selected page in the context of
+ * all pages.
+ */
+ private fun updatePageIndicators() {
+ val page: Int = mViewPager.getCurrentItem()
+ val pageIndicatorCount = mPageIndicators.size
+ val pageCount = mAdapter.getCount()
+
+ val states = computePageIndicatorStates(page, pageIndicatorCount, pageCount)
+ for (i in states.indices) {
+ val state = states[i]
+ val pageIndicator = mPageIndicators[i]
+ if (state == 0) {
+ pageIndicator.visibility = View.GONE
+ } else {
+ pageIndicator.visibility = View.VISIBLE
+ pageIndicator.setImageResource(state)
+ }
+ }
+ }
+
+ /**
+ * Display the view that creates a new timer.
+ */
+ private fun showCreateTimerView(updateTypes: Int) {
+ // Stop animating the timers.
+ stopUpdatingTime()
+
+ // Show the creation view; hide the timer view.
+ mTimersView?.visibility = View.GONE
+ mCreateTimerView.visibility = View.VISIBLE
+
+ // Record the fact that the create view is visible.
+ mCurrentView = mCreateTimerView
+
+ // Update the fab and buttons.
+ updateFab(updateTypes)
+ }
+
+ /**
+ * Display the view that lists all existing timers.
+ */
+ private fun showTimersView(updateTypes: Int) {
+ // Clear any defunct timer creation state; the next timer creation starts fresh.
+ mTimerSetupState = null
+
+ // Show the timer view; hide the creation view.
+ mTimersView?.visibility = View.VISIBLE
+ mCreateTimerView.visibility = View.GONE
+
+ // Record the fact that the create view is visible.
+ mCurrentView = mTimersView
+
+ // Update the fab and buttons.
+ updateFab(updateTypes)
+
+ // Start animating the timers.
+ startUpdatingTime()
+ }
+
+ /**
+ * @param timerToRemove the timer to be removed during the animation
+ */
+ private fun animateTimerRemove(timerToRemove: Timer) {
+ val duration = UiDataModel.uiDataModel.shortAnimationDuration
+
+ val fadeOut: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 1f, 0f)
+ fadeOut.duration = duration
+ fadeOut.interpolator = DecelerateInterpolator()
+ fadeOut.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ DataModel.dataModel.removeTimer(timerToRemove)
+ Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock)
+ }
+ })
+
+ val fadeIn: Animator = ObjectAnimator.ofFloat(mViewPager, View.ALPHA, 0f, 1f)
+ fadeIn.duration = duration
+ fadeIn.interpolator = AccelerateInterpolator()
+
+ val animatorSet = AnimatorSet()
+ animatorSet.play(fadeOut).before(fadeIn)
+ animatorSet.start()
+ }
+
+ /**
+ * @param toView one of [.mTimersView] or [.mCreateTimerView]
+ * @param timerToRemove the timer to be removed during the animation; `null` if no timer
+ * should be removed
+ * @param animateDown `true` if the views should animate upwards, otherwise downwards
+ */
+ private fun animateToView(
+ toView: View?,
+ timerToRemove: Timer?,
+ animateDown: Boolean
+ ) {
+ if (mCurrentView === toView) {
+ return
+ }
+
+ val toTimers = toView === mTimersView
+ if (toTimers) {
+ mTimersView?.visibility = View.VISIBLE
+ } else {
+ mCreateTimerView.visibility = View.VISIBLE
+ }
+ // Avoid double-taps by enabling/disabling the set of buttons active on the new view.
+ updateFab(FabContainer.BUTTONS_DISABLE)
+
+ val animationDuration = UiDataModel.uiDataModel.longAnimationDuration
+
+ val viewTreeObserver = toView!!.viewTreeObserver
+ viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ if (viewTreeObserver.isAlive) {
+ viewTreeObserver.removeOnPreDrawListener(this)
+ }
+
+ val view = mTimersView?.findViewById<View>(R.id.timer_time)
+ val distanceY: Float = if (view != null) view.height + view.y else 0f
+ val translationDistance = if (animateDown) distanceY else -distanceY
+
+ toView.translationY = -translationDistance
+ mCurrentView?.translationY = 0f
+ toView.alpha = 0f
+ mCurrentView?.alpha = 1f
+
+ val translateCurrent: Animator = ObjectAnimator.ofFloat(mCurrentView,
+ View.TRANSLATION_Y, translationDistance)
+ val translateNew: Animator = ObjectAnimator.ofFloat(toView, View.TRANSLATION_Y, 0f)
+ val translationAnimatorSet = AnimatorSet()
+ translationAnimatorSet.playTogether(translateCurrent, translateNew)
+ translationAnimatorSet.duration = animationDuration
+ translationAnimatorSet.interpolator = AnimatorUtils.INTERPOLATOR_FAST_OUT_SLOW_IN
+
+ val fadeOutAnimator: Animator = ObjectAnimator.ofFloat(mCurrentView, View.ALPHA, 0f)
+ fadeOutAnimator.duration = animationDuration / 2
+ fadeOutAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ super.onAnimationStart(animation)
+
+ // The fade-out animation and fab-shrinking animation should run together.
+ updateFab(FabContainer.FAB_AND_BUTTONS_SHRINK)
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ super.onAnimationEnd(animation)
+ if (toTimers) {
+ showTimersView(FabContainer.FAB_AND_BUTTONS_EXPAND)
+
+ // Reset the state of the create view.
+ mCreateTimerView.reset()
+ } else {
+ showCreateTimerView(FabContainer.FAB_AND_BUTTONS_EXPAND)
+ }
+ if (timerToRemove != null) {
+ DataModel.dataModel.removeTimer(timerToRemove)
+ Events.sendTimerEvent(R.string.action_delete, R.string.label_deskclock)
+ }
+
+ // Update the fab and button states now that the correct view is visible and
+ // before the animation to expand the fab and buttons starts.
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+ }
+ })
+
+ val fadeInAnimator: Animator = ObjectAnimator.ofFloat(toView, View.ALPHA, 1f)
+ fadeInAnimator.duration = animationDuration / 2
+ fadeInAnimator.startDelay = animationDuration / 2
+
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(fadeOutAnimator, fadeInAnimator, translationAnimatorSet)
+ animatorSet.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ super.onAnimationEnd(animation)
+ mTimersView?.translationY = 0f
+ mCreateTimerView.translationY = 0f
+ mTimersView?.alpha = 1f
+ mCreateTimerView.alpha = 1f
+ }
+ })
+ animatorSet.start()
+
+ return true
+ }
+ })
+ }
+
+ private fun hasTimers(): Boolean {
+ return mAdapter.getCount() > 0
+ }
+
+ private val timer: Timer?
+ get() {
+ if (!::mViewPager.isInitialized) {
+ return null
+ }
+
+ return if (mAdapter.getCount() == 0) {
+ null
+ } else {
+ mAdapter.getTimer(mViewPager.getCurrentItem())
+ }
+ }
+
+ private fun startUpdatingTime() {
+ // Ensure only one copy of the runnable is ever scheduled by first stopping updates.
+ stopUpdatingTime()
+ mViewPager.post(mTimeUpdateRunnable)
+ }
+
+ private fun stopUpdatingTime() {
+ mViewPager.removeCallbacks(mTimeUpdateRunnable)
+ }
+
+ /**
+ * Periodically refreshes the state of each timer.
+ */
+ private inner class TimeUpdateRunnable : Runnable {
+ override fun run() {
+ val startTime = SystemClock.elapsedRealtime()
+ // If no timers require continuous updates, avoid scheduling the next update.
+ if (!mAdapter.updateTime()) {
+ return
+ }
+ val endTime = SystemClock.elapsedRealtime()
+
+ // Try to maintain a consistent period of time between redraws.
+ val delay = max(0, startTime + 20 - endTime)
+ mTimersView?.postDelayed(this, delay)
+ }
+ }
+
+ /**
+ * Update the page indicators and fab in response to a new timer becoming visible.
+ */
+ private inner class TimerPageChangeListener : ViewPager.SimpleOnPageChangeListener() {
+ override fun onPageSelected(position: Int) {
+ updatePageIndicators()
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+ // Showing a new timer page may introduce a timer requiring continuous updates.
+ startUpdatingTime()
+ }
+
+ override fun onPageScrollStateChanged(state: Int) {
+ // Teasing a neighboring timer may introduce a timer requiring continuous updates.
+ if (state == ViewPager.SCROLL_STATE_DRAGGING) {
+ startUpdatingTime()
+ }
+ }
+ }
+
+ /**
+ * Update the page indicators in response to timers being added or removed.
+ * Update the fab in response to the visible timer changing.
+ */
+ private inner class TimerWatcher : TimerListener {
+ override fun timerAdded(timer: Timer) {
+ updatePageIndicators()
+ // If the timer is being created via this fragment avoid adjusting the fab.
+ // Timer setup view is about to be animated away in response to this timer creation.
+ // Changes to the fab immediately preceding that animation are jarring.
+ if (!mCreatingTimer) {
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+ }
+ }
+
+ override fun timerUpdated(before: Timer, after: Timer) {
+ // If the timer started, animate the timers.
+ if (before.isReset && !after.isReset) {
+ startUpdatingTime()
+ }
+
+ // Fetch the index of the change.
+ val index: Int = DataModel.dataModel.timers.indexOf(after)
+
+ // If the timer just expired but is not displayed, display it now.
+ if (!before.isExpired && after.isExpired && index != mViewPager.getCurrentItem()) {
+ mViewPager.setCurrentItem(index, true)
+ } else if (mCurrentView === mTimersView && index == mViewPager.getCurrentItem()) {
+ // Morph the fab from its old state to new state if necessary.
+ if (before.state != after.state &&
+ !(before.isPaused && after.isReset)) {
+ updateFab(FabContainer.FAB_MORPH)
+ }
+ }
+ }
+
+ override fun timerRemoved(timer: Timer) {
+ updatePageIndicators()
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE)
+
+ if (mCurrentView === mTimersView && mAdapter.getCount() == 0) {
+ animateToView(mCreateTimerView, null, false)
+ }
+ }
+ }
+
+ companion object {
+ private const val EXTRA_TIMER_SETUP = "com.android.deskclock.action.TIMER_SETUP"
+
+ private const val KEY_TIMER_SETUP_STATE = "timer_setup_input"
+
+ /**
+ * @return an Intent that selects the timers tab with the
+ * setup screen for a new timer in place.
+ */
+ @VisibleForTesting
+ @JvmStatic
+ fun createTimerSetupIntent(context: Context): Intent {
+ return Intent(context, DeskClock::class.java).putExtra(EXTRA_TIMER_SETUP, true)
+ }
+
+ /**
+ * @param page the selected page; value between 0 and `pageCount`
+ * @param pageIndicatorCount the number of indicators displaying the `page` location
+ * @param pageCount the number of pages that exist
+ * @return an array of length `pageIndicatorCount` specifying which image to display for
+ * each page indicator or 0 if the page indicator should be hidden
+ */
+ @VisibleForTesting
+ @JvmStatic
+ fun computePageIndicatorStates(
+ page: Int,
+ pageIndicatorCount: Int,
+ pageCount: Int
+ ): IntArray {
+ // Compute the number of page indicators that will be visible.
+ val rangeSize = min(pageIndicatorCount, pageCount)
+
+ // Compute the inclusive range of pages to indicate centered around the selected page.
+ var rangeStart = page - rangeSize / 2
+ var rangeEnd = rangeStart + rangeSize - 1
+
+ // Clamp the range of pages if they extend beyond the last page.
+ if (rangeEnd >= pageCount) {
+ rangeEnd = pageCount - 1
+ rangeStart = rangeEnd - rangeSize + 1
+ }
+
+ // Clamp the range of pages if they extend beyond the first page.
+ if (rangeStart < 0) {
+ rangeStart = 0
+ rangeEnd = rangeSize - 1
+ }
+
+ // Build the result with all page indicators initially hidden.
+ val states = IntArray(pageIndicatorCount)
+ states.fill(0)
+
+ // If 0 or 1 total pages exist, all page indicators must remain hidden.
+ if (rangeSize < 2) {
+ return states
+ }
+
+ // Initialize the visible page indicators to be dark.
+ states.fill(R.drawable.ic_swipe_circle_dark, 0, rangeSize)
+
+ // If more pages exist before the first page indicator, make it a fade-in gradient.
+ if (rangeStart > 0) {
+ states[0] = R.drawable.ic_swipe_circle_top
+ }
+
+ // If more pages exist after the last page indicator, make it a fade-out gradient.
+ if (rangeEnd < pageCount - 1) {
+ states[rangeSize - 1] = R.drawable.ic_swipe_circle_bottom
+ }
+
+ // Set the indicator of the selected page to be light.
+ states[page - rangeStart] = R.drawable.ic_swipe_circle_light
+ return states
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerItem.java b/src/com/android/deskclock/timer/TimerItem.java
deleted file mode 100644
index d122d77..0000000
--- a/src/com/android/deskclock/timer/TimerItem.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.os.SystemClock;
-import androidx.core.view.ViewCompat;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.deskclock.R;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.TimerTextController;
-import com.android.deskclock.Utils.ClickAccessibilityDelegate;
-import com.android.deskclock.data.Timer;
-
-import static android.R.attr.state_activated;
-import static android.R.attr.state_pressed;
-
-/**
- * This view is a visual representation of a {@link Timer}.
- */
-public class TimerItem extends LinearLayout {
-
- /** Displays the remaining time or time since expiration. */
- private TextView mTimerText;
-
- /** Formats and displays the text in the timer. */
- private TimerTextController mTimerTextController;
-
- /** Displays timer progress as a color circle that changes from white to red. */
- private TimerCircleView mCircleView;
-
- /** A button that either resets the timer or adds time to it, depending on its state. */
- private Button mResetAddButton;
-
- /** Displays the label associated with the timer. Tapping it presents an edit dialog. */
- private TextView mLabelView;
-
- /** The last state of the timer that was rendered; used to avoid expensive operations. */
- private Timer.State mLastState;
-
- public TimerItem(Context context) {
- this(context, null);
- }
-
- public TimerItem(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mLabelView = (TextView) findViewById(R.id.timer_label);
- mResetAddButton = (Button) findViewById(R.id.reset_add);
- mCircleView = (TimerCircleView) findViewById(R.id.timer_time);
- mTimerText = (TextView) findViewById(R.id.timer_time_text);
- mTimerTextController = new TimerTextController(mTimerText);
-
- final Context c = mTimerText.getContext();
- final int colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent);
- final int textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary);
- mTimerText.setTextColor(new ColorStateList(
- new int[][] { { -state_activated, -state_pressed }, {} },
- new int[] { textColorPrimary, colorAccent }));
- }
-
- /**
- * Updates this view to display the latest state of the {@code timer}.
- */
- void update(Timer timer) {
- // Update the time.
- mTimerTextController.setTimeString(timer.getRemainingTime());
-
- // Update the label if it changed.
- final String label = timer.getLabel();
- if (!TextUtils.equals(label, mLabelView.getText())) {
- mLabelView.setText(label);
- }
-
- // Update visibility of things that may blink.
- final boolean blinkOff = SystemClock.elapsedRealtime() % 1000 < 500;
- if (mCircleView != null) {
- final boolean hideCircle = (timer.isExpired() || timer.isMissed()) && blinkOff;
- mCircleView.setVisibility(hideCircle ? INVISIBLE : VISIBLE);
-
- if (!hideCircle) {
- // Update the progress of the circle.
- mCircleView.update(timer);
- }
- }
- if (!timer.isPaused() || !blinkOff || mTimerText.isPressed()) {
- mTimerText.setAlpha(1f);
- } else {
- mTimerText.setAlpha(0f);
- }
-
- // Update some potentially expensive areas of the user interface only on state changes.
- if (timer.getState() != mLastState) {
- mLastState = timer.getState();
- final Context context = getContext();
- switch (mLastState) {
- case RESET:
- case PAUSED: {
- mResetAddButton.setText(R.string.timer_reset);
- mResetAddButton.setContentDescription(null);
- mTimerText.setClickable(true);
- mTimerText.setActivated(false);
- mTimerText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- ViewCompat.setAccessibilityDelegate(mTimerText, new ClickAccessibilityDelegate(
- context.getString(R.string.timer_start), true));
- break;
- }
- case RUNNING: {
- final String addTimeDesc = context.getString(R.string.timer_plus_one);
- mResetAddButton.setText(R.string.timer_add_minute);
- mResetAddButton.setContentDescription(addTimeDesc);
- mTimerText.setClickable(true);
- mTimerText.setActivated(false);
- mTimerText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- ViewCompat.setAccessibilityDelegate(mTimerText, new ClickAccessibilityDelegate(
- context.getString(R.string.timer_pause)));
- break;
- }
- case EXPIRED:
- case MISSED: {
- final String addTimeDesc = context.getString(R.string.timer_plus_one);
- mResetAddButton.setText(R.string.timer_add_minute);
- mResetAddButton.setContentDescription(addTimeDesc);
- mTimerText.setClickable(false);
- mTimerText.setActivated(true);
- mTimerText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- break;
- }
- }
- }
- }
-}
diff --git a/src/com/android/deskclock/timer/TimerItem.kt b/src/com/android/deskclock/timer/TimerItem.kt
new file mode 100644
index 0000000..9cdcca4
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerItem.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.R.attr
+import android.content.Context
+import android.content.res.ColorStateList
+import android.os.SystemClock
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.View
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.view.ViewCompat
+
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.TimerTextController
+import com.android.deskclock.Utils.ClickAccessibilityDelegate
+import com.android.deskclock.data.Timer
+
+/**
+ * This view is a visual representation of a [Timer].
+ */
+class TimerItem @JvmOverloads constructor(
+ context: Context?,
+ attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+ /** Displays the remaining time or time since expiration. */
+ private lateinit var mTimerText: TextView
+
+ /** Formats and displays the text in the timer. */
+ private lateinit var mTimerTextController: TimerTextController
+
+ /** Displays timer progress as a color circle that changes from white to red. */
+ private var mCircleView: TimerCircleView? = null
+
+ /** A button that either resets the timer or adds time to it, depending on its state. */
+ private lateinit var mResetAddButton: Button
+
+ /** Displays the label associated with the timer. Tapping it presents an edit dialog. */
+ private lateinit var mLabelView: TextView
+
+ /** The last state of the timer that was rendered; used to avoid expensive operations. */
+ private var mLastState: Timer.State? = null
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ mLabelView = findViewById<View>(R.id.timer_label) as TextView
+ mResetAddButton = findViewById<View>(R.id.reset_add) as Button
+ mCircleView = findViewById<View>(R.id.timer_time) as TimerCircleView
+ mTimerText = findViewById<View>(R.id.timer_time_text) as TextView
+ mTimerTextController = TimerTextController(mTimerText)
+
+ val c = mTimerText.context
+ val colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent)
+ val textColorPrimary = ThemeUtils.resolveColor(c, attr.textColorPrimary)
+ mTimerText.setTextColor(ColorStateList(
+ arrayOf(intArrayOf(-attr.state_activated, -attr.state_pressed),
+ intArrayOf()),
+ intArrayOf(textColorPrimary, colorAccent)))
+ }
+
+ /**
+ * Updates this view to display the latest state of the `timer`.
+ */
+ fun update(timer: Timer) {
+ // Update the time.
+ mTimerTextController.setTimeString(timer.remainingTime)
+
+ // Update the label if it changed.
+ val label: String? = timer.label
+ if (!TextUtils.equals(label, mLabelView.text)) {
+ mLabelView.text = label
+ }
+
+ // Update visibility of things that may blink.
+ val blinkOff = SystemClock.elapsedRealtime() % 1000 < 500
+ if (mCircleView != null) {
+ val hideCircle = (timer.isExpired || timer.isMissed) && blinkOff
+ mCircleView!!.visibility = if (hideCircle) View.INVISIBLE else View.VISIBLE
+
+ if (!hideCircle) {
+ // Update the progress of the circle.
+ mCircleView!!.update(timer)
+ }
+ }
+ if (!timer.isPaused || !blinkOff || mTimerText.isPressed) {
+ mTimerText.alpha = 1f
+ } else {
+ mTimerText.alpha = 0f
+ }
+
+ // Update some potentially expensive areas of the user interface only on state changes.
+ if (timer.state != mLastState) {
+ mLastState = timer.state
+ val context = context
+ when (mLastState) {
+ Timer.State.RESET, Timer.State.PAUSED -> {
+ mResetAddButton.setText(R.string.timer_reset)
+ mResetAddButton.contentDescription = null
+ mTimerText.isClickable = true
+ mTimerText.isActivated = false
+ mTimerText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ ViewCompat.setAccessibilityDelegate(mTimerText, ClickAccessibilityDelegate(
+ context.getString(R.string.timer_start), true))
+ }
+ Timer.State.RUNNING -> {
+ val addTimeDesc = context.getString(R.string.timer_plus_one)
+ mResetAddButton.setText(R.string.timer_add_minute)
+ mResetAddButton.contentDescription = addTimeDesc
+ mTimerText.isClickable = true
+ mTimerText.isActivated = false
+ mTimerText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ ViewCompat.setAccessibilityDelegate(mTimerText, ClickAccessibilityDelegate(
+ context.getString(R.string.timer_pause)))
+ }
+ Timer.State.EXPIRED, Timer.State.MISSED -> {
+ val addTimeDesc = context.getString(R.string.timer_plus_one)
+ mResetAddButton.setText(R.string.timer_add_minute)
+ mResetAddButton.contentDescription = addTimeDesc
+ mTimerText.isClickable = false
+ mTimerText.isActivated = true
+ mTimerText.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerItemFragment.java b/src/com/android/deskclock/timer/TimerItemFragment.java
deleted file mode 100644
index 7ce6876..0000000
--- a/src/com/android/deskclock/timer/TimerItemFragment.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.app.Fragment;
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.LabelDialogFragment;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.data.TimerStringFormatter;
-import com.android.deskclock.events.Events;
-
-public class TimerItemFragment extends Fragment {
-
- private static final String KEY_TIMER_ID = "KEY_TIMER_ID";
- private int mTimerId;
-
- /** The public no-arg constructor required by all fragments. */
- public TimerItemFragment() {}
-
- public static TimerItemFragment newInstance(Timer timer) {
- final TimerItemFragment fragment = new TimerItemFragment();
- final Bundle args = new Bundle();
- args.putInt(KEY_TIMER_ID, timer.getId());
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mTimerId = getArguments().getInt(KEY_TIMER_ID);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- final Timer timer = getTimer();
- if (timer == null) {
- return null;
- }
-
- final TimerItem view = (TimerItem) inflater.inflate(R.layout.timer_item, container, false);
- view.findViewById(R.id.reset_add).setOnClickListener(new ResetAddListener());
- view.findViewById(R.id.timer_label).setOnClickListener(new EditLabelListener());
- view.findViewById(R.id.timer_time_text).setOnClickListener(new TimeTextListener());
- view.update(timer);
-
- return view;
- }
-
- /**
- * @return {@code true} iff the timer is in a state that requires continuous updates
- */
- boolean updateTime() {
- final TimerItem view = (TimerItem) getView();
- if (view != null) {
- final Timer timer = getTimer();
- view.update(timer);
- return !timer.isReset();
- }
-
- return false;
- }
-
- int getTimerId() {
- return mTimerId;
- }
-
- Timer getTimer() {
- return DataModel.getDataModel().getTimer(getTimerId());
- }
-
- private final class ResetAddListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- final Timer timer = getTimer();
- if (timer.isPaused()) {
- DataModel.getDataModel().resetOrDeleteTimer(timer, R.string.label_deskclock);
- } else if (timer.isRunning() || timer.isExpired() || timer.isMissed()) {
- DataModel.getDataModel().addTimerMinute(timer);
- Events.sendTimerEvent(R.string.action_add_minute, R.string.label_deskclock);
-
- final Context context = v.getContext();
- // Must use getTimer() because old timer is no longer accurate.
- final long currentTime = getTimer().getRemainingTime();
- if (currentTime > 0) {
- v.announceForAccessibility(TimerStringFormatter.formatString(
- context, R.string.timer_accessibility_one_minute_added, currentTime,
- true));
- }
- }
- }
- }
-
- private final class EditLabelListener implements View.OnClickListener {
- @Override
- public void onClick(View v) {
- final LabelDialogFragment fragment = LabelDialogFragment.newInstance(getTimer());
- LabelDialogFragment.show(getFragmentManager(), fragment);
- }
- }
-
- private final class TimeTextListener implements View.OnClickListener {
- @Override
- public void onClick(View view) {
- final Timer clickedTimer = getTimer();
- if (clickedTimer.isPaused() || clickedTimer.isReset()) {
- DataModel.getDataModel().startTimer(clickedTimer);
- } else if (clickedTimer.isRunning()) {
- DataModel.getDataModel().pauseTimer(clickedTimer);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerItemFragment.kt b/src/com/android/deskclock/timer/TimerItemFragment.kt
new file mode 100644
index 0000000..52cfd60
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerItemFragment.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+
+import com.android.deskclock.LabelDialogFragment
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.data.TimerStringFormatter
+import com.android.deskclock.events.Events
+
+/** The public no-arg constructor required by all fragments. */
+class TimerItemFragment : Fragment() {
+ var timerId = 0
+ private set
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ timerId = requireArguments().getInt(KEY_TIMER_ID)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val timer = timer ?: return null
+
+ val view = inflater.inflate(R.layout.timer_item, container, false) as TimerItem
+ view.findViewById<View>(R.id.reset_add).setOnClickListener(ResetAddListener())
+ view.findViewById<View>(R.id.timer_label).setOnClickListener(EditLabelListener())
+ view.findViewById<View>(R.id.timer_time_text).setOnClickListener(TimeTextListener())
+ view.update(timer)
+
+ return view
+ }
+
+ /**
+ * @return `true` iff the timer is in a state that requires continuous updates
+ */
+ fun updateTime(): Boolean {
+ val view = view as TimerItem?
+ if (view != null) {
+ val timer = timer!!
+ view.update(timer)
+ return !timer.isReset
+ }
+
+ return false
+ }
+
+ val timer: Timer?
+ get() = DataModel.dataModel.getTimer(timerId)
+
+ private inner class ResetAddListener : View.OnClickListener {
+ override fun onClick(v: View) {
+ val timer = timer!!
+ if (timer.isPaused) {
+ DataModel.dataModel.resetOrDeleteTimer(timer, R.string.label_deskclock)
+ } else if (timer.isRunning || timer.isExpired || timer.isMissed) {
+ DataModel.dataModel.addTimerMinute(timer)
+ Events.sendTimerEvent(R.string.action_add_minute, R.string.label_deskclock)
+
+ val context = v.context
+ // Must re-retrieve timer because old timer is no longer accurate.
+ val currentTime: Long = this@TimerItemFragment.timer!!.remainingTime
+ if (currentTime > 0) {
+ v.announceForAccessibility(TimerStringFormatter.formatString(
+ context, R.string.timer_accessibility_one_minute_added, currentTime,
+ true))
+ }
+ }
+ }
+ }
+
+ private inner class EditLabelListener : View.OnClickListener {
+ override fun onClick(v: View) {
+ val fragment = LabelDialogFragment.newInstance(timer!!)
+ LabelDialogFragment.show(parentFragmentManager, fragment)
+ }
+ }
+
+ private inner class TimeTextListener : View.OnClickListener {
+ override fun onClick(view: View) {
+ val clickedTimer = timer!!
+ if (clickedTimer.isPaused || clickedTimer.isReset) {
+ DataModel.dataModel.startTimer(clickedTimer)
+ } else if (clickedTimer.isRunning) {
+ DataModel.dataModel.pauseTimer(clickedTimer)
+ }
+ }
+ }
+
+ companion object {
+ private const val KEY_TIMER_ID = "KEY_TIMER_ID"
+
+ fun newInstance(timer: Timer): TimerItemFragment {
+ val fragment = TimerItemFragment()
+ val args = Bundle()
+ args.putInt(KEY_TIMER_ID, timer.id)
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerKlaxon.java b/src/com/android/deskclock/timer/TimerKlaxon.java
deleted file mode 100644
index 1c40b4a..0000000
--- a/src/com/android/deskclock/timer/TimerKlaxon.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.media.AudioAttributes;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Vibrator;
-
-import com.android.deskclock.AsyncRingtonePlayer;
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-/**
- * Manages playing the timer ringtone and vibrating the device.
- */
-public abstract class TimerKlaxon {
-
- private static final long[] VIBRATE_PATTERN = {500, 500};
-
- private static boolean sStarted = false;
- private static AsyncRingtonePlayer sAsyncRingtonePlayer;
-
- private TimerKlaxon() {
- }
-
- public static void stop(Context context) {
- if (sStarted) {
- LogUtils.i("TimerKlaxon.stop()");
- sStarted = false;
- getAsyncRingtonePlayer(context).stop();
- ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE)).cancel();
- }
- }
-
- public static void start(Context context) {
- // Make sure we are stopped before starting
- stop(context);
- LogUtils.i("TimerKlaxon.start()");
-
- // Look up user-selected timer ringtone.
- if (DataModel.getDataModel().isTimerRingtoneSilent()) {
- // Special case: Silent ringtone
- LogUtils.i("Playing silent ringtone for timer");
- } else {
- final Uri uri = DataModel.getDataModel().getTimerRingtoneUri();
- final long crescendoDuration = DataModel.getDataModel().getTimerCrescendoDuration();
- getAsyncRingtonePlayer(context).play(uri, crescendoDuration);
- }
-
- if (DataModel.getDataModel().getTimerVibrate()) {
- final Vibrator vibrator = getVibrator(context);
- if (Utils.isLOrLater()) {
- vibrateLOrLater(vibrator);
- } else {
- vibrator.vibrate(VIBRATE_PATTERN, 0);
- }
- }
- sStarted = true;
- }
-
- @TargetApi(Build.VERSION_CODES.LOLLIPOP)
- private static void vibrateLOrLater(Vibrator vibrator) {
- vibrator.vibrate(VIBRATE_PATTERN, 0, new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_ALARM)
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
- .build());
- }
-
- private static Vibrator getVibrator(Context context) {
- return ((Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE));
- }
-
- private static synchronized AsyncRingtonePlayer getAsyncRingtonePlayer(Context context) {
- if (sAsyncRingtonePlayer == null) {
- sAsyncRingtonePlayer = new AsyncRingtonePlayer(context.getApplicationContext());
- }
-
- return sAsyncRingtonePlayer;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerKlaxon.kt b/src/com/android/deskclock/timer/TimerKlaxon.kt
new file mode 100644
index 0000000..9d1fde5
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerKlaxon.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.media.AudioAttributes
+import android.net.Uri
+import android.os.Build
+import android.os.Vibrator
+
+import com.android.deskclock.AsyncRingtonePlayer
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+/**
+ * Manages playing the timer ringtone and vibrating the device.
+ */
+object TimerKlaxon {
+ private val VIBRATE_PATTERN = longArrayOf(500, 500)
+
+ private var sStarted = false
+ private var sAsyncRingtonePlayer: AsyncRingtonePlayer? = null
+
+ @JvmStatic
+ fun stop(context: Context) {
+ if (sStarted) {
+ LogUtils.i("TimerKlaxon.stop()")
+ sStarted = false
+ getAsyncRingtonePlayer(context)!!.stop()
+ (context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).cancel()
+ }
+ }
+
+ @JvmStatic
+ fun start(context: Context) {
+ // Make sure we are stopped before starting
+ stop(context)
+ LogUtils.i("TimerKlaxon.start()")
+
+ // Look up user-selected timer ringtone.
+ if (DataModel.dataModel.isTimerRingtoneSilent) {
+ // Special case: Silent ringtone
+ LogUtils.i("Playing silent ringtone for timer")
+ } else {
+ val uri: Uri = DataModel.dataModel.timerRingtoneUri
+ val crescendoDuration: Long = DataModel.dataModel.timerCrescendoDuration
+ getAsyncRingtonePlayer(context)!!.play(uri, crescendoDuration)
+ }
+
+ if (DataModel.dataModel.timerVibrate) {
+ val vibrator = getVibrator(context)
+ if (Utils.isLOrLater) {
+ vibrateLOrLater(vibrator)
+ } else {
+ vibrator.vibrate(VIBRATE_PATTERN, 0)
+ }
+ }
+ sStarted = true
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private fun vibrateLOrLater(vibrator: Vibrator) {
+ vibrator.vibrate(VIBRATE_PATTERN, 0, AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build())
+ }
+
+ private fun getVibrator(context: Context): Vibrator {
+ return context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ }
+
+ @Synchronized
+ private fun getAsyncRingtonePlayer(context: Context): AsyncRingtonePlayer? {
+ if (sAsyncRingtonePlayer == null) {
+ sAsyncRingtonePlayer = AsyncRingtonePlayer(context.applicationContext)
+ }
+
+ return sAsyncRingtonePlayer
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerPagerAdapter.java b/src/com/android/deskclock/timer/TimerPagerAdapter.java
deleted file mode 100644
index 5255f16..0000000
--- a/src/com/android/deskclock/timer/TimerPagerAdapter.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.annotation.SuppressLint;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import androidx.legacy.app.FragmentCompat;
-import androidx.viewpager.widget.PagerAdapter;
-import android.util.ArrayMap;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.data.TimerListener;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * This adapter produces a {@link TimerItemFragment} for each timer.
- */
-class TimerPagerAdapter extends PagerAdapter implements TimerListener {
-
- private final FragmentManager mFragmentManager;
-
- /** Maps each timer id to the corresponding {@link TimerItemFragment} that draws it. */
- private final Map<Integer, TimerItemFragment> mFragments = new ArrayMap<>();
-
- /** The current fragment transaction in play or {@code null}. */
- private FragmentTransaction mCurrentTransaction;
-
- /** The {@link TimerItemFragment} that is current visible on screen. */
- private Fragment mCurrentPrimaryItem;
-
- public TimerPagerAdapter(FragmentManager fragmentManager) {
- mFragmentManager = fragmentManager;
- }
-
- @Override
- public int getCount() {
- return getTimers().size();
- }
-
- @Override
- public boolean isViewFromObject(View view, Object object) {
- return ((Fragment) object).getView() == view;
- }
-
- @Override
- public int getItemPosition(Object object) {
- final TimerItemFragment fragment = (TimerItemFragment) object;
- final Timer timer = fragment.getTimer();
-
- final int position = getTimers().indexOf(timer);
- return position == -1 ? POSITION_NONE : position;
- }
-
- @Override
- @SuppressLint("CommitTransaction")
- public Fragment instantiateItem(ViewGroup container, int position) {
- if (mCurrentTransaction == null) {
- mCurrentTransaction = mFragmentManager.beginTransaction();
- }
-
- final Timer timer = getTimers().get(position);
-
- // Search for the existing fragment by tag.
- final String tag = getClass().getSimpleName() + timer.getId();
- TimerItemFragment fragment = (TimerItemFragment) mFragmentManager.findFragmentByTag(tag);
-
- if (fragment != null) {
- // Reattach the existing fragment.
- mCurrentTransaction.attach(fragment);
- } else {
- // Create and add a new fragment.
- fragment = TimerItemFragment.newInstance(timer);
- mCurrentTransaction.add(container.getId(), fragment, tag);
- }
-
- if (fragment != mCurrentPrimaryItem) {
- setItemVisible(fragment, false);
- }
-
- mFragments.put(timer.getId(), fragment);
-
- return fragment;
- }
-
- @Override
- @SuppressLint("CommitTransaction")
- public void destroyItem(ViewGroup container, int position, Object object) {
- final TimerItemFragment fragment = (TimerItemFragment) object;
-
- if (mCurrentTransaction == null) {
- mCurrentTransaction = mFragmentManager.beginTransaction();
- }
-
- mFragments.remove(fragment.getTimerId());
- mCurrentTransaction.remove(fragment);
- }
-
- @Override
- public void setPrimaryItem(ViewGroup container, int position, Object object) {
- final Fragment fragment = (Fragment) object;
- if (fragment != mCurrentPrimaryItem) {
- if (mCurrentPrimaryItem != null) {
- setItemVisible(mCurrentPrimaryItem, false);
- }
-
- mCurrentPrimaryItem = fragment;
-
- if (mCurrentPrimaryItem != null) {
- setItemVisible(mCurrentPrimaryItem, true);
- }
- }
- }
-
- @Override
- public void finishUpdate(ViewGroup container) {
- if (mCurrentTransaction != null) {
- mCurrentTransaction.commitAllowingStateLoss();
- mCurrentTransaction = null;
- mFragmentManager.executePendingTransactions();
- }
- }
-
- @Override
- public void timerAdded(Timer timer) {
- notifyDataSetChanged();
- }
-
- @Override
- public void timerRemoved(Timer timer) {
- notifyDataSetChanged();
- }
-
- @Override
- public void timerUpdated(Timer before, Timer after) {
- final TimerItemFragment timerItemFragment = mFragments.get(after.getId());
- if (timerItemFragment != null) {
- timerItemFragment.updateTime();
- }
- }
-
- /**
- * @return {@code true} if at least one timer is in a state requiring continuous updates
- */
- boolean updateTime() {
- boolean continuousUpdates = false;
- for (TimerItemFragment fragment : mFragments.values()) {
- continuousUpdates |= fragment.updateTime();
- }
- return continuousUpdates;
- }
-
- Timer getTimer(int index) {
- return getTimers().get(index);
- }
-
- private List<Timer> getTimers() {
- return DataModel.getDataModel().getTimers();
- }
-
- private static void setItemVisible(Fragment item, boolean visible) {
- FragmentCompat.setMenuVisibility(item, visible);
- FragmentCompat.setUserVisibleHint(item, visible);
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerPagerAdapter.kt b/src/com/android/deskclock/timer/TimerPagerAdapter.kt
new file mode 100644
index 0000000..fa49933
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerPagerAdapter.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.annotation.SuppressLint
+import android.util.ArrayMap
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import androidx.viewpager.widget.PagerAdapter
+
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.data.TimerListener
+
+/**
+ * This adapter produces a [TimerItemFragment] for each timer.
+ */
+internal class TimerPagerAdapter(
+ private val mFragmentManager: FragmentManager
+) : PagerAdapter(), TimerListener {
+
+ /** Maps each timer id to the corresponding [TimerItemFragment] that draws it. */
+ private val mFragments: MutableMap<Int, TimerItemFragment?> = ArrayMap()
+
+ /** The current fragment transaction in play or `null`. */
+ private var mCurrentTransaction: FragmentTransaction? = null
+
+ /** The [TimerItemFragment] that is current visible on screen. */
+ private var mCurrentPrimaryItem: Fragment? = null
+
+ override fun getCount(): Int = timers.size
+
+ override fun isViewFromObject(view: View, any: Any): Boolean {
+ return (any as Fragment).view === view
+ }
+
+ override fun getItemPosition(any: Any): Int {
+ val fragment = any as TimerItemFragment
+ val timer = fragment.timer
+
+ val position = timers.indexOf(timer)
+ return if (position == -1) POSITION_NONE else position
+ }
+
+ @SuppressLint("CommitTransaction")
+ override fun instantiateItem(container: ViewGroup, position: Int): Fragment {
+ if (mCurrentTransaction == null) {
+ mCurrentTransaction = mFragmentManager.beginTransaction()
+ }
+
+ val timer = timers[position]
+
+ // Search for the existing fragment by tag.
+ val tag = javaClass.simpleName + timer.id
+ var fragment = mFragmentManager.findFragmentByTag(tag) as TimerItemFragment?
+
+ if (fragment != null) {
+ // Reattach the existing fragment.
+ mCurrentTransaction!!.attach(fragment)
+ } else {
+ // Create and add a new fragment.
+ fragment = TimerItemFragment.newInstance(timer)
+ mCurrentTransaction!!.add(container.id, fragment, tag)
+ }
+
+ if (fragment !== mCurrentPrimaryItem) {
+ setItemVisible(fragment, false)
+ }
+
+ mFragments[timer.id] = fragment
+
+ return fragment
+ }
+
+ @SuppressLint("CommitTransaction")
+ override fun destroyItem(container: ViewGroup, position: Int, any: Any) {
+ val fragment = any as TimerItemFragment
+
+ if (mCurrentTransaction == null) {
+ mCurrentTransaction = mFragmentManager.beginTransaction()
+ }
+
+ mFragments.remove(fragment.timerId)
+ mCurrentTransaction!!.remove(fragment)
+ }
+
+ override fun setPrimaryItem(container: ViewGroup, position: Int, any: Any) {
+ val fragment = any as Fragment
+ if (fragment !== mCurrentPrimaryItem) {
+ mCurrentPrimaryItem?.let {
+ setItemVisible(it, false)
+ }
+
+ mCurrentPrimaryItem = fragment
+
+ mCurrentPrimaryItem?.let {
+ setItemVisible(it, true)
+ }
+ }
+ }
+
+ override fun finishUpdate(container: ViewGroup) {
+ if (mCurrentTransaction != null) {
+ mCurrentTransaction!!.commitAllowingStateLoss()
+ mCurrentTransaction = null
+
+ if (!mFragmentManager.isDestroyed) {
+ mFragmentManager.executePendingTransactions()
+ }
+ }
+ }
+
+ override fun timerAdded(timer: Timer) {
+ notifyDataSetChanged()
+ }
+
+ override fun timerRemoved(timer: Timer) {
+ notifyDataSetChanged()
+ }
+
+ override fun timerUpdated(before: Timer, after: Timer) {
+ val timerItemFragment = mFragments[after.id]
+ timerItemFragment?.updateTime()
+ }
+
+ /**
+ * @return `true` if at least one timer is in a state requiring continuous updates
+ */
+ fun updateTime(): Boolean {
+ var continuousUpdates = false
+ for (fragment in mFragments.values) {
+ continuousUpdates = continuousUpdates or fragment!!.updateTime()
+ }
+ return continuousUpdates
+ }
+
+ fun getTimer(index: Int): Timer {
+ return timers[index]
+ }
+
+ private val timers: List<Timer>
+ get() = DataModel.dataModel.timers
+
+ companion object {
+ private fun setItemVisible(item: Fragment, visible: Boolean) {
+ item.setMenuVisibility(visible)
+ item.setUserVisibleHint(visible)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerReceiver.java b/src/com/android/deskclock/timer/TimerReceiver.java
deleted file mode 100644
index 7a41789..0000000
--- a/src/com/android/deskclock/timer/TimerReceiver.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-import com.android.deskclock.LogUtils;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-
-/**
- * This broadcast receiver exists to handle timer expiry scheduled in 4.2.1 and prior. It must exist
- * for at least one release cycle before removal to honor these old scheduled timers after upgrading
- * beyond 4.2.1. After 4.2.1, all timer expiration is directed to TimerService.
- */
-public class TimerReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- LogUtils.e("TimerReceiver", "Received legacy timer broadcast: %s", intent.getAction());
-
- if ("times_up".equals(intent.getAction())) {
- final int timerId = intent.getIntExtra("timer.intent.extra", -1);
- final Timer timer = DataModel.getDataModel().getTimer(timerId);
- context.startService(TimerService.createTimerExpiredIntent(context, timer));
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerReceiver.kt b/src/com/android/deskclock/timer/TimerReceiver.kt
new file mode 100644
index 0000000..aa74177
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerReceiver.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+
+/**
+ * This broadcast receiver exists to handle timer expiry scheduled in 4.2.1 and prior. It must exist
+ * for at least one release cycle before removal to honor these old scheduled timers after upgrading
+ * beyond 4.2.1. After 4.2.1, all timer expiration is directed to TimerService.
+ */
+class TimerReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ LogUtils.e("TimerReceiver", "Received legacy timer broadcast: %s", intent.action)
+
+ if ("times_up" == intent.action) {
+ val timerId = intent.getIntExtra("timer.intent.extra", -1)
+ val timer: Timer? = DataModel.dataModel.getTimer(timerId)
+ context.startService(TimerService.createTimerExpiredIntent(context, timer))
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerService.java b/src/com/android/deskclock/timer/TimerService.java
deleted file mode 100644
index a82652e..0000000
--- a/src/com/android/deskclock/timer/TimerService.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.os.IBinder;
-
-import com.android.deskclock.DeskClock;
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Timer;
-import com.android.deskclock.events.Events;
-import com.android.deskclock.uidata.UiDataModel;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
-
-/**
- * <p>This service exists solely to allow {@link android.app.AlarmManager} and timer notifications
- * to alter the state of timers without disturbing the notification shade. If an activity were used
- * instead (even one that is not displayed) the notification manager implicitly closes the
- * notification shade which clashes with the use case of starting/pausing/resetting timers without
- * disturbing the notification shade.</p>
- *
- * <p>The service has a second benefit. It is used to start heads-up notifications for expired
- * timers in the foreground. This keeps the entire application in the foreground and thus prevents
- * the operating system from killing it while expired timers are firing.</p>
- */
-public final class TimerService extends Service {
-
- private static final String ACTION_PREFIX = "com.android.deskclock.action.";
-
- /** Shows the tab with timers; scrolls to a specific timer. */
- public static final String ACTION_SHOW_TIMER = ACTION_PREFIX + "SHOW_TIMER";
- /** Pauses running timers; resets expired timers. */
- public static final String ACTION_PAUSE_TIMER = ACTION_PREFIX + "PAUSE_TIMER";
- /** Starts the sole timer. */
- public static final String ACTION_START_TIMER = ACTION_PREFIX + "START_TIMER";
- /** Resets the timer. */
- public static final String ACTION_RESET_TIMER = ACTION_PREFIX + "RESET_TIMER";
- /** Adds an extra minute to the timer. */
- public static final String ACTION_ADD_MINUTE_TIMER = ACTION_PREFIX + "ADD_MINUTE_TIMER";
-
- /** Extra for many actions specific to a given timer. */
- public static final String EXTRA_TIMER_ID = "com.android.deskclock.extra.TIMER_ID";
-
- private static final String ACTION_TIMER_EXPIRED =
- ACTION_PREFIX + "TIMER_EXPIRED";
- private static final String ACTION_UPDATE_NOTIFICATION =
- ACTION_PREFIX + "UPDATE_NOTIFICATION";
- private static final String ACTION_RESET_EXPIRED_TIMERS =
- ACTION_PREFIX + "RESET_EXPIRED_TIMERS";
- private static final String ACTION_RESET_UNEXPIRED_TIMERS =
- ACTION_PREFIX + "RESET_UNEXPIRED_TIMERS";
- private static final String ACTION_RESET_MISSED_TIMERS =
- ACTION_PREFIX + "RESET_MISSED_TIMERS";
-
- public static Intent createTimerExpiredIntent(Context context, Timer timer) {
- final int timerId = timer == null ? -1 : timer.getId();
- return new Intent(context, TimerService.class)
- .setAction(ACTION_TIMER_EXPIRED)
- .putExtra(EXTRA_TIMER_ID, timerId);
- }
-
- public static Intent createResetExpiredTimersIntent(Context context) {
- return new Intent(context, TimerService.class)
- .setAction(ACTION_RESET_EXPIRED_TIMERS);
- }
-
- public static Intent createResetUnexpiredTimersIntent(Context context) {
- return new Intent(context, TimerService.class)
- .setAction(ACTION_RESET_UNEXPIRED_TIMERS);
- }
-
- public static Intent createResetMissedTimersIntent(Context context) {
- return new Intent(context, TimerService.class)
- .setAction(ACTION_RESET_MISSED_TIMERS);
- }
-
-
- public static Intent createAddMinuteTimerIntent(Context context, int timerId) {
- return new Intent(context, TimerService.class)
- .setAction(ACTION_ADD_MINUTE_TIMER)
- .putExtra(EXTRA_TIMER_ID, timerId);
- }
-
- public static Intent createUpdateNotificationIntent(Context context) {
- return new Intent(context, TimerService.class)
- .setAction(ACTION_UPDATE_NOTIFICATION);
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return null;
- }
-
- @Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- try {
- final String action = intent.getAction();
- final int label = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent);
- switch (action) {
- case ACTION_UPDATE_NOTIFICATION: {
- DataModel.getDataModel().updateTimerNotification();
- return START_NOT_STICKY;
- }
- case ACTION_RESET_EXPIRED_TIMERS: {
- DataModel.getDataModel().resetOrDeleteExpiredTimers(label);
- return START_NOT_STICKY;
- }
- case ACTION_RESET_UNEXPIRED_TIMERS: {
- DataModel.getDataModel().resetUnexpiredTimers(label);
- return START_NOT_STICKY;
- }
- case ACTION_RESET_MISSED_TIMERS: {
- DataModel.getDataModel().resetMissedTimers(label);
- return START_NOT_STICKY;
- }
- }
-
- // Look up the timer in question.
- final int timerId = intent.getIntExtra(EXTRA_TIMER_ID, -1);
- final Timer timer = DataModel.getDataModel().getTimer(timerId);
-
- // If the timer cannot be located, ignore the action.
- if (timer == null) {
- return START_NOT_STICKY;
- }
-
- // Perform the action on the timer.
- switch (action) {
- case ACTION_SHOW_TIMER: {
- Events.sendTimerEvent(R.string.action_show, label);
-
- // Change to the timers tab.
- UiDataModel.getUiDataModel().setSelectedTab(TIMERS);
-
- // Open DeskClock which is now positioned on the timers tab and show the timer
- // in question.
- final Intent showTimers = new Intent(this, DeskClock.class)
- .putExtra(EXTRA_TIMER_ID, timerId)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(showTimers);
- break;
- } case ACTION_START_TIMER: {
- Events.sendTimerEvent(R.string.action_start, label);
- DataModel.getDataModel().startTimer(this, timer);
- break;
- } case ACTION_PAUSE_TIMER: {
- Events.sendTimerEvent(R.string.action_pause, label);
- DataModel.getDataModel().pauseTimer(timer);
- break;
- } case ACTION_ADD_MINUTE_TIMER: {
- Events.sendTimerEvent(R.string.action_add_minute, label);
- DataModel.getDataModel().addTimerMinute(timer);
- break;
- } case ACTION_RESET_TIMER: {
- DataModel.getDataModel().resetOrDeleteTimer(timer, label);
- break;
- } case ACTION_TIMER_EXPIRED: {
- Events.sendTimerEvent(R.string.action_fire, label);
- DataModel.getDataModel().expireTimer(this, timer);
- break;
- }
- }
- } finally {
- // This service is foreground when expired timers exist and stopped when none exist.
- if (DataModel.getDataModel().getExpiredTimers().isEmpty()) {
- stopSelf();
- }
- }
-
- return START_NOT_STICKY;
- }
-}
diff --git a/src/com/android/deskclock/timer/TimerService.kt b/src/com/android/deskclock/timer/TimerService.kt
new file mode 100644
index 0000000..8e37a84
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerService.kt
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+
+import com.android.deskclock.DeskClock
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Timer
+import com.android.deskclock.events.Events
+import com.android.deskclock.uidata.UiDataModel
+
+/**
+ *
+ * This service exists solely to allow [android.app.AlarmManager] and timer notifications
+ * to alter the state of timers without disturbing the notification shade. If an activity were used
+ * instead (even one that is not displayed) the notification manager implicitly closes the
+ * notification shade which clashes with the use case of starting/pausing/resetting timers without
+ * disturbing the notification shade.
+ *
+ * The service has a second benefit. It is used to start heads-up notifications for expired
+ * timers in the foreground. This keeps the entire application in the foreground and thus prevents
+ * the operating system from killing it while expired timers are firing.
+ */
+class TimerService : Service() {
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ try {
+ val action = intent.action
+ val label = intent.getIntExtra(Events.EXTRA_EVENT_LABEL, R.string.label_intent)
+ when (action) {
+ ACTION_UPDATE_NOTIFICATION -> {
+ DataModel.dataModel.updateTimerNotification()
+ return START_NOT_STICKY
+ }
+ ACTION_RESET_EXPIRED_TIMERS -> {
+ DataModel.dataModel.resetOrDeleteExpiredTimers(label)
+ return START_NOT_STICKY
+ }
+ ACTION_RESET_UNEXPIRED_TIMERS -> {
+ DataModel.dataModel.resetUnexpiredTimers(label)
+ return START_NOT_STICKY
+ }
+ ACTION_RESET_MISSED_TIMERS -> {
+ DataModel.dataModel.resetMissedTimers(label)
+ return START_NOT_STICKY
+ }
+ }
+
+ // Look up the timer in question.
+ val timerId = intent.getIntExtra(EXTRA_TIMER_ID, -1)
+ // If the timer cannot be located, ignore the action.
+ val timer: Timer = DataModel.dataModel.getTimer(timerId) ?: return START_NOT_STICKY
+
+ when (action) {
+ ACTION_SHOW_TIMER -> {
+ Events.sendTimerEvent(R.string.action_show, label)
+
+ // Change to the timers tab.
+ UiDataModel.uiDataModel.selectedTab = UiDataModel.Tab.TIMERS
+
+ // Open DeskClock which is now positioned on the timers tab and show the timer
+ // in question.
+ val showTimers = Intent(this, DeskClock::class.java)
+ .putExtra(EXTRA_TIMER_ID, timerId)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(showTimers)
+ }
+ ACTION_START_TIMER -> {
+ Events.sendTimerEvent(R.string.action_start, label)
+ DataModel.dataModel.startTimer(this, timer)
+ }
+ ACTION_PAUSE_TIMER -> {
+ Events.sendTimerEvent(R.string.action_pause, label)
+ DataModel.dataModel.pauseTimer(timer)
+ }
+ ACTION_ADD_MINUTE_TIMER -> {
+ Events.sendTimerEvent(R.string.action_add_minute, label)
+ DataModel.dataModel.addTimerMinute(timer)
+ }
+ ACTION_RESET_TIMER -> {
+ DataModel.dataModel.resetOrDeleteTimer(timer, label)
+ }
+ ACTION_TIMER_EXPIRED -> {
+ Events.sendTimerEvent(R.string.action_fire, label)
+ DataModel.dataModel.expireTimer(this, timer)
+ }
+ }
+ } finally {
+ // This service is foreground when expired timers exist and stopped when none exist.
+ if (DataModel.dataModel.expiredTimers.isEmpty()) {
+ stopSelf()
+ }
+ }
+
+ return START_NOT_STICKY
+ }
+
+ companion object {
+ private const val ACTION_PREFIX = "com.android.deskclock.action."
+
+ /** Shows the tab with timers; scrolls to a specific timer. */
+ const val ACTION_SHOW_TIMER = ACTION_PREFIX + "SHOW_TIMER"
+ /** Pauses running timers; resets expired timers. */
+ const val ACTION_PAUSE_TIMER = ACTION_PREFIX + "PAUSE_TIMER"
+ /** Starts the sole timer. */
+ const val ACTION_START_TIMER = ACTION_PREFIX + "START_TIMER"
+ /** Resets the timer. */
+ const val ACTION_RESET_TIMER = ACTION_PREFIX + "RESET_TIMER"
+ /** Adds an extra minute to the timer. */
+ const val ACTION_ADD_MINUTE_TIMER = ACTION_PREFIX + "ADD_MINUTE_TIMER"
+ /** Extra for many actions specific to a given timer. */
+ const val EXTRA_TIMER_ID = "com.android.deskclock.extra.TIMER_ID"
+
+ private const val ACTION_TIMER_EXPIRED = ACTION_PREFIX + "TIMER_EXPIRED"
+ private const val ACTION_UPDATE_NOTIFICATION = ACTION_PREFIX + "UPDATE_NOTIFICATION"
+ private const val ACTION_RESET_EXPIRED_TIMERS = ACTION_PREFIX + "RESET_EXPIRED_TIMERS"
+ private const val ACTION_RESET_UNEXPIRED_TIMERS = ACTION_PREFIX + "RESET_UNEXPIRED_TIMERS"
+ private const val ACTION_RESET_MISSED_TIMERS = ACTION_PREFIX + "RESET_MISSED_TIMERS"
+
+ @JvmStatic
+ fun createTimerExpiredIntent(context: Context, timer: Timer?): Intent {
+ val timerId = timer?.id ?: -1
+ return Intent(context, TimerService::class.java)
+ .setAction(ACTION_TIMER_EXPIRED)
+ .putExtra(EXTRA_TIMER_ID, timerId)
+ }
+
+ fun createResetExpiredTimersIntent(context: Context): Intent {
+ return Intent(context, TimerService::class.java)
+ .setAction(ACTION_RESET_EXPIRED_TIMERS)
+ }
+
+ fun createResetUnexpiredTimersIntent(context: Context): Intent {
+ return Intent(context, TimerService::class.java)
+ .setAction(ACTION_RESET_UNEXPIRED_TIMERS)
+ }
+
+ fun createResetMissedTimersIntent(context: Context): Intent {
+ return Intent(context, TimerService::class.java)
+ .setAction(ACTION_RESET_MISSED_TIMERS)
+ }
+
+ fun createAddMinuteTimerIntent(context: Context, timerId: Int): Intent {
+ return Intent(context, TimerService::class.java)
+ .setAction(ACTION_ADD_MINUTE_TIMER)
+ .putExtra(EXTRA_TIMER_ID, timerId)
+ }
+
+ fun createUpdateNotificationIntent(context: Context): Intent {
+ return Intent(context, TimerService::class.java)
+ .setAction(ACTION_UPDATE_NOTIFICATION)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/timer/TimerSetupView.java b/src/com/android/deskclock/timer/TimerSetupView.java
deleted file mode 100644
index 557a7d6..0000000
--- a/src/com/android/deskclock/timer/TimerSetupView.java
+++ /dev/null
@@ -1,337 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.timer;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.PorterDuff;
-import androidx.annotation.IdRes;
-import androidx.core.view.ViewCompat;
-import android.text.BidiFormatter;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.text.style.RelativeSizeSpan;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.deskclock.FabContainer;
-import com.android.deskclock.FormattedTextUtils;
-import com.android.deskclock.R;
-import com.android.deskclock.ThemeUtils;
-import com.android.deskclock.uidata.UiDataModel;
-
-import java.io.Serializable;
-import java.util.Arrays;
-
-import static com.android.deskclock.FabContainer.FAB_REQUEST_FOCUS;
-import static com.android.deskclock.FabContainer.FAB_SHRINK_AND_EXPAND;
-
-public class TimerSetupView extends LinearLayout implements View.OnClickListener,
- View.OnLongClickListener {
-
- private final int[] mInput = { 0, 0, 0, 0, 0, 0 };
-
- private int mInputPointer = -1;
- private CharSequence mTimeTemplate;
-
- private TextView mTimeView;
- private View mDeleteView;
- private View mDividerView;
- private TextView[] mDigitViews;
-
- /** Updates to the fab are requested via this container. */
- private FabContainer mFabContainer;
-
- public TimerSetupView(Context context) {
- this(context, null /* attrs */);
- }
-
- public TimerSetupView(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final BidiFormatter bf = BidiFormatter.getInstance(false /* rtlContext */);
- final String hoursLabel = bf.unicodeWrap(context.getString(R.string.hours_label));
- final String minutesLabel = bf.unicodeWrap(context.getString(R.string.minutes_label));
- final String secondsLabel = bf.unicodeWrap(context.getString(R.string.seconds_label));
-
- // Create a formatted template for "00h 00m 00s".
- mTimeTemplate = TextUtils.expandTemplate("^1^4 ^2^5 ^3^6",
- bf.unicodeWrap("^1"),
- bf.unicodeWrap("^2"),
- bf.unicodeWrap("^3"),
- FormattedTextUtils.formatText(hoursLabel, new RelativeSizeSpan(0.5f)),
- FormattedTextUtils.formatText(minutesLabel, new RelativeSizeSpan(0.5f)),
- FormattedTextUtils.formatText(secondsLabel, new RelativeSizeSpan(0.5f)));
-
- LayoutInflater.from(context).inflate(R.layout.timer_setup_container, this);
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
-
- mTimeView = (TextView) findViewById(R.id.timer_setup_time);
- mDeleteView = findViewById(R.id.timer_setup_delete);
- mDividerView = findViewById(R.id.timer_setup_divider);
- mDigitViews = new TextView[] {
- (TextView) findViewById(R.id.timer_setup_digit_0),
- (TextView) findViewById(R.id.timer_setup_digit_1),
- (TextView) findViewById(R.id.timer_setup_digit_2),
- (TextView) findViewById(R.id.timer_setup_digit_3),
- (TextView) findViewById(R.id.timer_setup_digit_4),
- (TextView) findViewById(R.id.timer_setup_digit_5),
- (TextView) findViewById(R.id.timer_setup_digit_6),
- (TextView) findViewById(R.id.timer_setup_digit_7),
- (TextView) findViewById(R.id.timer_setup_digit_8),
- (TextView) findViewById(R.id.timer_setup_digit_9),
- };
-
- // Tint the divider to match the disabled control color by default and used the activated
- // control color when there is valid input.
- final Context dividerContext = mDividerView.getContext();
- final int colorControlActivated = ThemeUtils.resolveColor(dividerContext,
- R.attr.colorControlActivated);
- final int colorControlDisabled = ThemeUtils.resolveColor(dividerContext,
- R.attr.colorControlNormal, new int[] { ~android.R.attr.state_enabled });
- ViewCompat.setBackgroundTintList(mDividerView, new ColorStateList(
- new int[][] { { android.R.attr.state_activated }, {} },
- new int[] { colorControlActivated, colorControlDisabled }));
- ViewCompat.setBackgroundTintMode(mDividerView, PorterDuff.Mode.SRC);
-
- // Initialize the digit buttons.
- final UiDataModel uidm = UiDataModel.getUiDataModel();
- for (final TextView digitView : mDigitViews) {
- final int digit = getDigitForId(digitView.getId());
- digitView.setText(uidm.getFormattedNumber(digit, 1));
- digitView.setOnClickListener(this);
- }
-
- mDeleteView.setOnClickListener(this);
- mDeleteView.setOnLongClickListener(this);
-
- updateTime();
- updateDeleteAndDivider();
- }
-
- public void setFabContainer(FabContainer fabContainer) {
- mFabContainer = fabContainer;
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- View view = null;
- if (keyCode == KeyEvent.KEYCODE_DEL) {
- view = mDeleteView;
- } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
- view = mDigitViews[keyCode - KeyEvent.KEYCODE_0];
- }
-
- if (view != null) {
- final boolean result = view.performClick();
- if (result && hasValidInput()) {
- mFabContainer.updateFab(FAB_REQUEST_FOCUS);
- }
- return result;
- }
-
- return false;
- }
-
- @Override
- public void onClick(View view) {
- if (view == mDeleteView) {
- delete();
- } else {
- append(getDigitForId(view.getId()));
- }
- }
-
- @Override
- public boolean onLongClick(View view) {
- if (view == mDeleteView) {
- reset();
- updateFab();
- return true;
- }
- return false;
- }
-
- private int getDigitForId(@IdRes int id) {
- switch (id) {
- case R.id.timer_setup_digit_0:
- return 0;
- case R.id.timer_setup_digit_1:
- return 1;
- case R.id.timer_setup_digit_2:
- return 2;
- case R.id.timer_setup_digit_3:
- return 3;
- case R.id.timer_setup_digit_4:
- return 4;
- case R.id.timer_setup_digit_5:
- return 5;
- case R.id.timer_setup_digit_6:
- return 6;
- case R.id.timer_setup_digit_7:
- return 7;
- case R.id.timer_setup_digit_8:
- return 8;
- case R.id.timer_setup_digit_9:
- return 9;
- }
- throw new IllegalArgumentException("Invalid id: " + id);
- }
-
- private void updateTime() {
- final int seconds = mInput[1] * 10 + mInput[0];
- final int minutes = mInput[3] * 10 + mInput[2];
- final int hours = mInput[5] * 10 + mInput[4];
-
- final UiDataModel uidm = UiDataModel.getUiDataModel();
- mTimeView.setText(TextUtils.expandTemplate(mTimeTemplate,
- uidm.getFormattedNumber(hours, 2),
- uidm.getFormattedNumber(minutes, 2),
- uidm.getFormattedNumber(seconds, 2)));
-
- final Resources r = getResources();
- mTimeView.setContentDescription(r.getString(R.string.timer_setup_description,
- r.getQuantityString(R.plurals.hours, hours, hours),
- r.getQuantityString(R.plurals.minutes, minutes, minutes),
- r.getQuantityString(R.plurals.seconds, seconds, seconds)));
- }
-
- private void updateDeleteAndDivider() {
- final boolean enabled = hasValidInput();
- mDeleteView.setEnabled(enabled);
- mDividerView.setActivated(enabled);
- }
-
- private void updateFab() {
- mFabContainer.updateFab(FAB_SHRINK_AND_EXPAND);
- }
-
- private void append(int digit) {
- if (digit < 0 || digit > 9) {
- throw new IllegalArgumentException("Invalid digit: " + digit);
- }
-
- // Pressing "0" as the first digit does nothing.
- if (mInputPointer == -1 && digit == 0) {
- return;
- }
-
- // No space for more digits, so ignore input.
- if (mInputPointer == mInput.length - 1) {
- return;
- }
-
- // Append the new digit.
- System.arraycopy(mInput, 0, mInput, 1, mInputPointer + 1);
- mInput[0] = digit;
- mInputPointer++;
- updateTime();
-
- // Update TalkBack to read the number being deleted.
- mDeleteView.setContentDescription(getContext().getString(
- R.string.timer_descriptive_delete,
- UiDataModel.getUiDataModel().getFormattedNumber(digit)));
-
- // Update the fab, delete, and divider when we have valid input.
- if (mInputPointer == 0) {
- updateFab();
- updateDeleteAndDivider();
- }
- }
-
- private void delete() {
- // Nothing exists to delete so return.
- if (mInputPointer < 0) {
- return;
- }
-
- System.arraycopy(mInput, 1, mInput, 0, mInputPointer);
- mInput[mInputPointer] = 0;
- mInputPointer--;
- updateTime();
-
- // Update TalkBack to read the number being deleted or its original description.
- if (mInputPointer >= 0) {
- mDeleteView.setContentDescription(getContext().getString(
- R.string.timer_descriptive_delete,
- UiDataModel.getUiDataModel().getFormattedNumber(mInput[0])));
- } else {
- mDeleteView.setContentDescription(getContext().getString(R.string.timer_delete));
- }
-
- // Update the fab, delete, and divider when we no longer have valid input.
- if (mInputPointer == -1) {
- updateFab();
- updateDeleteAndDivider();
- }
- }
-
- public void reset() {
- if (mInputPointer != -1) {
- Arrays.fill(mInput, 0);
- mInputPointer = -1;
- updateTime();
- updateDeleteAndDivider();
- }
- }
-
- public boolean hasValidInput() {
- return mInputPointer != -1;
- }
-
- public long getTimeInMillis() {
- final int seconds = mInput[1] * 10 + mInput[0];
- final int minutes = mInput[3] * 10 + mInput[2];
- final int hours = mInput[5] * 10 + mInput[4];
- return seconds * DateUtils.SECOND_IN_MILLIS
- + minutes * DateUtils.MINUTE_IN_MILLIS
- + hours * DateUtils.HOUR_IN_MILLIS;
- }
-
- /**
- * @return an opaque representation of the state of timer setup
- */
- public Serializable getState() {
- return Arrays.copyOf(mInput, mInput.length);
- }
-
- /**
- * @param state an opaque state of this view previously produced by {@link #getState()}
- */
- public void setState(Serializable state) {
- final int[] input = (int[]) state;
- if (input != null && mInput.length == input.length) {
- for (int i = 0; i < mInput.length; i++) {
- mInput[i] = input[i];
- if (mInput[i] != 0) {
- mInputPointer = i;
- }
- }
- updateTime();
- updateDeleteAndDivider();
- }
- }
-}
diff --git a/src/com/android/deskclock/timer/TimerSetupView.kt b/src/com/android/deskclock/timer/TimerSetupView.kt
new file mode 100644
index 0000000..0d0f75f
--- /dev/null
+++ b/src/com/android/deskclock/timer/TimerSetupView.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.PorterDuff
+import android.text.BidiFormatter
+import android.text.TextUtils
+import android.text.format.DateUtils
+import android.text.style.RelativeSizeSpan
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnLongClickListener
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.core.view.ViewCompat
+
+import com.android.deskclock.FabContainer
+import com.android.deskclock.FormattedTextUtils
+import com.android.deskclock.R
+import com.android.deskclock.ThemeUtils
+import com.android.deskclock.uidata.UiDataModel
+
+import java.io.Serializable
+
+class TimerSetupView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : LinearLayout(context, attrs), View.OnClickListener, OnLongClickListener {
+ private val mInput = intArrayOf(0, 0, 0, 0, 0, 0)
+
+ private var mInputPointer = -1
+ private val mTimeTemplate: CharSequence
+
+ private lateinit var mTimeView: TextView
+ private lateinit var mDeleteView: View
+ private lateinit var mDividerView: View
+ private lateinit var mDigitViews: Array<TextView>
+
+ /** Updates to the fab are requested via this container. */
+ private lateinit var mFabContainer: FabContainer
+
+ init {
+ val bf = BidiFormatter.getInstance(false /* rtlContext */)
+ val hoursLabel = bf.unicodeWrap(context.getString(R.string.hours_label))
+ val minutesLabel = bf.unicodeWrap(context.getString(R.string.minutes_label))
+ val secondsLabel = bf.unicodeWrap(context.getString(R.string.seconds_label))
+
+ // Create a formatted template for "00h 00m 00s".
+ mTimeTemplate = TextUtils.expandTemplate("^1^4 ^2^5 ^3^6",
+ bf.unicodeWrap("^1"),
+ bf.unicodeWrap("^2"),
+ bf.unicodeWrap("^3"),
+ FormattedTextUtils.formatText(hoursLabel, RelativeSizeSpan(0.5f)),
+ FormattedTextUtils.formatText(minutesLabel, RelativeSizeSpan(0.5f)),
+ FormattedTextUtils.formatText(secondsLabel, RelativeSizeSpan(0.5f)))
+
+ LayoutInflater.from(context).inflate(R.layout.timer_setup_container, this)
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ mTimeView = findViewById<View>(R.id.timer_setup_time) as TextView
+ mDeleteView = findViewById(R.id.timer_setup_delete)
+ mDividerView = findViewById(R.id.timer_setup_divider)
+ mDigitViews = arrayOf(
+ findViewById<View>(R.id.timer_setup_digit_0) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_1) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_2) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_3) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_4) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_5) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_6) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_7) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_8) as TextView,
+ findViewById<View>(R.id.timer_setup_digit_9) as TextView)
+
+ // Tint the divider to match the disabled control color by default and used the activated
+ // control color when there is valid input.
+ val dividerContext = mDividerView.context
+ val colorControlActivated = ThemeUtils.resolveColor(dividerContext,
+ R.attr.colorControlActivated)
+ val colorControlDisabled = ThemeUtils.resolveColor(dividerContext,
+ R.attr.colorControlNormal, intArrayOf(android.R.attr.state_enabled.inv()))
+ ViewCompat.setBackgroundTintList(mDividerView,
+ ColorStateList(
+ arrayOf(intArrayOf(android.R.attr.state_activated), intArrayOf()),
+ intArrayOf(colorControlActivated, colorControlDisabled)))
+ ViewCompat.setBackgroundTintMode(mDividerView, PorterDuff.Mode.SRC)
+
+ // Initialize the digit buttons.
+ val uidm = UiDataModel.uiDataModel
+ for (digitView in mDigitViews) {
+ val digit = getDigitForId(digitView.id)
+ digitView.text = uidm.getFormattedNumber(digit, 1)
+ digitView.setOnClickListener(this)
+ }
+
+ mDeleteView.setOnClickListener(this)
+ mDeleteView.setOnLongClickListener(this)
+
+ updateTime()
+ updateDeleteAndDivider()
+ }
+
+ fun setFabContainer(fabContainer: FabContainer) {
+ mFabContainer = fabContainer
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ var view: View? = null
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ view = mDeleteView
+ } else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
+ view = mDigitViews[keyCode - KeyEvent.KEYCODE_0]
+ }
+
+ if (view != null) {
+ val result = view.performClick()
+ if (result && hasValidInput()) {
+ mFabContainer.updateFab(FabContainer.FAB_REQUEST_FOCUS)
+ }
+ return result
+ }
+
+ return false
+ }
+
+ override fun onClick(view: View) {
+ if (view === mDeleteView) {
+ delete()
+ } else {
+ append(getDigitForId(view.id))
+ }
+ }
+
+ override fun onLongClick(view: View): Boolean {
+ if (view === mDeleteView) {
+ reset()
+ updateFab()
+ return true
+ }
+ return false
+ }
+
+ private fun getDigitForId(@IdRes id: Int): Int = when (id) {
+ R.id.timer_setup_digit_0 -> 0
+ R.id.timer_setup_digit_1 -> 1
+ R.id.timer_setup_digit_2 -> 2
+ R.id.timer_setup_digit_3 -> 3
+ R.id.timer_setup_digit_4 -> 4
+ R.id.timer_setup_digit_5 -> 5
+ R.id.timer_setup_digit_6 -> 6
+ R.id.timer_setup_digit_7 -> 7
+ R.id.timer_setup_digit_8 -> 8
+ R.id.timer_setup_digit_9 -> 9
+ else -> throw IllegalArgumentException("Invalid id: $id")
+ }
+
+ private fun updateTime() {
+ val seconds = mInput[1] * 10 + mInput[0]
+ val minutes = mInput[3] * 10 + mInput[2]
+ val hours = mInput[5] * 10 + mInput[4]
+
+ val uidm = UiDataModel.uiDataModel
+ mTimeView.text = TextUtils.expandTemplate(mTimeTemplate,
+ uidm.getFormattedNumber(hours, 2),
+ uidm.getFormattedNumber(minutes, 2),
+ uidm.getFormattedNumber(seconds, 2))
+
+ val r = resources
+ mTimeView.contentDescription = r.getString(R.string.timer_setup_description,
+ r.getQuantityString(R.plurals.hours, hours, hours),
+ r.getQuantityString(R.plurals.minutes, minutes, minutes),
+ r.getQuantityString(R.plurals.seconds, seconds, seconds))
+ }
+
+ private fun updateDeleteAndDivider() {
+ val enabled = hasValidInput()
+ mDeleteView.isEnabled = enabled
+ mDividerView.isActivated = enabled
+ }
+
+ private fun updateFab() {
+ mFabContainer.updateFab(FabContainer.FAB_SHRINK_AND_EXPAND)
+ }
+
+ private fun append(digit: Int) {
+ require(!(digit < 0 || digit > 9)) { "Invalid digit: $digit" }
+
+ // Pressing "0" as the first digit does nothing.
+ if (mInputPointer == -1 && digit == 0) {
+ return
+ }
+
+ // No space for more digits, so ignore input.
+ if (mInputPointer == mInput.size - 1) {
+ return
+ }
+
+ // Append the new digit.
+ System.arraycopy(mInput, 0, mInput, 1, mInputPointer + 1)
+ mInput[0] = digit
+ mInputPointer++
+ updateTime()
+
+ // Update TalkBack to read the number being deleted.
+ mDeleteView.contentDescription = context.getString(
+ R.string.timer_descriptive_delete,
+ UiDataModel.uiDataModel.getFormattedNumber(digit))
+
+ // Update the fab, delete, and divider when we have valid input.
+ if (mInputPointer == 0) {
+ updateFab()
+ updateDeleteAndDivider()
+ }
+ }
+
+ private fun delete() {
+ // Nothing exists to delete so return.
+ if (mInputPointer < 0) {
+ return
+ }
+
+ System.arraycopy(mInput, 1, mInput, 0, mInputPointer)
+ mInput[mInputPointer] = 0
+ mInputPointer--
+ updateTime()
+
+ // Update TalkBack to read the number being deleted or its original description.
+ if (mInputPointer >= 0) {
+ mDeleteView.contentDescription = context.getString(
+ R.string.timer_descriptive_delete,
+ UiDataModel.uiDataModel.getFormattedNumber(mInput[0]))
+ } else {
+ mDeleteView.contentDescription = context.getString(R.string.timer_delete)
+ }
+
+ // Update the fab, delete, and divider when we no longer have valid input.
+ if (mInputPointer == -1) {
+ updateFab()
+ updateDeleteAndDivider()
+ }
+ }
+
+ fun reset() {
+ if (mInputPointer != -1) {
+ mInput.fill(0)
+ mInputPointer = -1
+ updateTime()
+ updateDeleteAndDivider()
+ }
+ }
+
+ fun hasValidInput(): Boolean {
+ return mInputPointer != -1
+ }
+
+ val timeInMillis: Long
+ get() {
+ val seconds = mInput[1] * 10 + mInput[0]
+ val minutes = mInput[3] * 10 + mInput[2]
+ val hours = mInput[5] * 10 + mInput[4]
+ return seconds * DateUtils.SECOND_IN_MILLIS +
+ minutes * DateUtils.MINUTE_IN_MILLIS +
+ hours * DateUtils.HOUR_IN_MILLIS
+ }
+
+ var state: Serializable?
+ /**
+ * @return an opaque representation of the state of timer setup
+ */
+ get() = mInput.copyOf(mInput.size)
+ /**
+ * @param state an opaque state of this view previously produced by [.getState]
+ */
+ set(state) {
+ val input = state as IntArray?
+ if (input != null && mInput.size == input.size) {
+ for (i in mInput.indices) {
+ mInput[i] = input[i]
+ if (mInput[i] != 0) {
+ mInputPointer = i
+ }
+ }
+ updateTime()
+ updateDeleteAndDivider()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/FormattedStringModel.java b/src/com/android/deskclock/uidata/FormattedStringModel.java
deleted file mode 100644
index e630d74..0000000
--- a/src/com/android/deskclock/uidata/FormattedStringModel.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.uidata;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.ArrayMap;
-import android.util.SparseArray;
-
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.Locale;
-import java.util.Map;
-
-import static java.util.Calendar.JULY;
-
-/**
- * All formatted strings that are cached for performance are accessed via this model.
- */
-final class FormattedStringModel {
-
- /** Clears data structures containing data that is locale-sensitive. */
- @SuppressWarnings("FieldCanBeLocal")
- private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
-
- /**
- * Caches formatted numbers in the current locale padded with zeroes to requested lengths.
- * The first level of the cache maps length to the second level of the cache.
- * The second level of the cache maps an integer to a formatted String in the current locale.
- */
- private final SparseArray<SparseArray<String>> mNumberFormatCache = new SparseArray<>(3);
-
- /** Single-character version of weekday names; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S' */
- private Map<Integer, String> mShortWeekdayNames;
-
- /** Full weekday names; e.g.: 'Sunday', 'Monday', 'Tuesday', etc. */
- private Map<Integer, String> mLongWeekdayNames;
-
- FormattedStringModel(Context context) {
- // Clear caches affected by locale when locale changes.
- final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
- context.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
- }
-
- /**
- * This method is intended to be used when formatting numbers occurs in a hotspot such as the
- * update loop of a timer or stopwatch. It returns cached results when possible in order to
- * provide speed and limit garbage to be collected by the virtual machine.
- *
- * @param value a positive integer to format as a String
- * @return the {@code value} formatted as a String in the current locale
- * @throws IllegalArgumentException if {@code value} is negative
- */
- String getFormattedNumber(int value) {
- final int length = value == 0 ? 1 : ((int) Math.log10(value) + 1);
- return getFormattedNumber(false, value, length);
- }
-
- /**
- * This method is intended to be used when formatting numbers occurs in a hotspot such as the
- * update loop of a timer or stopwatch. It returns cached results when possible in order to
- * provide speed and limit garbage to be collected by the virtual machine.
- *
- * @param value a positive integer to format as a String
- * @param length the length of the String; zeroes are padded to match this length
- * @return the {@code value} formatted as a String in the current locale and padded to the
- * requested {@code length}
- * @throws IllegalArgumentException if {@code value} is negative
- */
- String getFormattedNumber(int value, int length) {
- return getFormattedNumber(false, value, length);
- }
-
- /**
- * This method is intended to be used when formatting numbers occurs in a hotspot such as the
- * update loop of a timer or stopwatch. It returns cached results when possible in order to
- * provide speed and limit garbage to be collected by the virtual machine.
- *
- * @param negative force a minus sign (-) onto the display, even if {@code value} is {@code 0}
- * @param value a positive integer to format as a String
- * @param length the length of the String; zeroes are padded to match this length. If
- * {@code negative} is {@code true} the return value will contain a minus sign and a total
- * length of {@code length + 1}.
- * @return the {@code value} formatted as a String in the current locale and padded to the
- * requested {@code length}
- * @throws IllegalArgumentException if {@code value} is negative
- */
- String getFormattedNumber(boolean negative, int value, int length) {
- if (value < 0) {
- throw new IllegalArgumentException("value may not be negative: " + value);
- }
-
- // Look up the value cache using the length; -ve and +ve values are cached separately.
- final int lengthCacheKey = negative ? -length : length;
- SparseArray<String> valueCache = mNumberFormatCache.get(lengthCacheKey);
- if (valueCache == null) {
- valueCache = new SparseArray<>((int) Math.pow(10, length));
- mNumberFormatCache.put(lengthCacheKey, valueCache);
- }
-
- // Look up the cached formatted value using the value.
- String formatted = valueCache.get(value);
- if (formatted == null) {
- final String sign = negative ? "−" : "";
- formatted = String.format(Locale.getDefault(), sign + "%0" + length + "d", value);
- valueCache.put(value, formatted);
- }
-
- return formatted;
- }
-
- /**
- * @param calendarDay any of the following values
- * <ul>
- * <li>{@link Calendar#SUNDAY}</li>
- * <li>{@link Calendar#MONDAY}</li>
- * <li>{@link Calendar#TUESDAY}</li>
- * <li>{@link Calendar#WEDNESDAY}</li>
- * <li>{@link Calendar#THURSDAY}</li>
- * <li>{@link Calendar#FRIDAY}</li>
- * <li>{@link Calendar#SATURDAY}</li>
- * </ul>
- * @return single-character weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
- */
- String getShortWeekday(int calendarDay) {
- if (mShortWeekdayNames == null) {
- mShortWeekdayNames = new ArrayMap<>(7);
-
- final SimpleDateFormat format = new SimpleDateFormat("ccccc", Locale.getDefault());
- for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
- final Calendar calendar = new GregorianCalendar(2014, JULY, 20 + i - 1);
- final String weekday = format.format(calendar.getTime());
- mShortWeekdayNames.put(i, weekday);
- }
- }
-
- return mShortWeekdayNames.get(calendarDay);
- }
-
- /**
- * @param calendarDay any of the following values
- * <ul>
- * <li>{@link Calendar#SUNDAY}</li>
- * <li>{@link Calendar#MONDAY}</li>
- * <li>{@link Calendar#TUESDAY}</li>
- * <li>{@link Calendar#WEDNESDAY}</li>
- * <li>{@link Calendar#THURSDAY}</li>
- * <li>{@link Calendar#FRIDAY}</li>
- * <li>{@link Calendar#SATURDAY}</li>
- * </ul>
- * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
- */
- String getLongWeekday(int calendarDay) {
- if (mLongWeekdayNames == null) {
- mLongWeekdayNames = new ArrayMap<>(7);
-
- final Calendar calendar = new GregorianCalendar(2014, JULY, 20);
- final SimpleDateFormat format = new SimpleDateFormat("EEEE", Locale.getDefault());
- for (int i = Calendar.SUNDAY; i <= Calendar.SATURDAY; i++) {
- final String weekday = format.format(calendar.getTime());
- mLongWeekdayNames.put(i, weekday);
- calendar.add(Calendar.DAY_OF_YEAR, 1);
- }
- }
-
- return mLongWeekdayNames.get(calendarDay);
- }
-
- /**
- * Cached information that is locale-sensitive must be cleared in response to locale changes.
- */
- private final class LocaleChangedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- mNumberFormatCache.clear();
- mShortWeekdayNames = null;
- mLongWeekdayNames = null;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/FormattedStringModel.kt b/src/com/android/deskclock/uidata/FormattedStringModel.kt
new file mode 100644
index 0000000..f720299
--- /dev/null
+++ b/src/com/android/deskclock/uidata/FormattedStringModel.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.util.ArrayMap
+import android.util.SparseArray
+
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.GregorianCalendar
+import java.util.Locale
+
+/**
+ * All formatted strings that are cached for performance are accessed via this model.
+ */
+internal class FormattedStringModel(context: Context) {
+ /** Clears data structures containing data that is locale-sensitive. */
+ private val mLocaleChangedReceiver: BroadcastReceiver = LocaleChangedReceiver()
+
+ /**
+ * Caches formatted numbers in the current locale padded with zeroes to requested lengths.
+ * The first level of the cache maps length to the second level of the cache.
+ * The second level of the cache maps an integer to a formatted String in the current locale.
+ */
+ private val mNumberFormatCache = SparseArray<SparseArray<String>>(3)
+
+ /** Single-character version of weekday names; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S' */
+ private var mShortWeekdayNames: MutableMap<Int, String>? = null
+
+ /** Full weekday names; e.g.: 'Sunday', 'Monday', 'Tuesday', etc. */
+ private var mLongWeekdayNames: MutableMap<Int, String>? = null
+
+ init {
+ // Clear caches affected by locale when locale changes.
+ val localeBroadcastFilter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+ context.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @return the `value` formatted as a String in the current locale
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(value: Int): String {
+ val length = if (value == 0) 1 else Math.log10(value.toDouble()).toInt() + 1
+ return getFormattedNumber(false, value, length)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length
+ * @return the `value` formatted as a String in the current locale and padded to the
+ * requested `length`
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(value: Int, length: Int): String {
+ return getFormattedNumber(false, value, length)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param negative force a minus sign (-) onto the display, even if `value` is `0`
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length. If
+ * `negative` is `true` the return value will contain a minus sign and a total
+ * length of `length + 1`.
+ * @return the `value` formatted as a String in the current locale and padded to the
+ * requested `length`
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(negative: Boolean, value: Int, length: Int): String {
+ require(value >= 0) { "value may not be negative: $value" }
+
+ // Look up the value cache using the length; -ve and +ve values are cached separately.
+ val lengthCacheKey = if (negative) -length else length
+ var valueCache = mNumberFormatCache[lengthCacheKey]
+ if (valueCache == null) {
+ valueCache = SparseArray(Math.pow(10.0, length.toDouble()).toInt())
+ mNumberFormatCache.put(lengthCacheKey, valueCache)
+ }
+
+ // Look up the cached formatted value using the value.
+ var formatted = valueCache[value]
+ if (formatted == null) {
+ val sign = if (negative) "−" else ""
+ formatted = String.format(Locale.getDefault(), sign + "%0" + length + "d", value)
+ valueCache.put(value, formatted)
+ }
+
+ return formatted
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return single-character weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
+ */
+ fun getShortWeekday(calendarDay: Int): String? {
+ if (mShortWeekdayNames == null) {
+ mShortWeekdayNames = ArrayMap(7)
+
+ val format = SimpleDateFormat("ccccc", Locale.getDefault())
+ for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+ val calendar: Calendar = GregorianCalendar(2014, Calendar.JULY, 20 + i - 1)
+ val weekday = format.format(calendar.time)
+ mShortWeekdayNames!![i] = weekday
+ }
+ }
+
+ return mShortWeekdayNames!![calendarDay]
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
+ */
+ fun getLongWeekday(calendarDay: Int): String? {
+ if (mLongWeekdayNames == null) {
+ mLongWeekdayNames = ArrayMap(7)
+
+ val calendar: Calendar = GregorianCalendar(2014, Calendar.JULY, 20)
+ val format = SimpleDateFormat("EEEE", Locale.getDefault())
+ for (i in Calendar.SUNDAY..Calendar.SATURDAY) {
+ val weekday = format.format(calendar.time)
+ mLongWeekdayNames!![i] = weekday
+ calendar.add(Calendar.DAY_OF_YEAR, 1)
+ }
+ }
+
+ return mLongWeekdayNames!![calendarDay]
+ }
+
+ /**
+ * Cached information that is locale-sensitive must be cleared in response to locale changes.
+ */
+ private inner class LocaleChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ mNumberFormatCache.clear()
+ mShortWeekdayNames = null
+ mLongWeekdayNames = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/PeriodicCallbackModel.java b/src/com/android/deskclock/uidata/PeriodicCallbackModel.java
deleted file mode 100644
index 4b522d1..0000000
--- a/src/com/android/deskclock/uidata/PeriodicCallbackModel.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.uidata;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Handler;
-import androidx.annotation.VisibleForTesting;
-
-import com.android.deskclock.LogUtils;
-
-import java.util.Calendar;
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-
-import static android.content.Intent.ACTION_DATE_CHANGED;
-import static android.content.Intent.ACTION_TIMEZONE_CHANGED;
-import static android.content.Intent.ACTION_TIME_CHANGED;
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
-import static com.android.deskclock.Utils.enforceMainLooper;
-import static java.util.Calendar.DATE;
-import static java.util.Calendar.HOUR_OF_DAY;
-import static java.util.Calendar.MILLISECOND;
-import static java.util.Calendar.MINUTE;
-import static java.util.Calendar.SECOND;
-
-/**
- * All callbacks to be delivered at requested times on the main thread if the application is in the
- * foreground when the callback time passes.
- */
-final class PeriodicCallbackModel {
-
- private static final LogUtils.Logger LOGGER = new LogUtils.Logger("Periodic");
-
- @VisibleForTesting
- enum Period {MINUTE, QUARTER_HOUR, HOUR, MIDNIGHT}
-
- private static final long QUARTER_HOUR_IN_MILLIS = 15 * MINUTE_IN_MILLIS;
-
- private static Handler sHandler;
-
- /** Reschedules callbacks when the device time changes. */
- @SuppressWarnings("FieldCanBeLocal")
- private final BroadcastReceiver mTimeChangedReceiver = new TimeChangedReceiver();
-
- private final List<PeriodicRunnable> mPeriodicRunnables = new CopyOnWriteArrayList<>();
-
- PeriodicCallbackModel(Context context) {
- // Reschedules callbacks when the device time changes.
- final IntentFilter timeChangedBroadcastFilter = new IntentFilter();
- timeChangedBroadcastFilter.addAction(ACTION_TIME_CHANGED);
- timeChangedBroadcastFilter.addAction(ACTION_DATE_CHANGED);
- timeChangedBroadcastFilter.addAction(ACTION_TIMEZONE_CHANGED);
- context.registerReceiver(mTimeChangedReceiver, timeChangedBroadcastFilter);
- }
-
- /**
- * @param runnable to be called every minute
- * @param offset an offset applied to the minute to control when the callback occurs
- */
- void addMinuteCallback(Runnable runnable, long offset) {
- addPeriodicCallback(runnable, Period.MINUTE, offset);
- }
-
- /**
- * @param runnable to be called every quarter-hour
- * @param offset an offset applied to the quarter-hour to control when the callback occurs
- */
- void addQuarterHourCallback(Runnable runnable, long offset) {
- addPeriodicCallback(runnable, Period.QUARTER_HOUR, offset);
- }
-
- /**
- * @param runnable to be called every hour
- * @param offset an offset applied to the hour to control when the callback occurs
- */
- void addHourCallback(Runnable runnable, long offset) {
- addPeriodicCallback(runnable, Period.HOUR, offset);
- }
-
- /**
- * @param runnable to be called every midnight
- * @param offset an offset applied to the midnight to control when the callback occurs
- */
- void addMidnightCallback(Runnable runnable, long offset) {
- addPeriodicCallback(runnable, Period.MIDNIGHT, offset);
- }
-
- /**
- * @param runnable to be called periodically
- */
- private void addPeriodicCallback(Runnable runnable, Period period, long offset) {
- final PeriodicRunnable periodicRunnable = new PeriodicRunnable(runnable, period, offset);
- mPeriodicRunnables.add(periodicRunnable);
- periodicRunnable.schedule();
- }
-
- /**
- * @param runnable to no longer be called periodically
- */
- void removePeriodicCallback(Runnable runnable) {
- for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
- if (periodicRunnable.mDelegate == runnable) {
- periodicRunnable.unSchedule();
- mPeriodicRunnables.remove(periodicRunnable);
- return;
- }
- }
- }
-
- /**
- * Return the delay until the given {@code period} elapses adjusted by the given {@code offset}.
- *
- * @param now the current time
- * @param period the frequency with which callbacks should be given
- * @param offset an offset to add to the normal period; allows the callback to be made relative
- * to the normally scheduled period end
- * @return the time delay from {@code now} to schedule the callback
- */
- @VisibleForTesting
- static long getDelay(long now, Period period, long offset) {
- final long periodStart = now - offset;
-
- switch (period) {
- case MINUTE:
- final long lastMinute = periodStart - (periodStart % MINUTE_IN_MILLIS);
- final long nextMinute = lastMinute + MINUTE_IN_MILLIS;
- return nextMinute - now + offset;
-
- case QUARTER_HOUR:
- final long lastQuarterHour = periodStart - (periodStart % QUARTER_HOUR_IN_MILLIS);
- final long nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS;
- return nextQuarterHour - now + offset;
-
- case HOUR:
- final long lastHour = periodStart - (periodStart % HOUR_IN_MILLIS);
- final long nextHour = lastHour + HOUR_IN_MILLIS;
- return nextHour - now + offset;
-
- case MIDNIGHT:
- final Calendar nextMidnight = Calendar.getInstance();
- nextMidnight.setTimeInMillis(periodStart);
- nextMidnight.add(DATE, 1);
- nextMidnight.set(HOUR_OF_DAY, 0);
- nextMidnight.set(MINUTE, 0);
- nextMidnight.set(SECOND, 0);
- nextMidnight.set(MILLISECOND, 0);
- return nextMidnight.getTimeInMillis() - now + offset;
-
- default:
- throw new IllegalArgumentException("unexpected period: " + period);
- }
- }
-
- private static Handler getHandler() {
- enforceMainLooper();
- if (sHandler == null) {
- sHandler = new Handler();
- }
- return sHandler;
- }
-
- /**
- * Schedules the execution of the given delegate Runnable at the next callback time.
- */
- private static final class PeriodicRunnable implements Runnable {
-
- private final Runnable mDelegate;
- private final Period mPeriod;
- private final long mOffset;
-
- public PeriodicRunnable(Runnable delegate, Period period, long offset) {
- mDelegate = delegate;
- mPeriod = period;
- mOffset = offset;
- }
-
- @Override
- public void run() {
- LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod);
- mDelegate.run();
- schedule();
- }
-
- private void runAndReschedule() {
- LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod);
- unSchedule();
- mDelegate.run();
- schedule();
- }
-
- private void schedule() {
- final long delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset);
- getHandler().postDelayed(this, delay);
- }
-
- private void unSchedule() {
- getHandler().removeCallbacks(this);
- }
- }
-
- /**
- * Reschedules callbacks when the device time changes.
- */
- private final class TimeChangedReceiver extends BroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- for (PeriodicRunnable periodicRunnable : mPeriodicRunnables) {
- periodicRunnable.runAndReschedule();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt b/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt
new file mode 100644
index 0000000..0a76c94
--- /dev/null
+++ b/src/com/android/deskclock/uidata/PeriodicCallbackModel.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import android.os.Looper
+import android.text.format.DateUtils
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.LogUtils
+import com.android.deskclock.Utils
+
+import java.util.concurrent.CopyOnWriteArrayList
+import java.util.Calendar
+
+/**
+ * All callbacks to be delivered at requested times on the main thread if the application is in the
+ * foreground when the callback time passes.
+ */
+internal class PeriodicCallbackModel(context: Context) {
+
+ @VisibleForTesting
+ internal enum class Period {
+ MINUTE, QUARTER_HOUR, HOUR, MIDNIGHT
+ }
+
+ /** Reschedules callbacks when the device time changes. */
+ private val mTimeChangedReceiver: BroadcastReceiver = TimeChangedReceiver()
+
+ private val mPeriodicRunnables: MutableList<PeriodicRunnable> = CopyOnWriteArrayList()
+
+ init {
+ // Reschedules callbacks when the device time changes.
+ val timeChangedBroadcastFilter = IntentFilter()
+ timeChangedBroadcastFilter.addAction(Intent.ACTION_TIME_CHANGED)
+ timeChangedBroadcastFilter.addAction(Intent.ACTION_DATE_CHANGED)
+ timeChangedBroadcastFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED)
+ context.registerReceiver(mTimeChangedReceiver, timeChangedBroadcastFilter)
+ }
+
+ /**
+ * @param runnable to be called every minute
+ * @param offset an offset applied to the minute to control when the callback occurs
+ */
+ fun addMinuteCallback(runnable: Runnable, offset: Long) {
+ addPeriodicCallback(runnable, Period.MINUTE, offset)
+ }
+
+ /**
+ * @param runnable to be called every quarter-hour
+ */
+ fun addQuarterHourCallback(runnable: Runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the quarter-hour callback
+ // to ensure the sampled wallclock time reflects the subsequent quarter-hour.
+ addPeriodicCallback(runnable, Period.QUARTER_HOUR, 100L)
+ }
+
+ /**
+ * @param runnable to be called every hour
+ */
+ fun addHourCallback(runnable: Runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the hour callback to ensure
+ // the sampled wallclock time reflects the subsequent hour.
+ addPeriodicCallback(runnable, Period.HOUR, 100L)
+ }
+
+ /**
+ * @param runnable to be called every midnight
+ */
+ fun addMidnightCallback(runnable: Runnable) {
+ // Callbacks *can* occur early so pad in an extra 100ms on the midnight callback to ensure
+ // the sampled wallclock time reflects the subsequent day.
+ addPeriodicCallback(runnable, Period.MIDNIGHT, 100L)
+ }
+
+ /**
+ * @param runnable to be called periodically
+ */
+ private fun addPeriodicCallback(runnable: Runnable, period: Period, offset: Long) {
+ val periodicRunnable = PeriodicRunnable(runnable, period, offset)
+ mPeriodicRunnables.add(periodicRunnable)
+ periodicRunnable.schedule()
+ }
+
+ /**
+ * @param runnable to no longer be called periodically
+ */
+ fun removePeriodicCallback(runnable: Runnable) {
+ for (periodicRunnable in mPeriodicRunnables) {
+ if (periodicRunnable.mDelegate === runnable) {
+ periodicRunnable.unSchedule()
+ mPeriodicRunnables.remove(periodicRunnable)
+ return
+ }
+ }
+ }
+
+ /**
+ * Schedules the execution of the given delegate Runnable at the next callback time.
+ */
+ private class PeriodicRunnable(
+ val mDelegate: Runnable,
+ private val mPeriod: Period,
+ private val mOffset: Long
+ ) : Runnable {
+ override fun run() {
+ LOGGER.i("Executing periodic callback for %s because the period ended", mPeriod)
+ mDelegate.run()
+ schedule()
+ }
+
+ fun runAndReschedule() {
+ LOGGER.i("Executing periodic callback for %s because the time changed", mPeriod)
+ unSchedule()
+ mDelegate.run()
+ schedule()
+ }
+
+ fun schedule() {
+ val delay = getDelay(System.currentTimeMillis(), mPeriod, mOffset)
+ handler.postDelayed(this, delay)
+ }
+
+ fun unSchedule() {
+ handler.removeCallbacks(this)
+ }
+ }
+
+ /**
+ * Reschedules callbacks when the device time changes.
+ */
+ private inner class TimeChangedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ for (periodicRunnable in mPeriodicRunnables) {
+ periodicRunnable.runAndReschedule()
+ }
+ }
+ }
+
+ companion object {
+ private val LOGGER = LogUtils.Logger("Periodic")
+
+ private const val QUARTER_HOUR_IN_MILLIS = 15 * DateUtils.MINUTE_IN_MILLIS
+
+ private var sHandler: Handler? = null
+
+ /**
+ * Return the delay until the given `period` elapses adjusted by the given `offset`.
+ *
+ * @param now the current time
+ * @param period the frequency with which callbacks should be given
+ * @param offset an offset to add to the normal period; allows the callback to
+ * be made relative to the normally scheduled period end
+ * @return the time delay from `now` to schedule the callback
+ */
+ @VisibleForTesting
+ @JvmStatic
+ fun getDelay(now: Long, period: Period, offset: Long): Long {
+ val periodStart = now - offset
+
+ return when (period) {
+ Period.MINUTE -> {
+ val lastMinute = periodStart - periodStart % DateUtils.MINUTE_IN_MILLIS
+ val nextMinute = lastMinute + DateUtils.MINUTE_IN_MILLIS
+ nextMinute - now + offset
+ }
+ Period.QUARTER_HOUR -> {
+ val lastQuarterHour = periodStart - periodStart % QUARTER_HOUR_IN_MILLIS
+ val nextQuarterHour = lastQuarterHour + QUARTER_HOUR_IN_MILLIS
+ nextQuarterHour - now + offset
+ }
+ Period.HOUR -> {
+ val lastHour = periodStart - periodStart % DateUtils.HOUR_IN_MILLIS
+ val nextHour = lastHour + DateUtils.HOUR_IN_MILLIS
+ nextHour - now + offset
+ }
+ Period.MIDNIGHT -> {
+ val nextMidnight = Calendar.getInstance()
+ nextMidnight.timeInMillis = periodStart
+ nextMidnight.add(Calendar.DATE, 1)
+ nextMidnight[Calendar.HOUR_OF_DAY] = 0
+ nextMidnight[Calendar.MINUTE] = 0
+ nextMidnight[Calendar.SECOND] = 0
+ nextMidnight[Calendar.MILLISECOND] = 0
+ nextMidnight.timeInMillis - now + offset
+ }
+ }
+ }
+
+ private val handler: Handler
+ get() {
+ Utils.enforceMainLooper()
+ if (sHandler == null) {
+ sHandler = Handler(Looper.myLooper()!!)
+ }
+ return sHandler!!
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabDAO.java b/src/com/android/deskclock/uidata/TabDAO.java
deleted file mode 100644
index 3be5d19..0000000
--- a/src/com/android/deskclock/uidata/TabDAO.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.uidata;
-
-import android.content.SharedPreferences;
-
-import static com.android.deskclock.uidata.UiDataModel.Tab;
-
-/**
- * This class encapsulates the storage of tab data in {@link SharedPreferences}.
- */
-final class TabDAO {
-
- /** Key to a preference that stores the ordinal of the selected tab. */
- private static final String KEY_SELECTED_TAB = "selected_tab";
-
- private TabDAO() {}
-
- /**
- * @return an enumerated value indicating the currently selected primary tab
- */
- static Tab getSelectedTab(SharedPreferences prefs) {
- final int ordinal = prefs.getInt(KEY_SELECTED_TAB, Tab.CLOCKS.ordinal());
- return Tab.values()[ordinal];
- }
-
- /**
- * @param tab an enumerated value indicating the newly selected primary tab
- */
- static void setSelectedTab(SharedPreferences prefs, Tab tab) {
- prefs.edit().putInt(KEY_SELECTED_TAB, tab.ordinal()).apply();
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabDAO.kt b/src/com/android/deskclock/uidata/TabDAO.kt
new file mode 100644
index 0000000..fdbf3e1
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabDAO.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata
+
+import android.content.SharedPreferences
+
+/**
+ * This class encapsulates the storage of tab data in [SharedPreferences].
+ */
+internal object TabDAO {
+ /** Key to a preference that stores the ordinal of the selected tab. */
+ private const val KEY_SELECTED_TAB = "selected_tab"
+
+ /**
+ * @return an enumerated value indicating the currently selected primary tab
+ */
+ fun getSelectedTab(prefs: SharedPreferences): UiDataModel.Tab {
+ val ordinal = prefs.getInt(KEY_SELECTED_TAB, UiDataModel.Tab.CLOCKS.ordinal)
+ return UiDataModel.Tab.values()[ordinal]
+ }
+
+ /**
+ * @param tab an enumerated value indicating the newly selected primary tab
+ */
+ fun setSelectedTab(prefs: SharedPreferences, tab: UiDataModel.Tab) {
+ prefs.edit().putInt(KEY_SELECTED_TAB, tab.ordinal).apply()
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabListener.java b/src/com/android/deskclock/uidata/TabListener.kt
similarity index 77%
rename from src/com/android/deskclock/uidata/TabListener.java
rename to src/com/android/deskclock/uidata/TabListener.kt
index 918b893..c0e7721 100644
--- a/src/com/android/deskclock/uidata/TabListener.java
+++ b/src/com/android/deskclock/uidata/TabListener.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,18 +14,15 @@
* limitations under the License.
*/
-package com.android.deskclock.uidata;
-
-import com.android.deskclock.uidata.UiDataModel.Tab;
+package com.android.deskclock.uidata
/**
* The interface through which interested parties are notified of changes to the selected tab.
*/
-public interface TabListener {
-
+interface TabListener {
/**
* @param oldSelectedTab an enumerated value indicating the prior selected tab
* @param newSelectedTab an enumerated value indicating the newly selected tab
*/
- void selectedTabChanged(Tab oldSelectedTab, Tab newSelectedTab);
+ fun selectedTabChanged(oldSelectedTab: UiDataModel.Tab, newSelectedTab: UiDataModel.Tab)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabModel.java b/src/com/android/deskclock/uidata/TabModel.java
deleted file mode 100644
index 5b878ed..0000000
--- a/src/com/android/deskclock/uidata/TabModel.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.uidata;
-
-import android.content.SharedPreferences;
-import android.text.TextUtils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Locale;
-
-import static android.view.View.LAYOUT_DIRECTION_RTL;
-import static com.android.deskclock.uidata.UiDataModel.Tab;
-
-/**
- * All tab data is accessed via this model.
- */
-final class TabModel {
-
- private final SharedPreferences mPrefs;
-
- /** The listeners to notify when the selected tab is changed. */
- private final List<TabListener> mTabListeners = new ArrayList<>();
-
- /** The listeners to notify when the vertical scroll state of the selected tab is changed. */
- private final List<TabScrollListener> mTabScrollListeners = new ArrayList<>();
-
- /** The scrolled-to-top state of each tab. */
- private final boolean[] mTabScrolledToTop = new boolean[Tab.values().length];
-
- /** An enumerated value indicating the currently selected tab. */
- private Tab mSelectedTab;
-
- TabModel(SharedPreferences prefs) {
- mPrefs = prefs;
- Arrays.fill(mTabScrolledToTop, true);
- }
-
- //
- // Selected tab
- //
-
- /**
- * @param tabListener to be notified when the selected tab changes
- */
- void addTabListener(TabListener tabListener) {
- mTabListeners.add(tabListener);
- }
-
- /**
- * @param tabListener to no longer be notified when the selected tab changes
- */
- void removeTabListener(TabListener tabListener) {
- mTabListeners.remove(tabListener);
- }
-
- /**
- * @return the number of tabs
- */
- int getTabCount() {
- return Tab.values().length;
- }
-
- /**
- * @param ordinal the ordinal (left-to-right index) of the tab
- * @return the tab at the given {@code ordinal}
- */
- Tab getTab(int ordinal) {
- return Tab.values()[ordinal];
- }
-
- /**
- * @param position the position of the tab in the user interface
- * @return the tab at the given {@code ordinal}
- */
- Tab getTabAt(int position) {
- final int ordinal;
- if (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == LAYOUT_DIRECTION_RTL) {
- ordinal = getTabCount() - position - 1;
- } else {
- ordinal = position;
- }
- return getTab(ordinal);
- }
-
- /**
- * @return an enumerated value indicating the currently selected primary tab
- */
- Tab getSelectedTab() {
- if (mSelectedTab == null) {
- mSelectedTab = TabDAO.getSelectedTab(mPrefs);
- }
- return mSelectedTab;
- }
-
- /**
- * @param tab an enumerated value indicating the newly selected primary tab
- */
- void setSelectedTab(Tab tab) {
- final Tab oldSelectedTab = getSelectedTab();
- if (oldSelectedTab != tab) {
- mSelectedTab = tab;
- TabDAO.setSelectedTab(mPrefs, tab);
-
- // Notify of the tab change.
- for (TabListener tl : mTabListeners) {
- tl.selectedTabChanged(oldSelectedTab, tab);
- }
-
- // Notify of the vertical scroll position change if there is one.
- final boolean tabScrolledToTop = isTabScrolledToTop(tab);
- if (isTabScrolledToTop(oldSelectedTab) != tabScrolledToTop) {
- for (TabScrollListener tsl : mTabScrollListeners) {
- tsl.selectedTabScrollToTopChanged(tab, tabScrolledToTop);
- }
- }
- }
- }
-
- //
- // Tab scrolling
- //
-
- /**
- * @param tabScrollListener to be notified when the scroll position of the selected tab changes
- */
- void addTabScrollListener(TabScrollListener tabScrollListener) {
- mTabScrollListeners.add(tabScrollListener);
- }
-
- /**
- * @param tabScrollListener to be notified when the scroll position of the selected tab changes
- */
- void removeTabScrollListener(TabScrollListener tabScrollListener) {
- mTabScrollListeners.remove(tabScrollListener);
- }
-
- /**
- * Updates the scrolling state in the {@link UiDataModel} for this tab.
- *
- * @param tab an enumerated value indicating the tab reporting its vertical scroll position
- * @param scrolledToTop {@code true} iff the vertical scroll position of this tab is at the top
- */
- void setTabScrolledToTop(Tab tab, boolean scrolledToTop) {
- if (isTabScrolledToTop(tab) != scrolledToTop) {
- mTabScrolledToTop[tab.ordinal()] = scrolledToTop;
- if (tab == getSelectedTab()) {
- for (TabScrollListener tsl : mTabScrollListeners) {
- tsl.selectedTabScrollToTopChanged(tab, scrolledToTop);
- }
- }
- }
- }
-
- /**
- * @param tab identifies the tab
- * @return {@code true} iff the content in the given {@code tab} is currently scrolled to top
- */
- boolean isTabScrolledToTop(Tab tab) {
- return mTabScrolledToTop[tab.ordinal()];
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabModel.kt b/src/com/android/deskclock/uidata/TabModel.kt
new file mode 100644
index 0000000..8d0c9f0
--- /dev/null
+++ b/src/com/android/deskclock/uidata/TabModel.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata
+
+import android.content.SharedPreferences
+import android.text.TextUtils
+import android.view.View
+
+import java.util.Locale
+
+/**
+ * All tab data is accessed via this model.
+ */
+internal class TabModel(private val mPrefs: SharedPreferences) {
+
+ /** The listeners to notify when the selected tab is changed. */
+ private val mTabListeners: MutableList<TabListener> = ArrayList()
+
+ /** The listeners to notify when the vertical scroll state of the selected tab is changed. */
+ private val mTabScrollListeners: MutableList<TabScrollListener> = ArrayList()
+
+ /** The scrolled-to-top state of each tab. */
+ private val mTabScrolledToTop = BooleanArray(UiDataModel.Tab.values().size)
+
+ /** An enumerated value indicating the currently selected tab. */
+ private var mSelectedTab: UiDataModel.Tab? = null
+
+ init {
+ mTabScrolledToTop.fill(true)
+ }
+
+ //
+ // Selected tab
+ //
+
+ /**
+ * @param tabListener to be notified when the selected tab changes
+ */
+ fun addTabListener(tabListener: TabListener) {
+ mTabListeners.add(tabListener)
+ }
+
+ /**
+ * @param tabListener to no longer be notified when the selected tab changes
+ */
+ fun removeTabListener(tabListener: TabListener) {
+ mTabListeners.remove(tabListener)
+ }
+
+ /**
+ * @return the number of tabs
+ */
+ val tabCount: Int
+ get() = UiDataModel.Tab.values().size
+
+ /**
+ * @param ordinal the ordinal (left-to-right index) of the tab
+ * @return the tab at the given `ordinal`
+ */
+ fun getTab(ordinal: Int): UiDataModel.Tab {
+ return UiDataModel.Tab.values()[ordinal]
+ }
+
+ /**
+ * @param position the position of the tab in the user interface
+ * @return the tab at the given `ordinal`
+ */
+ fun getTabAt(position: Int): UiDataModel.Tab {
+ val layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ val ordinal: Int = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
+ tabCount - position - 1
+ } else {
+ position
+ }
+ return getTab(ordinal)
+ }
+
+ /**
+ * @return an enumerated value indicating the currently selected primary tab
+ */
+ val selectedTab: UiDataModel.Tab
+ get() {
+ if (mSelectedTab == null) {
+ mSelectedTab = TabDAO.getSelectedTab(mPrefs)
+ }
+ return mSelectedTab!!
+ }
+
+ /**
+ * @param tab an enumerated value indicating the newly selected primary tab
+ */
+ fun setSelectedTab(tab: UiDataModel.Tab) {
+ val oldSelectedTab = selectedTab
+ if (oldSelectedTab != tab) {
+ mSelectedTab = tab
+ TabDAO.setSelectedTab(mPrefs, tab)
+
+ // Notify of the tab change.
+ for (tl in mTabListeners) {
+ tl.selectedTabChanged(oldSelectedTab, tab)
+ }
+
+ // Notify of the vertical scroll position change if there is one.
+ val tabScrolledToTop = isTabScrolledToTop(tab)
+ if (isTabScrolledToTop(oldSelectedTab) != tabScrolledToTop) {
+ for (tsl in mTabScrollListeners) {
+ tsl.selectedTabScrollToTopChanged(tab, tabScrolledToTop)
+ }
+ }
+ }
+ }
+
+ //
+ // Tab scrolling
+ //
+
+ /**
+ * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+ */
+ fun addTabScrollListener(tabScrollListener: TabScrollListener) {
+ mTabScrollListeners.add(tabScrollListener)
+ }
+
+ /**
+ * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+ */
+ fun removeTabScrollListener(tabScrollListener: TabScrollListener) {
+ mTabScrollListeners.remove(tabScrollListener)
+ }
+
+ /**
+ * Updates the scrolling state in the [UiDataModel] for this tab.
+ *
+ * @param tab an enumerated value indicating the tab reporting its vertical scroll position
+ * @param scrolledToTop `true` iff the vertical scroll position of this tab is at the top
+ */
+ fun setTabScrolledToTop(tab: UiDataModel.Tab, scrolledToTop: Boolean) {
+ if (isTabScrolledToTop(tab) != scrolledToTop) {
+ mTabScrolledToTop[tab.ordinal] = scrolledToTop
+ if (tab == selectedTab) {
+ for (tsl in mTabScrollListeners) {
+ tsl.selectedTabScrollToTopChanged(tab, scrolledToTop)
+ }
+ }
+ }
+ }
+
+ /**
+ * @param tab identifies the tab
+ * @return `true` iff the content in the given `tab` is currently scrolled to top
+ */
+ fun isTabScrolledToTop(tab: UiDataModel.Tab): Boolean {
+ return mTabScrolledToTop[tab.ordinal]
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/TabScrollListener.java b/src/com/android/deskclock/uidata/TabScrollListener.kt
similarity index 63%
rename from src/com/android/deskclock/uidata/TabScrollListener.java
rename to src/com/android/deskclock/uidata/TabScrollListener.kt
index 1a7f155..82d78e4 100644
--- a/src/com/android/deskclock/uidata/TabScrollListener.java
+++ b/src/com/android/deskclock/uidata/TabScrollListener.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,25 +14,22 @@
* limitations under the License.
*/
-package com.android.deskclock.uidata;
-
-import com.android.deskclock.uidata.UiDataModel.Tab;
+package com.android.deskclock.uidata
/**
* The interface through which interested parties are notified of changes to the vertical scroll
* position of the selected tab. Callbacks to listener occur when any of these events occur:
*
* <ul>
- * <li>the vertical scroll position of the selected tab is now scrolled to the top</li>
- * <li>the vertical scroll position of the selected tab is no longer scrolled to the top</li>
- * <li>the selected tab changed and the new tab scroll state does not match the prior tab</li>
+ * <li>the vertical scroll position of the selected tab is now scrolled to the top
+ * <li>the vertical scroll position of the selected tab is no longer scrolled to the top
+ * <li>the selected tab changed and the new tab scroll state does not match the prior tab
* </ul>
*/
-public interface TabScrollListener {
-
+interface TabScrollListener {
/**
* @param selectedTab an enumerated value indicating the current selected tab
* @param scrolledToTop indicates whether the current selected tab is scrolled to its top
*/
- void selectedTabScrollToTopChanged(Tab selectedTab, boolean scrolledToTop);
+ fun selectedTabScrollToTopChanged(selectedTab: UiDataModel.Tab, scrolledToTop: Boolean)
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/uidata/UiDataModel.java b/src/com/android/deskclock/uidata/UiDataModel.java
deleted file mode 100644
index f7a6917..0000000
--- a/src/com/android/deskclock/uidata/UiDataModel.java
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.uidata;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.Typeface;
-import androidx.annotation.DrawableRes;
-import androidx.annotation.StringRes;
-
-import com.android.deskclock.AlarmClockFragment;
-import com.android.deskclock.ClockFragment;
-import com.android.deskclock.R;
-import com.android.deskclock.stopwatch.StopwatchFragment;
-import com.android.deskclock.timer.TimerFragment;
-
-import java.util.Calendar;
-
-import static com.android.deskclock.Utils.enforceMainLooper;
-
-/**
- * All application-wide user interface data is accessible through this singleton.
- */
-public final class UiDataModel {
-
- /** Identifies each of the primary tabs within the application. */
- public enum Tab {
- ALARMS(AlarmClockFragment.class, R.drawable.ic_tab_alarm, R.string.menu_alarm),
- CLOCKS(ClockFragment.class, R.drawable.ic_tab_clock, R.string.menu_clock),
- TIMERS(TimerFragment.class, R.drawable.ic_tab_timer, R.string.menu_timer),
- STOPWATCH(StopwatchFragment.class, R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);
-
- private final String mFragmentClassName;
- private final @DrawableRes int mIconResId;
- private final @StringRes int mLabelResId;
-
- Tab(Class fragmentClass, @DrawableRes int iconResId, @StringRes int labelResId) {
- mFragmentClassName = fragmentClass.getName();
- mIconResId = iconResId;
- mLabelResId = labelResId;
- }
-
- public String getFragmentClassName() { return mFragmentClassName; }
- public @DrawableRes int getIconResId() { return mIconResId; }
- public @StringRes int getLabelResId() { return mLabelResId; }
- }
-
- /** The single instance of this data model that exists for the life of the application. */
- private static final UiDataModel sUiDataModel = new UiDataModel();
-
- public static UiDataModel getUiDataModel() {
- return sUiDataModel;
- }
-
- private Context mContext;
-
- /** The model from which tab data are fetched. */
- private TabModel mTabModel;
-
- /** The model from which formatted strings are fetched. */
- private FormattedStringModel mFormattedStringModel;
-
- /** The model from which timed callbacks originate. */
- private PeriodicCallbackModel mPeriodicCallbackModel;
-
- private UiDataModel() {}
-
- /**
- * The context may be set precisely once during the application life.
- */
- public void init(Context context, SharedPreferences prefs) {
- if (mContext != context) {
- mContext = context.getApplicationContext();
-
- mPeriodicCallbackModel = new PeriodicCallbackModel(mContext);
- mFormattedStringModel = new FormattedStringModel(mContext);
- mTabModel = new TabModel(prefs);
- }
- }
-
- /**
- * To display the alarm clock in this font, use the character {@link R.string#clock_emoji}.
- *
- * @return a special font containing a glyph that draws an alarm clock
- */
- public Typeface getAlarmIconTypeface() {
- return Typeface.createFromAsset(mContext.getAssets(), "fonts/clock.ttf");
- }
-
- //
- // Formatted Strings
- //
-
- /**
- * This method is intended to be used when formatting numbers occurs in a hotspot such as the
- * update loop of a timer or stopwatch. It returns cached results when possible in order to
- * provide speed and limit garbage to be collected by the virtual machine.
- *
- * @param value a positive integer to format as a String
- * @return the {@code value} formatted as a String in the current locale
- * @throws IllegalArgumentException if {@code value} is negative
- */
- public String getFormattedNumber(int value) {
- enforceMainLooper();
- return mFormattedStringModel.getFormattedNumber(value);
- }
-
- /**
- * This method is intended to be used when formatting numbers occurs in a hotspot such as the
- * update loop of a timer or stopwatch. It returns cached results when possible in order to
- * provide speed and limit garbage to be collected by the virtual machine.
- *
- * @param value a positive integer to format as a String
- * @param length the length of the String; zeroes are padded to match this length
- * @return the {@code value} formatted as a String in the current locale and padded to the
- * requested {@code length}
- * @throws IllegalArgumentException if {@code value} is negative
- */
- public String getFormattedNumber(int value, int length) {
- enforceMainLooper();
- return mFormattedStringModel.getFormattedNumber(value, length);
- }
-
- /**
- * This method is intended to be used when formatting numbers occurs in a hotspot such as the
- * update loop of a timer or stopwatch. It returns cached results when possible in order to
- * provide speed and limit garbage to be collected by the virtual machine.
- *
- * @param negative force a minus sign (-) onto the display, even if {@code value} is {@code 0}
- * @param value a positive integer to format as a String
- * @param length the length of the String; zeroes are padded to match this length. If
- * {@code negative} is {@code true} the return value will contain a minus sign and a total
- * length of {@code length + 1}.
- * @return the {@code value} formatted as a String in the current locale and padded to the
- * requested {@code length}
- * @throws IllegalArgumentException if {@code value} is negative
- */
- public String getFormattedNumber(boolean negative, int value, int length) {
- enforceMainLooper();
- return mFormattedStringModel.getFormattedNumber(negative, value, length);
- }
-
- /**
- * @param calendarDay any of the following values
- * <ul>
- * <li>{@link Calendar#SUNDAY}</li>
- * <li>{@link Calendar#MONDAY}</li>
- * <li>{@link Calendar#TUESDAY}</li>
- * <li>{@link Calendar#WEDNESDAY}</li>
- * <li>{@link Calendar#THURSDAY}</li>
- * <li>{@link Calendar#FRIDAY}</li>
- * <li>{@link Calendar#SATURDAY}</li>
- * </ul>
- * @return single-character version of weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
- */
- public String getShortWeekday(int calendarDay) {
- enforceMainLooper();
- return mFormattedStringModel.getShortWeekday(calendarDay);
- }
-
- /**
- * @param calendarDay any of the following values
- * <ul>
- * <li>{@link Calendar#SUNDAY}</li>
- * <li>{@link Calendar#MONDAY}</li>
- * <li>{@link Calendar#TUESDAY}</li>
- * <li>{@link Calendar#WEDNESDAY}</li>
- * <li>{@link Calendar#THURSDAY}</li>
- * <li>{@link Calendar#FRIDAY}</li>
- * <li>{@link Calendar#SATURDAY}</li>
- * </ul>
- * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
- */
- public String getLongWeekday(int calendarDay) {
- enforceMainLooper();
- return mFormattedStringModel.getLongWeekday(calendarDay);
- }
-
- //
- // Animations
- //
-
- /**
- * @return the duration in milliseconds of short animations
- */
- public long getShortAnimationDuration() {
- enforceMainLooper();
- return mContext.getResources().getInteger(android.R.integer.config_shortAnimTime);
- }
-
- /**
- * @return the duration in milliseconds of long animations
- */
- public long getLongAnimationDuration() {
- enforceMainLooper();
- return mContext.getResources().getInteger(android.R.integer.config_longAnimTime);
- }
-
- //
- // Tabs
- //
-
- /**
- * @param tabListener to be notified when the selected tab changes
- */
- public void addTabListener(TabListener tabListener) {
- enforceMainLooper();
- mTabModel.addTabListener(tabListener);
- }
-
- /**
- * @param tabListener to no longer be notified when the selected tab changes
- */
- public void removeTabListener(TabListener tabListener) {
- enforceMainLooper();
- mTabModel.removeTabListener(tabListener);
- }
-
- /**
- * @return the number of tabs
- */
- public int getTabCount() {
- enforceMainLooper();
- return mTabModel.getTabCount();
- }
-
- /**
- * @param ordinal the ordinal of the tab
- * @return the tab at the given {@code ordinal}
- */
- public Tab getTab(int ordinal) {
- enforceMainLooper();
- return mTabModel.getTab(ordinal);
- }
-
- /**
- * @param position the position of the tab in the user interface
- * @return the tab at the given {@code ordinal}
- */
- public Tab getTabAt(int position) {
- enforceMainLooper();
- return mTabModel.getTabAt(position);
- }
-
- /**
- * @return an enumerated value indicating the currently selected primary tab
- */
- public Tab getSelectedTab() {
- enforceMainLooper();
- return mTabModel.getSelectedTab();
- }
-
- /**
- * @param tab an enumerated value indicating the newly selected primary tab
- */
- public void setSelectedTab(Tab tab) {
- enforceMainLooper();
- mTabModel.setSelectedTab(tab);
- }
-
- /**
- * @param tabScrollListener to be notified when the scroll position of the selected tab changes
- */
- public void addTabScrollListener(TabScrollListener tabScrollListener) {
- enforceMainLooper();
- mTabModel.addTabScrollListener(tabScrollListener);
- }
-
- /**
- * @param tabScrollListener to be notified when the scroll position of the selected tab changes
- */
- public void removeTabScrollListener(TabScrollListener tabScrollListener) {
- enforceMainLooper();
- mTabModel.removeTabScrollListener(tabScrollListener);
- }
-
- /**
- * Updates the scrolling state in the {@link UiDataModel} for this tab.
- *
- * @param tab an enumerated value indicating the tab reporting its vertical scroll position
- * @param scrolledToTop {@code true} iff the vertical scroll position of the tab is at the top
- */
- public void setTabScrolledToTop(Tab tab, boolean scrolledToTop) {
- enforceMainLooper();
- mTabModel.setTabScrolledToTop(tab, scrolledToTop);
- }
-
- /**
- * @return {@code true} iff the content in the selected tab is currently scrolled to the top
- */
- public boolean isSelectedTabScrolledToTop() {
- enforceMainLooper();
- return mTabModel.isTabScrolledToTop(getSelectedTab());
- }
-
- //
- // Shortcut Ids
- //
-
- /**
- * @param category which category of shortcut of which to get the id
- * @param action the desired action to perform
- * @return the id of the shortcut
- */
- public String getShortcutId(@StringRes int category, @StringRes int action) {
- if (category == R.string.category_stopwatch) {
- return mContext.getString(category);
- }
- return mContext.getString(category) + "_" + mContext.getString(action);
- }
-
- //
- // Timed Callbacks
- //
-
- /**
- * @param runnable to be called every minute
- * @param offset an offset applied to the minute to control when the callback occurs
- */
- public void addMinuteCallback(Runnable runnable, long offset) {
- enforceMainLooper();
- mPeriodicCallbackModel.addMinuteCallback(runnable, offset);
- }
-
- /**
- * @param runnable to be called every quarter-hour
- * @param offset an offset applied to the quarter-hour to control when the callback occurs
- */
- public void addQuarterHourCallback(Runnable runnable, long offset) {
- enforceMainLooper();
- mPeriodicCallbackModel.addQuarterHourCallback(runnable, offset);
- }
-
- /**
- * @param runnable to be called every hour
- * @param offset an offset applied to the hour to control when the callback occurs
- */
- public void addHourCallback(Runnable runnable, long offset) {
- enforceMainLooper();
- mPeriodicCallbackModel.addHourCallback(runnable, offset);
- }
-
- /**
- * @param runnable to be called every midnight
- * @param offset an offset applied to the midnight to control when the callback occurs
- */
- public void addMidnightCallback(Runnable runnable, long offset) {
- enforceMainLooper();
- mPeriodicCallbackModel.addMidnightCallback(runnable, offset);
- }
-
- /**
- * @param runnable to no longer be called periodically
- */
- public void removePeriodicCallback(Runnable runnable) {
- enforceMainLooper();
- mPeriodicCallbackModel.removePeriodicCallback(runnable);
- }
-}
diff --git a/src/com/android/deskclock/uidata/UiDataModel.kt b/src/com/android/deskclock/uidata/UiDataModel.kt
new file mode 100644
index 0000000..c1561d7
--- /dev/null
+++ b/src/com/android/deskclock/uidata/UiDataModel.kt
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.graphics.Typeface
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+
+import com.android.deskclock.AlarmClockFragment
+import com.android.deskclock.ClockFragment
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.stopwatch.StopwatchFragment
+import com.android.deskclock.timer.TimerFragment
+
+/**
+ * All application-wide user interface data is accessible through this singleton.
+ */
+class UiDataModel private constructor() {
+ /** Identifies each of the primary tabs within the application. */
+ enum class Tab(
+ fragmentClass: Class<*>,
+ @DrawableRes val iconResId: Int,
+ @StringRes val labelResId: Int
+ ) {
+ ALARMS(AlarmClockFragment::class.java, R.drawable.ic_tab_alarm, R.string.menu_alarm),
+ CLOCKS(ClockFragment::class.java, R.drawable.ic_tab_clock, R.string.menu_clock),
+ TIMERS(TimerFragment::class.java, R.drawable.ic_tab_timer, R.string.menu_timer),
+ STOPWATCH(StopwatchFragment::class.java,
+ R.drawable.ic_tab_stopwatch, R.string.menu_stopwatch);
+
+ val fragmentClassName: String = fragmentClass.name
+ }
+
+ private var mContext: Context? = null
+
+ /** The model from which tab data are fetched. */
+ private lateinit var mTabModel: TabModel
+
+ /** The model from which formatted strings are fetched. */
+ private lateinit var mFormattedStringModel: FormattedStringModel
+
+ /** The model from which timed callbacks originate. */
+ private lateinit var mPeriodicCallbackModel: PeriodicCallbackModel
+
+ /**
+ * The context may be set precisely once during the application life.
+ */
+ fun init(context: Context, prefs: SharedPreferences) {
+ if (mContext !== context) {
+ mContext = context.applicationContext
+
+ mPeriodicCallbackModel = PeriodicCallbackModel(mContext!!)
+ mFormattedStringModel = FormattedStringModel(mContext!!)
+ mTabModel = TabModel(prefs)
+ }
+ }
+
+ /**
+ * To display the alarm clock in this font, use the character [R.string.clock_emoji].
+ *
+ * @return a special font containing a glyph that draws an alarm clock
+ */
+ val alarmIconTypeface: Typeface
+ get() = Typeface.createFromAsset(mContext!!.assets, "fonts/clock.ttf")
+
+ //
+ // Formatted Strings
+ //
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @return the `value` formatted as a String in the current locale
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(value: Int): String {
+ Utils.enforceMainLooper()
+ return mFormattedStringModel.getFormattedNumber(value)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length
+ * @return the `value` formatted as a String in the current locale and padded to the
+ * requested `length`
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(value: Int, length: Int): String {
+ Utils.enforceMainLooper()
+ return mFormattedStringModel.getFormattedNumber(value, length)
+ }
+
+ /**
+ * This method is intended to be used when formatting numbers occurs in a hotspot such as the
+ * update loop of a timer or stopwatch. It returns cached results when possible in order to
+ * provide speed and limit garbage to be collected by the virtual machine.
+ *
+ * @param negative force a minus sign (-) onto the display, even if `value` is `0`
+ * @param value a positive integer to format as a String
+ * @param length the length of the String; zeroes are padded to match this length. If
+ * `negative` is `true` the return value will contain a minus sign and a total
+ * length of `length + 1`.
+ * @return the `value` formatted as a String in the current locale and padded to the
+ * requested `length`
+ * @throws IllegalArgumentException if `value` is negative
+ */
+ fun getFormattedNumber(negative: Boolean, value: Int, length: Int): String {
+ Utils.enforceMainLooper()
+ return mFormattedStringModel.getFormattedNumber(negative, value, length)
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return single-character version of weekday name; e.g.: 'S', 'M', 'T', 'W', 'T', 'F', 'S'
+ */
+ fun getShortWeekday(calendarDay: Int): String? {
+ Utils.enforceMainLooper()
+ return mFormattedStringModel.getShortWeekday(calendarDay)
+ }
+
+ /**
+ * @param calendarDay any of the following values
+ *
+ * * [Calendar.SUNDAY]
+ * * [Calendar.MONDAY]
+ * * [Calendar.TUESDAY]
+ * * [Calendar.WEDNESDAY]
+ * * [Calendar.THURSDAY]
+ * * [Calendar.FRIDAY]
+ * * [Calendar.SATURDAY]
+ *
+ * @return full weekday name; e.g.: 'Sunday', 'Monday', 'Tuesday', etc.
+ */
+ fun getLongWeekday(calendarDay: Int): String? {
+ Utils.enforceMainLooper()
+ return mFormattedStringModel.getLongWeekday(calendarDay)
+ }
+
+ //
+ // Animations
+ //
+
+ /**
+ * @return the duration in milliseconds of short animations
+ */
+ val shortAnimationDuration: Long
+ get() {
+ Utils.enforceMainLooper()
+ return mContext!!.resources.getInteger(android.R.integer.config_shortAnimTime).toLong()
+ }
+
+ /**
+ * @return the duration in milliseconds of long animations
+ */
+ val longAnimationDuration: Long
+ get() {
+ Utils.enforceMainLooper()
+ return mContext!!.resources.getInteger(android.R.integer.config_longAnimTime).toLong()
+ }
+
+ //
+ // Tabs
+ //
+
+ /**
+ * @param tabListener to be notified when the selected tab changes
+ */
+ fun addTabListener(tabListener: TabListener) {
+ Utils.enforceMainLooper()
+ mTabModel.addTabListener(tabListener)
+ }
+
+ /**
+ * @param tabListener to no longer be notified when the selected tab changes
+ */
+ fun removeTabListener(tabListener: TabListener) {
+ Utils.enforceMainLooper()
+ mTabModel.removeTabListener(tabListener)
+ }
+
+ /**
+ * @return the number of tabs
+ */
+ val tabCount: Int
+ get() {
+ Utils.enforceMainLooper()
+ return mTabModel.tabCount
+ }
+
+ /**
+ * @param ordinal the ordinal of the tab
+ * @return the tab at the given `ordinal`
+ */
+ fun getTab(ordinal: Int): Tab {
+ Utils.enforceMainLooper()
+ return mTabModel.getTab(ordinal)
+ }
+
+ /**
+ * @param position the position of the tab in the user interface
+ * @return the tab at the given `ordinal`
+ */
+ fun getTabAt(position: Int): Tab {
+ Utils.enforceMainLooper()
+ return mTabModel.getTabAt(position)
+ }
+
+ var selectedTab: Tab
+ /**
+ * @return an enumerated value indicating the currently selected primary tab
+ */
+ get() {
+ Utils.enforceMainLooper()
+ return mTabModel.selectedTab
+ }
+ /**
+ * @param tab an enumerated value indicating the newly selected primary tab
+ */
+ set(tab) {
+ Utils.enforceMainLooper()
+ mTabModel.setSelectedTab(tab)
+ }
+
+ /**
+ * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+ */
+ fun addTabScrollListener(tabScrollListener: TabScrollListener) {
+ Utils.enforceMainLooper()
+ mTabModel.addTabScrollListener(tabScrollListener)
+ }
+
+ /**
+ * @param tabScrollListener to be notified when the scroll position of the selected tab changes
+ */
+ fun removeTabScrollListener(tabScrollListener: TabScrollListener) {
+ Utils.enforceMainLooper()
+ mTabModel.removeTabScrollListener(tabScrollListener)
+ }
+
+ /**
+ * Updates the scrolling state in the [UiDataModel] for this tab.
+ *
+ * @param tab an enumerated value indicating the tab reporting its vertical scroll position
+ * @param scrolledToTop `true` iff the vertical scroll position of the tab is at the top
+ */
+ fun setTabScrolledToTop(tab: Tab, scrolledToTop: Boolean) {
+ Utils.enforceMainLooper()
+ mTabModel.setTabScrolledToTop(tab, scrolledToTop)
+ }
+
+ /**
+ * @return `true` iff the content in the selected tab is currently scrolled to the top
+ */
+ val isSelectedTabScrolledToTop: Boolean
+ get() {
+ Utils.enforceMainLooper()
+ return mTabModel.isTabScrolledToTop(selectedTab)
+ }
+
+ //
+ // Shortcut Ids
+ //
+
+ /**
+ * @param category which category of shortcut of which to get the id
+ * @param action the desired action to perform
+ * @return the id of the shortcut
+ */
+ fun getShortcutId(@StringRes category: Int, @StringRes action: Int): String {
+ return if (category == R.string.category_stopwatch) {
+ mContext!!.getString(category)
+ } else {
+ mContext!!.getString(category) + "_" + mContext!!.getString(action)
+ }
+ }
+
+ //
+ // Timed Callbacks
+ //
+
+ /**
+ * @param runnable to be called every minute
+ * @param offset an offset applied to the minute to control when the callback occurs
+ */
+ fun addMinuteCallback(runnable: Runnable, offset: Long) {
+ Utils.enforceMainLooper()
+ mPeriodicCallbackModel.addMinuteCallback(runnable, offset)
+ }
+
+ /**
+ * @param runnable to be called every quarter-hour
+ */
+ fun addQuarterHourCallback(runnable: Runnable) {
+ Utils.enforceMainLooper()
+ mPeriodicCallbackModel.addQuarterHourCallback(runnable)
+ }
+
+ /**
+ * @param runnable to be called every hour
+ */
+ fun addHourCallback(runnable: Runnable) {
+ Utils.enforceMainLooper()
+ mPeriodicCallbackModel.addHourCallback(runnable)
+ }
+
+ /**
+ * @param runnable to be called every midnight
+ */
+ fun addMidnightCallback(runnable: Runnable) {
+ Utils.enforceMainLooper()
+ mPeriodicCallbackModel.addMidnightCallback(runnable)
+ }
+
+ /**
+ * @param runnable to no longer be called periodically
+ */
+ fun removePeriodicCallback(runnable: Runnable) {
+ Utils.enforceMainLooper()
+ mPeriodicCallbackModel.removePeriodicCallback(runnable)
+ }
+
+ companion object {
+ /** The single instance of this data model that exists for the life of the application. */
+ val sUiDataModel = UiDataModel()
+
+ @get:JvmStatic
+ val uiDataModel
+ get() = sUiDataModel
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/AutoSizingTextClock.java b/src/com/android/deskclock/widget/AutoSizingTextClock.java
deleted file mode 100644
index 1a70da9..0000000
--- a/src/com/android/deskclock/widget/AutoSizingTextClock.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.TextClock;
-
-/**
- * Wrapper around TextClock that automatically re-sizes itself to fit within the given bounds.
- */
-public class AutoSizingTextClock extends TextClock {
-
- private final TextSizeHelper mTextSizeHelper;
- private boolean mSuppressLayout = false;
-
- public AutoSizingTextClock(Context context) {
- this(context, null);
- }
-
- public AutoSizingTextClock(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public AutoSizingTextClock(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mTextSizeHelper = new TextSizeHelper(this);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- mTextSizeHelper.onMeasure(widthMeasureSpec, heightMeasureSpec);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
- super.onTextChanged(text, start, lengthBefore, lengthAfter);
- if (mTextSizeHelper != null) {
- if (lengthBefore != lengthAfter) {
- mSuppressLayout = false;
- }
- mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter);
- } else {
- requestLayout();
- }
- }
-
- @Override
- public void setText(CharSequence text, BufferType type) {
- mSuppressLayout = true;
- super.setText(text, type);
- mSuppressLayout = false;
- }
-
- @Override
- public void requestLayout() {
- if (mTextSizeHelper == null || !mTextSizeHelper.shouldIgnoreRequestLayout()) {
- if (!mSuppressLayout) {
- super.requestLayout();
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/AutoSizingTextClock.kt b/src/com/android/deskclock/widget/AutoSizingTextClock.kt
new file mode 100644
index 0000000..c332762
--- /dev/null
+++ b/src/com/android/deskclock/widget/AutoSizingTextClock.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.TextClock
+
+/**
+ * Wrapper around TextClock that automatically re-sizes itself to fit within the given bounds.
+ */
+class AutoSizingTextClock @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : TextClock(context, attrs, defStyleAttr) {
+ private val mTextSizeHelper: TextSizeHelper? = TextSizeHelper(this)
+
+ private var mSuppressLayout = false
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ mTextSizeHelper!!.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+
+ override fun onTextChanged(
+ text: CharSequence,
+ start: Int,
+ lengthBefore: Int,
+ lengthAfter: Int
+ ) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter)
+ if (mTextSizeHelper != null) {
+ if (lengthBefore != lengthAfter) {
+ mSuppressLayout = false
+ }
+ mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter)
+ } else {
+ requestLayout()
+ }
+ }
+
+ override fun setText(text: CharSequence, type: BufferType) {
+ mSuppressLayout = true
+ super.setText(text, type)
+ mSuppressLayout = false
+ }
+
+ override fun requestLayout() {
+ if (mTextSizeHelper == null || !mTextSizeHelper.shouldIgnoreRequestLayout()) {
+ if (!mSuppressLayout) {
+ super.requestLayout()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/AutoSizingTextView.java b/src/com/android/deskclock/widget/AutoSizingTextView.java
deleted file mode 100644
index 7e41997..0000000
--- a/src/com/android/deskclock/widget/AutoSizingTextView.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget;
-
-import android.content.Context;
-import androidx.appcompat.widget.AppCompatTextView;
-import android.util.AttributeSet;
-
-/**
- * A TextView which automatically re-sizes its text to fit within its boundaries.
- */
-public class AutoSizingTextView extends AppCompatTextView {
-
- private final TextSizeHelper mTextSizeHelper;
-
- public AutoSizingTextView(Context context) {
- this(context, null);
- }
-
- public AutoSizingTextView(Context context, AttributeSet attrs) {
- this(context, attrs, android.R.attr.textViewStyle);
- }
-
- public AutoSizingTextView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mTextSizeHelper = new TextSizeHelper(this);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- mTextSizeHelper.onMeasure(widthMeasureSpec, heightMeasureSpec);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- @Override
- protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
- super.onTextChanged(text, start, lengthBefore, lengthAfter);
- if (mTextSizeHelper != null) {
- mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter);
- } else {
- requestLayout();
- }
- }
-
- @Override
- public void requestLayout() {
- if (mTextSizeHelper == null || !mTextSizeHelper.shouldIgnoreRequestLayout()) {
- super.requestLayout();
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/AutoSizingTextView.kt b/src/com/android/deskclock/widget/AutoSizingTextView.kt
new file mode 100644
index 0000000..d393542
--- /dev/null
+++ b/src/com/android/deskclock/widget/AutoSizingTextView.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.appcompat.widget.AppCompatTextView
+
+/**
+ * A TextView which automatically re-sizes its text to fit within its boundaries.
+ */
+class AutoSizingTextView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = android.R.attr.textViewStyle
+) : AppCompatTextView(context, attrs, defStyleAttr) {
+ private val mTextSizeHelper: TextSizeHelper? = TextSizeHelper(this)
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ mTextSizeHelper!!.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+
+ override fun onTextChanged(
+ text: CharSequence?,
+ start: Int,
+ lengthBefore: Int,
+ lengthAfter: Int
+ ) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter)
+ if (mTextSizeHelper != null) {
+ mTextSizeHelper.onTextChanged(lengthBefore, lengthAfter)
+ } else {
+ requestLayout()
+ }
+ }
+
+ override fun requestLayout() {
+ if (mTextSizeHelper == null || !mTextSizeHelper.shouldIgnoreRequestLayout()) {
+ super.requestLayout()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/CircleView.java b/src/com/android/deskclock/widget/CircleView.java
deleted file mode 100644
index b3a4675..0000000
--- a/src/com/android/deskclock/widget/CircleView.java
+++ /dev/null
@@ -1,341 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.util.AttributeSet;
-import android.util.Property;
-import android.view.Gravity;
-import android.view.View;
-
-import com.android.deskclock.R;
-
-/**
- * A {@link View} that draws primitive circles.
- */
-public class CircleView extends View {
-
- /**
- * A Property wrapper around the fillColor functionality handled by the
- * {@link #setFillColor(int)} and {@link #getFillColor()} methods.
- */
- public final static Property<CircleView, Integer> FILL_COLOR =
- new Property<CircleView, Integer>(Integer.class, "fillColor") {
- @Override
- public Integer get(CircleView view) {
- return view.getFillColor();
- }
-
- @Override
- public void set(CircleView view, Integer value) {
- view.setFillColor(value);
- }
- };
-
- /**
- * A Property wrapper around the radius functionality handled by the
- * {@link #setRadius(float)} and {@link #getRadius()} methods.
- */
- public final static Property<CircleView, Float> RADIUS =
- new Property<CircleView, Float>(Float.class, "radius") {
- @Override
- public Float get(CircleView view) {
- return view.getRadius();
- }
-
- @Override
- public void set(CircleView view, Float value) {
- view.setRadius(value);
- }
- };
-
- /**
- * The {@link Paint} used to draw the circle.
- */
- private final Paint mCirclePaint = new Paint();
-
- private int mGravity;
- private float mCenterX;
- private float mCenterY;
- private float mRadius;
-
- public CircleView(Context context) {
- this(context, null /* attrs */);
- }
-
- public CircleView(Context context, AttributeSet attrs) {
- this(context, attrs, 0 /* defStyleAttr */);
- }
-
- public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.CircleView, defStyleAttr, 0 /* defStyleRes */);
-
- mGravity = a.getInt(R.styleable.CircleView_android_gravity, Gravity.NO_GRAVITY);
- mCenterX = a.getDimension(R.styleable.CircleView_centerX, 0.0f);
- mCenterY = a.getDimension(R.styleable.CircleView_centerY, 0.0f);
- mRadius = a.getDimension(R.styleable.CircleView_radius, 0.0f);
-
- mCirclePaint.setColor(a.getColor(R.styleable.CircleView_fillColor, Color.WHITE));
-
- a.recycle();
- }
-
- @Override
- public void onRtlPropertiesChanged(int layoutDirection) {
- super.onRtlPropertiesChanged(layoutDirection);
-
- if (mGravity != Gravity.NO_GRAVITY) {
- applyGravity(mGravity, layoutDirection);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mGravity != Gravity.NO_GRAVITY) {
- applyGravity(mGravity, getLayoutDirection());
- }
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
-
- // draw the circle, duh
- canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);
- }
-
- @Override
- public boolean hasOverlappingRendering() {
- // only if we have a background, which we shouldn't...
- return getBackground() != null;
- }
-
- /**
- * @return the current {@link Gravity} used to align/size the circle
- */
- public final int getGravity() {
- return mGravity;
- }
-
- /**
- * Describes how to align/size the circle relative to the view's bounds. Defaults to
- * {@link Gravity#NO_GRAVITY}.
- * <p/>
- * Note: using {@link #setCenterX(float)}, {@link #setCenterY(float)}, or
- * {@link #setRadius(float)} will automatically clear any conflicting gravity bits.
- *
- * @param gravity the {@link Gravity} flags to use
- * @return this object, allowing calls to methods in this class to be chained
- * @see R.styleable#CircleView_android_gravity
- */
- public CircleView setGravity(int gravity) {
- if (mGravity != gravity) {
- mGravity = gravity;
-
- if (gravity != Gravity.NO_GRAVITY && isLayoutDirectionResolved()) {
- applyGravity(gravity, getLayoutDirection());
- }
- }
- return this;
- }
-
- /**
- * @return the ARGB color used to fill the circle
- */
- public final int getFillColor() {
- return mCirclePaint.getColor();
- }
-
- /**
- * Sets the ARGB color used to fill the circle and invalidates only the affected area.
- *
- * @param color the ARGB color to use
- * @return this object, allowing calls to methods in this class to be chained
- * @see R.styleable#CircleView_fillColor
- */
- public CircleView setFillColor(int color) {
- if (mCirclePaint.getColor() != color) {
- mCirclePaint.setColor(color);
-
- // invalidate the current area
- invalidate(mCenterX, mCenterY, mRadius);
- }
- return this;
- }
-
- /**
- * Sets the x-coordinate for the center of the circle and invalidates only the affected area.
- *
- * @param centerX the x-coordinate to use, relative to the view's bounds
- * @return this object, allowing calls to methods in this class to be chained
- * @see R.styleable#CircleView_centerX
- */
- public CircleView setCenterX(float centerX) {
- final float oldCenterX = mCenterX;
- if (oldCenterX != centerX) {
- mCenterX = centerX;
-
- // invalidate the old/new areas
- invalidate(oldCenterX, mCenterY, mRadius);
- invalidate(centerX, mCenterY, mRadius);
- }
-
- // clear the horizontal gravity flags
- mGravity &= ~Gravity.HORIZONTAL_GRAVITY_MASK;
-
- return this;
- }
-
- /**
- * Sets the y-coordinate for the center of the circle and invalidates only the affected area.
- *
- * @param centerY the y-coordinate to use, relative to the view's bounds
- * @return this object, allowing calls to methods in this class to be chained
- * @see R.styleable#CircleView_centerY
- */
- public CircleView setCenterY(float centerY) {
- final float oldCenterY = mCenterY;
- if (oldCenterY != centerY) {
- mCenterY = centerY;
-
- // invalidate the old/new areas
- invalidate(mCenterX, oldCenterY, mRadius);
- invalidate(mCenterX, centerY, mRadius);
- }
-
- // clear the vertical gravity flags
- mGravity &= ~Gravity.VERTICAL_GRAVITY_MASK;
-
- return this;
- }
-
- /**
- * @return the radius of the circle
- */
- public final float getRadius() {
- return mRadius;
- }
-
- /**
- * Sets the radius of the circle and invalidates only the affected area.
- *
- * @param radius the radius to use
- * @return this object, allowing calls to methods in this class to be chained
- * @see R.styleable#CircleView_radius
- */
- public CircleView setRadius(float radius) {
- final float oldRadius = mRadius;
- if (oldRadius != radius) {
- mRadius = radius;
-
- // invalidate the old/new areas
- invalidate(mCenterX, mCenterY, oldRadius);
- if (radius > oldRadius) {
- invalidate(mCenterX, mCenterY, radius);
- }
- }
-
- // clear the fill gravity flags
- if ((mGravity & Gravity.FILL_HORIZONTAL) == Gravity.FILL_HORIZONTAL) {
- mGravity &= ~Gravity.FILL_HORIZONTAL;
- }
- if ((mGravity & Gravity.FILL_VERTICAL) == Gravity.FILL_VERTICAL) {
- mGravity &= ~Gravity.FILL_VERTICAL;
- }
-
- return this;
- }
-
- /**
- * Invalidates the rectangular area that circumscribes the circle defined by {@code centerX},
- * {@code centerY}, and {@code radius}.
- */
- private void invalidate(float centerX, float centerY, float radius) {
- invalidate((int) (centerX - radius - 0.5f), (int) (centerY - radius - 0.5f),
- (int) (centerX + radius + 0.5f), (int) (centerY + radius + 0.5f));
- }
-
- /**
- * Applies the specified {@code gravity} and {@code layoutDirection}, adjusting the alignment
- * and size of the circle depending on the resolved {@link Gravity} flags. Also invalidates the
- * affected area if necessary.
- *
- * @param gravity the {@link Gravity} the {@link Gravity} flags to use
- * @param layoutDirection the layout direction used to resolve the absolute gravity
- */
- @SuppressLint("RtlHardcoded")
- private void applyGravity(int gravity, int layoutDirection) {
- final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
-
- final float oldRadius = mRadius;
- final float oldCenterX = mCenterX;
- final float oldCenterY = mCenterY;
-
- switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
- case Gravity.LEFT:
- mCenterX = 0.0f;
- break;
- case Gravity.CENTER_HORIZONTAL:
- case Gravity.FILL_HORIZONTAL:
- mCenterX = getWidth() / 2.0f;
- break;
- case Gravity.RIGHT:
- mCenterX = getWidth();
- break;
- }
-
- switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.TOP:
- mCenterY = 0.0f;
- break;
- case Gravity.CENTER_VERTICAL:
- case Gravity.FILL_VERTICAL:
- mCenterY = getHeight() / 2.0f;
- break;
- case Gravity.BOTTOM:
- mCenterY = getHeight();
- break;
- }
-
- switch (absoluteGravity & Gravity.FILL) {
- case Gravity.FILL:
- mRadius = Math.min(getWidth(), getHeight()) / 2.0f;
- break;
- case Gravity.FILL_HORIZONTAL:
- mRadius = getWidth() / 2.0f;
- break;
- case Gravity.FILL_VERTICAL:
- mRadius = getHeight() / 2.0f;
- break;
- }
-
- if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != mRadius) {
- invalidate(oldCenterX, oldCenterY, oldRadius);
- invalidate(mCenterX, mCenterY, mRadius);
- }
- }
-}
diff --git a/src/com/android/deskclock/widget/CircleView.kt b/src/com/android/deskclock/widget/CircleView.kt
new file mode 100644
index 0000000..062a284
--- /dev/null
+++ b/src/com/android/deskclock/widget/CircleView.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.deskclock.widget
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.util.Property
+import android.view.Gravity
+import android.view.View
+
+import com.android.deskclock.R
+
+import kotlin.math.min
+
+/**
+ * A [View] that draws primitive circles.
+ */
+class CircleView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+ /** The [Paint] used to draw the circle. */
+ private val mCirclePaint = Paint()
+
+ /** the current [Gravity] used to align/size the circle */
+ var gravity: Int
+ private set
+
+ private var mCenterX: Float
+ private var mCenterY: Float
+
+ /** the radius of the circle */
+ var radius: Float
+ private set
+
+ init {
+ val a = context.obtainStyledAttributes(attrs, R.styleable.CircleView, defStyleAttr, 0)
+
+ gravity = a.getInt(R.styleable.CircleView_android_gravity, Gravity.NO_GRAVITY)
+ mCenterX = a.getDimension(R.styleable.CircleView_centerX, 0.0f)
+ mCenterY = a.getDimension(R.styleable.CircleView_centerY, 0.0f)
+ radius = a.getDimension(R.styleable.CircleView_radius, 0.0f)
+
+ mCirclePaint.color = a.getColor(R.styleable.CircleView_fillColor, Color.WHITE)
+
+ a.recycle()
+ }
+
+ override fun onRtlPropertiesChanged(layoutDirection: Int) {
+ super.onRtlPropertiesChanged(layoutDirection)
+
+ if (gravity != Gravity.NO_GRAVITY) {
+ applyGravity(gravity, layoutDirection)
+ }
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+
+ if (gravity != Gravity.NO_GRAVITY) {
+ applyGravity(gravity, layoutDirection)
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ // draw the circle, duh
+ canvas.drawCircle(mCenterX, mCenterY, radius, mCirclePaint)
+ }
+
+ override fun hasOverlappingRendering(): Boolean {
+ // only if we have a background, which we shouldn't...
+ return background != null
+ }
+
+ /**
+ * Describes how to align/size the circle relative to the view's bounds. Defaults to
+ * [Gravity.NO_GRAVITY].
+ *
+ * Note: using [.setCenterX], [.setCenterY], or
+ * [.setRadius] will automatically clear any conflicting gravity bits.
+ *
+ * @param gravity the [Gravity] flags to use
+ * @return this object, allowing calls to methods in this class to be chained
+ * @see R.styleable.CircleView_android_gravity
+ */
+ fun setGravity(gravity: Int): CircleView {
+ if (this.gravity != gravity) {
+ this.gravity = gravity
+
+ if (gravity != Gravity.NO_GRAVITY && isLayoutDirectionResolved) {
+ applyGravity(gravity, layoutDirection)
+ }
+ }
+ return this
+ }
+
+ /**
+ * @return the ARGB color used to fill the circle
+ */
+ val fillColor: Int
+ get() = mCirclePaint.color
+
+ /**
+ * Sets the ARGB color used to fill the circle and invalidates only the affected area.
+ *
+ * @param color the ARGB color to use
+ * @return this object, allowing calls to methods in this class to be chained
+ * @see R.styleable.CircleView_fillColor
+ */
+ fun setFillColor(color: Int): CircleView {
+ if (mCirclePaint.color != color) {
+ mCirclePaint.color = color
+
+ // invalidate the current area
+ invalidate(mCenterX, mCenterY, radius)
+ }
+ return this
+ }
+
+ /**
+ * Sets the x-coordinate for the center of the circle and invalidates only the affected area.
+ *
+ * @param centerX the x-coordinate to use, relative to the view's bounds
+ * @return this object, allowing calls to methods in this class to be chained
+ * @see R.styleable.CircleView_centerX
+ */
+ fun setCenterX(centerX: Float): CircleView {
+ val oldCenterX = mCenterX
+ if (oldCenterX != centerX) {
+ mCenterX = centerX
+
+ // invalidate the old/new areas
+ invalidate(oldCenterX, mCenterY, radius)
+ invalidate(centerX, mCenterY, radius)
+ }
+
+ // clear the horizontal gravity flags
+ gravity = gravity and Gravity.HORIZONTAL_GRAVITY_MASK.inv()
+
+ return this
+ }
+
+ /**
+ * Sets the y-coordinate for the center of the circle and invalidates only the affected area.
+ *
+ * @param centerY the y-coordinate to use, relative to the view's bounds
+ * @return this object, allowing calls to methods in this class to be chained
+ * @see R.styleable.CircleView_centerY
+ */
+ fun setCenterY(centerY: Float): CircleView {
+ val oldCenterY = mCenterY
+ if (oldCenterY != centerY) {
+ mCenterY = centerY
+
+ // invalidate the old/new areas
+ invalidate(mCenterX, oldCenterY, radius)
+ invalidate(mCenterX, centerY, radius)
+ }
+
+ // clear the vertical gravity flags
+ gravity = gravity and Gravity.VERTICAL_GRAVITY_MASK.inv()
+
+ return this
+ }
+
+ /**
+ * Sets the radius of the circle and invalidates only the affected area.
+ *
+ * @param radius the radius to use
+ * @return this object, allowing calls to methods in this class to be chained
+ * @see R.styleable.CircleView_radius
+ */
+ fun setRadius(radius: Float): CircleView {
+ val oldRadius = this.radius
+ if (oldRadius != radius) {
+ this.radius = radius
+
+ // invalidate the old/new areas
+ invalidate(mCenterX, mCenterY, oldRadius)
+ if (radius > oldRadius) {
+ invalidate(mCenterX, mCenterY, radius)
+ }
+ }
+
+ // clear the fill gravity flags
+ if (gravity and Gravity.FILL_HORIZONTAL == Gravity.FILL_HORIZONTAL) {
+ gravity = gravity and Gravity.FILL_HORIZONTAL.inv()
+ }
+ if (gravity and Gravity.FILL_VERTICAL == Gravity.FILL_VERTICAL) {
+ gravity = gravity and Gravity.FILL_VERTICAL.inv()
+ }
+
+ return this
+ }
+
+ /**
+ * Invalidates the rectangular area that circumscribes the circle defined by `centerX`,
+ * `centerY`, and `radius`.
+ */
+ private fun invalidate(centerX: Float, centerY: Float, radius: Float) {
+ invalidate((centerX - radius - 0.5f).toInt(), (centerY - radius - 0.5f).toInt(),
+ (centerX + radius + 0.5f).toInt(), (centerY + radius + 0.5f).toInt())
+ }
+
+ /**
+ * Applies the specified `gravity` and `layoutDirection`, adjusting the alignment
+ * and size of the circle depending on the resolved [Gravity] flags. Also invalidates the
+ * affected area if necessary.
+ *
+ * @param gravity the [Gravity] the [Gravity] flags to use
+ * @param layoutDirection the layout direction used to resolve the absolute gravity
+ */
+ @SuppressLint("RtlHardcoded")
+ private fun applyGravity(gravity: Int, layoutDirection: Int) {
+ val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
+
+ val oldRadius = radius
+ val oldCenterX = mCenterX
+ val oldCenterY = mCenterY
+
+ when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
+ Gravity.LEFT -> mCenterX = 0.0f
+ Gravity.CENTER_HORIZONTAL, Gravity.FILL_HORIZONTAL -> mCenterX = width / 2.0f
+ Gravity.RIGHT -> mCenterX = width.toFloat()
+ }
+
+ when (absoluteGravity and Gravity.VERTICAL_GRAVITY_MASK) {
+ Gravity.TOP -> mCenterY = 0.0f
+ Gravity.CENTER_VERTICAL, Gravity.FILL_VERTICAL -> mCenterY = height / 2.0f
+ Gravity.BOTTOM -> mCenterY = height.toFloat()
+ }
+
+ when (absoluteGravity and Gravity.FILL) {
+ Gravity.FILL -> radius = min(width, height) / 2.0f
+ Gravity.FILL_HORIZONTAL -> radius = width / 2.0f
+ Gravity.FILL_VERTICAL -> radius = height / 2.0f
+ }
+
+ if (oldCenterX != mCenterX || oldCenterY != mCenterY || oldRadius != radius) {
+ invalidate(oldCenterX, oldCenterY, oldRadius)
+ invalidate(mCenterX, mCenterY, radius)
+ }
+ }
+
+ companion object {
+ /**
+ * A Property wrapper around the fillColor functionality handled by the
+ * [.setFillColor] and [.getFillColor] methods.
+ */
+ @JvmField
+ val FILL_COLOR: Property<CircleView, Int> =
+ object : Property<CircleView, Int>(Int::class.java, "fillColor") {
+ override fun get(view: CircleView): Int {
+ return view.fillColor
+ }
+
+ override fun set(view: CircleView, value: Int) {
+ view.setFillColor(value)
+ }
+ }
+
+ /**
+ * A Property wrapper around the radius functionality handled by the
+ * [.setRadius] and [.getRadius] methods.
+ */
+ @JvmField
+ val RADIUS: Property<CircleView, Float> =
+ object : Property<CircleView, Float>(Float::class.java, "radius") {
+ override fun get(view: CircleView): Float {
+ return view.radius
+ }
+
+ override fun set(view: CircleView, value: Float) {
+ view.setRadius(value)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/EllipsizeLayout.java b/src/com/android/deskclock/widget/EllipsizeLayout.java
deleted file mode 100644
index 6c5dd33..0000000
--- a/src/com/android/deskclock/widget/EllipsizeLayout.java
+++ /dev/null
@@ -1,124 +0,0 @@
-package com.android.deskclock.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * When this layout is in the Horizontal orientation and one and only one child is a TextView with a
- * non-null android:ellipsize, this layout will reduce android:maxWidth of that TextView to ensure
- * the siblings are not truncated. This class is useful when that ellipsize-text-view "starts"
- * before other children of this view group. This layout has no effect if:
- * <ul>
- * <li>the orientation is not horizontal</li>
- * <li>any child has weights.</li>
- * <li>more than one child has a non-null android:ellipsize.</li>
- * </ul>
- *
- * <p>The purpose of this horizontal-linear-layout is to ensure that when the sum of widths of the
- * children are greater than this parent, the maximum width of the ellipsize-text-view, is reduced
- * so that no siblings are truncated.</p>
- *
- * <p>For example: Given Text1 has android:ellipsize="end" and Text2 has android:ellipsize="none",
- * as Text1 and/or Text2 grow in width, both will consume more width until Text2 hits the end
- * margin, then Text1 will cease to grow and instead shrink to accommodate any further growth in
- * Text2.</p>
- * <ul>
- * <li>|[text1]|[text2] |</li>
- * <li>|[text1 text1]|[text2 text2] |</li>
- * <li>|[text...]|[text2 text2 text2]|</li>
- * </ul>
- */
-public class EllipsizeLayout extends LinearLayout {
-
- @SuppressWarnings("unused")
- public EllipsizeLayout(Context context) {
- this(context, null);
- }
-
- public EllipsizeLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- /**
- * This override only acts when the LinearLayout is in the Horizontal orientation and is in it's
- * final measurement pass(MeasureSpec.EXACTLY). In this case only, this class
- * <ul>
- * <li>Identifies the one TextView child with the non-null android:ellipsize.</li>
- * <li>Re-measures the needed width of all children (by calling measureChildWithMargins with
- * the width measure specification to MeasureSpec.UNSPECIFIED.)</li>
- * <li>Sums the children's widths.</li>
- * <li>Whenever the sum of the children's widths is greater than this parent was allocated,
- * the maximum width of the one TextView child with the non-null android:ellipsize is
- * reduced.</li>
- * </ul>
- *
- * @param widthMeasureSpec horizontal space requirements as imposed by the parent
- * @param heightMeasureSpec vertical space requirements as imposed by the parent
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (getOrientation() == HORIZONTAL
- && (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY)) {
- int totalLength = 0;
- // If any of the constraints of this class are exceeded, outOfSpec becomes true
- // and the no alterations are made to the ellipsize-text-view.
- boolean outOfSpec = false;
- TextView ellipsizeView = null;
- final int count = getChildCount();
- final int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
- final int queryWidthMeasureSpec = MeasureSpec.
- makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
-
- for (int ii = 0; ii < count && !outOfSpec; ++ii) {
- final View child = getChildAt(ii);
- if (child != null && child.getVisibility() != GONE) {
- // Identify the ellipsize view
- if (child instanceof TextView) {
- final TextView tv = (TextView) child;
- if (tv.getEllipsize() != null) {
- if (ellipsizeView == null) {
- ellipsizeView = tv;
- // Clear the maximum width on ellipsizeView before measurement
- ellipsizeView.setMaxWidth(Integer.MAX_VALUE);
- } else {
- // TODO: support multiple android:ellipsize
- outOfSpec = true;
- }
- }
- }
- // Ask the child to measure itself
- measureChildWithMargins(child, queryWidthMeasureSpec, 0, heightMeasureSpec, 0);
-
- // Get the layout parameters to check for a weighted width and to add the
- // child's margins to the total length.
- final LinearLayout.LayoutParams layoutParams =
- (LinearLayout.LayoutParams) child.getLayoutParams();
- if (layoutParams != null) {
- outOfSpec |= (layoutParams.weight > 0f);
- totalLength += child.getMeasuredWidth()
- + layoutParams.leftMargin + layoutParams.rightMargin;
- } else {
- outOfSpec = true;
- }
- }
- }
- // Last constraint test
- outOfSpec |= (ellipsizeView == null) || (totalLength == 0);
-
- if (!outOfSpec && totalLength > parentWidth) {
- int maxWidth = ellipsizeView.getMeasuredWidth() - (totalLength - parentWidth);
- // TODO: Respect android:minWidth (easy with @TargetApi(16))
- final int minWidth = 0;
- if (maxWidth < minWidth) {
- maxWidth = minWidth;
- }
- ellipsizeView.setMaxWidth(maxWidth);
- }
- }
-
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-}
diff --git a/src/com/android/deskclock/widget/EllipsizeLayout.kt b/src/com/android/deskclock/widget/EllipsizeLayout.kt
new file mode 100644
index 0000000..3d31477
--- /dev/null
+++ b/src/com/android/deskclock/widget/EllipsizeLayout.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+
+/**
+ * When this layout is in the Horizontal orientation and one and only one child is a TextView with a
+ * non-null android:ellipsize, this layout will reduce android:maxWidth of that TextView to ensure
+ * the siblings are not truncated. This class is useful when that ellipsize-text-view "starts"
+ * before other children of this view group. This layout has no effect if:
+ * <ul>
+ * <li>the orientation is not horizontal</li>
+ * <li>any child has weights.</li>
+ * <li>more than one child has a non-null android:ellipsize.</li>
+ * </ul>
+ *
+ * The purpose of this horizontal-linear-layout is to ensure that when the sum of widths of the
+ * children are greater than this parent, the maximum width of the ellipsize-text-view, is reduced
+ * so that no siblings are truncated.
+ *
+ *
+ * For example: Given Text1 has android:ellipsize="end" and Text2 has android:ellipsize="none",
+ * as Text1 and/or Text2 grow in width, both will consume more width until Text2 hits the end
+ * margin, then Text1 will cease to grow and instead shrink to accommodate any further growth in
+ * Text2.
+ * <ul>
+ * <li>|[text1]|[text2] |</li>
+ * <li>|[text1 text1]|[text2 text2] |</li>
+ * <li>|[text... ]|[text2 text2 text2]|</li>
+ * </ul>
+ */
+class EllipsizeLayout @JvmOverloads constructor(
+ context: Context?,
+ attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+ /**
+ * This override only acts when the LinearLayout is in the Horizontal orientation and is in it's
+ * final measurement pass(MeasureSpec.EXACTLY). In this case only, this class
+ *
+ * * Identifies the one TextView child with the non-null android:ellipsize.
+ * * Re-measures the needed width of all children (by calling measureChildWithMargins with
+ * the width measure specification to MeasureSpec.UNSPECIFIED.)
+ * * Sums the children's widths.
+ * * Whenever the sum of the children's widths is greater than this parent was allocated,
+ * the maximum width of the one TextView child with the non-null android:ellipsize is
+ * reduced.
+ *
+ *
+ * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+ * @param heightMeasureSpec vertical space requirements as imposed by the parent
+ */
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ if (orientation == HORIZONTAL &&
+ MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ var totalLength = 0
+ // If any of the constraints of this class are exceeded, outOfSpec becomes true
+ // and the no alterations are made to the ellipsize-text-view.
+ var outOfSpec = false
+ var ellipsizeView: TextView? = null
+ val count = childCount
+ val parentWidth = MeasureSpec.getSize(widthMeasureSpec)
+ val queryWidthMeasureSpec =
+ MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.UNSPECIFIED)
+
+ var ii = 0
+ while (ii < count && !outOfSpec) {
+ val child = getChildAt(ii)
+ if (child != null && child.visibility != View.GONE) {
+ // Identify the ellipsize view
+ if (child is TextView) {
+ val tv = child
+ if (tv.ellipsize != null) {
+ if (ellipsizeView == null) {
+ ellipsizeView = tv
+ // Clear the maximum width on ellipsizeView before measurement
+ ellipsizeView.maxWidth = Int.MAX_VALUE
+ } else {
+ // TODO: support multiple android:ellipsize
+ outOfSpec = true
+ }
+ }
+ }
+ // Ask the child to measure itself
+ measureChildWithMargins(child, queryWidthMeasureSpec, 0, heightMeasureSpec, 0)
+
+ // Get the layout parameters to check for a weighted width and to add the
+ // child's margins to the total length.
+ val layoutParams = child.layoutParams as LayoutParams?
+ if (layoutParams != null) {
+ outOfSpec = outOfSpec or (layoutParams.weight > 0f)
+ totalLength += (child.measuredWidth +
+ layoutParams.leftMargin + layoutParams.rightMargin)
+ } else {
+ outOfSpec = true
+ }
+ }
+ ++ii
+ }
+ // Last constraint test
+ outOfSpec = outOfSpec or (ellipsizeView == null || totalLength == 0)
+
+ if (!outOfSpec && totalLength > parentWidth) {
+ var maxWidth = ellipsizeView!!.measuredWidth - (totalLength - parentWidth)
+ // TODO: Respect android:minWidth (easy with @TargetApi(16))
+ val minWidth = 0
+ if (maxWidth < minWidth) {
+ maxWidth = minWidth
+ }
+ ellipsizeView.maxWidth = maxWidth
+ }
+ }
+
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/EmptyViewController.java b/src/com/android/deskclock/widget/EmptyViewController.java
deleted file mode 100644
index 4a21999..0000000
--- a/src/com/android/deskclock/widget/EmptyViewController.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget;
-
-import android.transition.Fade;
-import android.transition.Transition;
-import android.transition.TransitionManager;
-import android.transition.TransitionSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.deskclock.Utils;
-
-/**
- * Controller that displays empty view and handles animation appropriately.
- */
-public final class EmptyViewController {
-
- private static final int ANIMATION_DURATION = 300;
- private static final boolean USE_TRANSITION_FRAMEWORK = Utils.isLOrLater();
-
- private final Transition mEmptyViewTransition;
- private final ViewGroup mMainLayout;
- private final View mContentView;
- private final View mEmptyView;
- private boolean mIsEmpty;
-
- /**
- * Constructor of the controller.
- *
- * @param contentView The view that should be displayed when empty view is hidden.
- * @param emptyView The view that should be displayed when main view is empty.
- */
- public EmptyViewController(ViewGroup mainLayout, View contentView, View emptyView) {
- mMainLayout = mainLayout;
- mContentView = contentView;
- mEmptyView = emptyView;
- if (USE_TRANSITION_FRAMEWORK) {
- mEmptyViewTransition = new TransitionSet()
- .setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
- .addTarget(contentView)
- .addTarget(emptyView)
- .addTransition(new Fade(Fade.OUT))
- .addTransition(new Fade(Fade.IN))
- .setDuration(ANIMATION_DURATION);
- } else {
- mEmptyViewTransition = null;
- }
- }
-
- /**
- * Sets the state for the controller. If it's empty, it will display the empty view.
- *
- * @param isEmpty Whether or not the controller should transition into empty state.
- */
- public void setEmpty(boolean isEmpty) {
- if (mIsEmpty == isEmpty) {
- return;
- }
- mIsEmpty = isEmpty;
- // State changed, perform transition.
- if (USE_TRANSITION_FRAMEWORK) {
- TransitionManager.beginDelayedTransition(mMainLayout, mEmptyViewTransition);
- }
- mEmptyView.setVisibility(mIsEmpty ? View.VISIBLE : View.GONE);
- mContentView.setVisibility(mIsEmpty ? View.GONE : View.VISIBLE);
- }
-}
diff --git a/src/com/android/deskclock/widget/EmptyViewController.kt b/src/com/android/deskclock/widget/EmptyViewController.kt
new file mode 100644
index 0000000..288cc8a
--- /dev/null
+++ b/src/com/android/deskclock/widget/EmptyViewController.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget
+
+import android.transition.Fade
+import android.transition.Transition
+import android.transition.TransitionManager
+import android.transition.TransitionSet
+import android.view.View
+import android.view.ViewGroup
+
+import com.android.deskclock.Utils
+
+/**
+ * Controller that displays empty view and handles animation appropriately.
+ *
+ * @param contentView The view that should be displayed when empty view is hidden.
+ * @param emptyView The view that should be displayed when main view is empty.
+ */
+class EmptyViewController(
+ private val mMainLayout: ViewGroup,
+ private val mContentView: View,
+ private val mEmptyView: View
+) {
+ private var mEmptyViewTransition: Transition? = null
+ private var mIsEmpty = false
+
+ init {
+ mEmptyViewTransition = if (USE_TRANSITION_FRAMEWORK) {
+ TransitionSet()
+ .setOrdering(TransitionSet.ORDERING_SEQUENTIAL)
+ .addTarget(mContentView)
+ .addTarget(mEmptyView)
+ .addTransition(Fade(Fade.OUT))
+ .addTransition(Fade(Fade.IN))
+ .setDuration(ANIMATION_DURATION.toLong())
+ } else {
+ null
+ }
+ }
+
+ /**
+ * Sets the state for the controller. If it's empty, it will display the empty view.
+ *
+ * @param isEmpty Whether or not the controller should transition into empty state.
+ */
+ fun setEmpty(isEmpty: Boolean) {
+ if (mIsEmpty == isEmpty) {
+ return
+ }
+ mIsEmpty = isEmpty
+ // State changed, perform transition.
+ if (USE_TRANSITION_FRAMEWORK) {
+ TransitionManager.beginDelayedTransition(mMainLayout, mEmptyViewTransition)
+ }
+ mEmptyView.visibility = if (mIsEmpty) View.VISIBLE else View.GONE
+ mContentView.visibility = if (mIsEmpty) View.GONE else View.VISIBLE
+ }
+
+ companion object {
+ private const val ANIMATION_DURATION = 300
+ private val USE_TRANSITION_FRAMEWORK = Utils.isLOrLater
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextSizeHelper.java b/src/com/android/deskclock/widget/TextSizeHelper.java
deleted file mode 100644
index bf37f76..0000000
--- a/src/com/android/deskclock/widget/TextSizeHelper.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget;
-
-import android.text.Layout;
-import android.text.TextPaint;
-import android.util.TypedValue;
-import android.view.View;
-import android.widget.TextView;
-
-import static java.lang.Integer.MAX_VALUE;
-
-/**
- * A TextView which automatically re-sizes its text to fit within its boundaries.
- */
-public final class TextSizeHelper {
-
- // The text view whose size this class controls.
- private final TextView mTextView;
-
- // Text paint used for measuring.
- private final TextPaint mMeasurePaint = new TextPaint();
-
- // The maximum size the text is allowed to be (in pixels).
- private float mMaxTextSize;
-
- // The maximum width the text is allowed to be (in pixels).
- private int mWidthConstraint = MAX_VALUE;
-
- // The maximum height the text is allowed to be (in pixels).
- private int mHeightConstraint = MAX_VALUE;
-
- // When {@code true} calls to {@link #requestLayout()} should be ignored.
- private boolean mIgnoreRequestLayout;
-
- public TextSizeHelper(TextView view) {
- mTextView = view;
- mMaxTextSize = view.getTextSize();
- }
-
- public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int widthConstraint = MAX_VALUE;
- if (View.MeasureSpec.getMode(widthMeasureSpec) != View.MeasureSpec.UNSPECIFIED) {
- widthConstraint = View.MeasureSpec.getSize(widthMeasureSpec)
- - mTextView.getCompoundPaddingLeft() - mTextView.getCompoundPaddingRight();
- }
-
- int heightConstraint = MAX_VALUE;
- if (View.MeasureSpec.getMode(heightMeasureSpec) != View.MeasureSpec.UNSPECIFIED) {
- heightConstraint = View.MeasureSpec.getSize(heightMeasureSpec)
- - mTextView.getCompoundPaddingTop() - mTextView.getCompoundPaddingBottom();
- }
-
- if (mTextView.isLayoutRequested() || mWidthConstraint != widthConstraint
- || mHeightConstraint != heightConstraint) {
- mWidthConstraint = widthConstraint;
- mHeightConstraint = heightConstraint;
-
- adjustTextSize();
- }
- }
-
- public void onTextChanged(int lengthBefore, int lengthAfter) {
- // The length of the text has changed, request layout to recalculate the current text
- // size. This is necessary to workaround an optimization in TextView#checkForRelayout()
- // which will avoid re-layout when the view has a fixed layout width.
- if (lengthBefore != lengthAfter) {
- mTextView.requestLayout();
- }
- }
-
- public boolean shouldIgnoreRequestLayout() {
- return mIgnoreRequestLayout;
- }
-
- private void adjustTextSize() {
- final CharSequence text = mTextView.getText();
- float textSize = mMaxTextSize;
- if (text.length() > 0 && (mWidthConstraint < MAX_VALUE || mHeightConstraint < MAX_VALUE)) {
- mMeasurePaint.set(mTextView.getPaint());
-
- float minTextSize = 1f;
- float maxTextSize = mMaxTextSize;
- while (maxTextSize >= minTextSize) {
- final float midTextSize = Math.round((maxTextSize + minTextSize) / 2f);
- mMeasurePaint.setTextSize(midTextSize);
-
- final float width = Layout.getDesiredWidth(text, mMeasurePaint);
- final float height = mMeasurePaint.getFontMetricsInt(null);
- if (width > mWidthConstraint || height > mHeightConstraint) {
- maxTextSize = midTextSize - 1f;
- } else {
- textSize = midTextSize;
- minTextSize = midTextSize + 1f;
- }
- }
- }
-
- if (mTextView.getTextSize() != textSize) {
- mIgnoreRequestLayout = true;
- mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
- mIgnoreRequestLayout = false;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextSizeHelper.kt b/src/com/android/deskclock/widget/TextSizeHelper.kt
new file mode 100644
index 0000000..6ca966f
--- /dev/null
+++ b/src/com/android/deskclock/widget/TextSizeHelper.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget
+
+import android.text.Layout
+import android.text.TextPaint
+import android.util.TypedValue
+import android.view.View.MeasureSpec
+import android.widget.TextView
+
+/**
+ * A TextView which automatically re-sizes its text to fit within its boundaries.
+ */
+class TextSizeHelper(private val mTextView: TextView) {
+
+ // Text paint used for measuring.
+ private val mMeasurePaint = TextPaint()
+
+ // The maximum size the text is allowed to be (in pixels).
+ private val mMaxTextSize: Float = mTextView.textSize
+
+ // The maximum width the text is allowed to be (in pixels).
+ private var mWidthConstraint = Int.MAX_VALUE
+
+ // The maximum height the text is allowed to be (in pixels).
+ private var mHeightConstraint = Int.MAX_VALUE
+
+ // When {@code true} calls to {@link #requestLayout()} should be ignored.
+ private var mIgnoreRequestLayout = false
+
+ fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ var widthConstraint = Int.MAX_VALUE
+ if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+ widthConstraint = (MeasureSpec.getSize(widthMeasureSpec) -
+ mTextView.compoundPaddingLeft - mTextView.compoundPaddingRight)
+ }
+
+ var heightConstraint = Int.MAX_VALUE
+ if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+ heightConstraint = (MeasureSpec.getSize(heightMeasureSpec) -
+ mTextView.compoundPaddingTop - mTextView.compoundPaddingBottom)
+ }
+
+ if (mTextView.isLayoutRequested ||
+ mWidthConstraint != widthConstraint ||
+ mHeightConstraint != heightConstraint) {
+ mWidthConstraint = widthConstraint
+ mHeightConstraint = heightConstraint
+ adjustTextSize()
+ }
+ }
+
+ fun onTextChanged(lengthBefore: Int, lengthAfter: Int) {
+ // The length of the text has changed, request layout to recalculate the current text
+ // size. This is necessary to workaround an optimization in TextView#checkForRelayout()
+ // which will avoid re-layout when the view has a fixed layout width.
+ if (lengthBefore != lengthAfter) {
+ mTextView.requestLayout()
+ }
+ }
+
+ fun shouldIgnoreRequestLayout(): Boolean {
+ return mIgnoreRequestLayout
+ }
+
+ private fun adjustTextSize() {
+ val text = mTextView.text
+ var textSize = mMaxTextSize
+ if (text.isNotEmpty() &&
+ (mWidthConstraint < Int.MAX_VALUE || mHeightConstraint < Int.MAX_VALUE)) {
+ mMeasurePaint.set(mTextView.paint)
+
+ var minTextSize = 1f
+ var maxTextSize = mMaxTextSize
+ while (maxTextSize >= minTextSize) {
+ val midTextSize = Math.round((maxTextSize + minTextSize) / 2f).toFloat()
+ mMeasurePaint.textSize = midTextSize
+
+ val width = Layout.getDesiredWidth(text, mMeasurePaint)
+ val height = mMeasurePaint.getFontMetricsInt(null).toFloat()
+ if (width > mWidthConstraint || height > mHeightConstraint) {
+ maxTextSize = midTextSize - 1f
+ } else {
+ textSize = midTextSize
+ minTextSize = midTextSize + 1f
+ }
+ }
+ }
+
+ if (mTextView.textSize != textSize) {
+ mIgnoreRequestLayout = true
+ mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
+ mIgnoreRequestLayout = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextTime.java b/src/com/android/deskclock/widget/TextTime.java
deleted file mode 100644
index 3e93124..0000000
--- a/src/com/android/deskclock/widget/TextTime.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.Handler;
-import android.provider.Settings;
-import androidx.annotation.VisibleForTesting;
-import android.text.format.DateFormat;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-import com.android.deskclock.Utils;
-import com.android.deskclock.data.DataModel;
-
-import java.util.Calendar;
-import java.util.TimeZone;
-
-import static java.util.Calendar.HOUR_OF_DAY;
-import static java.util.Calendar.MINUTE;
-
-/**
- * Based on {@link android.widget.TextClock}, This widget displays a constant time of day using
- * format specifiers. {@link android.widget.TextClock} doesn't support a non-ticking clock.
- */
-public class TextTime extends TextView {
-
- /** UTC does not have DST rules and will not alter the {@link #mHour} and {@link #mMinute}. */
- private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
-
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
- @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
- static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
-
- private CharSequence mFormat12;
- private CharSequence mFormat24;
- private CharSequence mFormat;
-
- private boolean mAttached;
-
- private int mHour;
- private int mMinute;
-
- private final ContentObserver mFormatChangeObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- chooseFormat();
- updateTime();
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- chooseFormat();
- updateTime();
- }
- };
-
- @SuppressWarnings("UnusedDeclaration")
- public TextTime(Context context) {
- this(context, null);
- }
-
- @SuppressWarnings("UnusedDeclaration")
- public TextTime(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TextTime(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- setFormat12Hour(Utils.get12ModeFormat(0.3f /* amPmRatio */, false));
- setFormat24Hour(Utils.get24ModeFormat(false));
-
- chooseFormat();
- }
-
- @SuppressWarnings("UnusedDeclaration")
- public CharSequence getFormat12Hour() {
- return mFormat12;
- }
-
- @SuppressWarnings("UnusedDeclaration")
- public void setFormat12Hour(CharSequence format) {
- mFormat12 = format;
-
- chooseFormat();
- updateTime();
- }
-
- @SuppressWarnings("UnusedDeclaration")
- public CharSequence getFormat24Hour() {
- return mFormat24;
- }
-
- @SuppressWarnings("UnusedDeclaration")
- public void setFormat24Hour(CharSequence format) {
- mFormat24 = format;
-
- chooseFormat();
- updateTime();
- }
-
- private void chooseFormat() {
- final boolean format24Requested = DataModel.getDataModel().is24HourFormat();
- if (format24Requested) {
- mFormat = mFormat24 == null ? DEFAULT_FORMAT_24_HOUR : mFormat24;
- } else {
- mFormat = mFormat12 == null ? DEFAULT_FORMAT_12_HOUR : mFormat12;
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- if (!mAttached) {
- mAttached = true;
- registerObserver();
- updateTime();
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mAttached) {
- unregisterObserver();
- mAttached = false;
- }
- }
-
- private void registerObserver() {
- final ContentResolver resolver = getContext().getContentResolver();
- resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver);
- }
-
- private void unregisterObserver() {
- final ContentResolver resolver = getContext().getContentResolver();
- resolver.unregisterContentObserver(mFormatChangeObserver);
- }
-
- public void setTime(int hour, int minute) {
- mHour = hour;
- mMinute = minute;
- updateTime();
- }
-
- private void updateTime() {
- // Format the time relative to UTC to ensure hour and minute are not adjusted for DST.
- final Calendar calendar = DataModel.getDataModel().getCalendar();
- calendar.setTimeZone(UTC);
- calendar.set(HOUR_OF_DAY, mHour);
- calendar.set(MINUTE, mMinute);
- final CharSequence text = DateFormat.format(mFormat, calendar);
- setText(text);
- // Strip away the spans from text so talkback is not confused
- setContentDescription(text.toString());
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/TextTime.kt b/src/com/android/deskclock/widget/TextTime.kt
new file mode 100644
index 0000000..8f2be6c
--- /dev/null
+++ b/src/com/android/deskclock/widget/TextTime.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget
+
+import android.content.Context
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.text.format.DateFormat
+import android.util.AttributeSet
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+
+import com.android.deskclock.Utils
+import com.android.deskclock.data.DataModel
+
+import java.util.Calendar
+import java.util.TimeZone
+
+/**
+ * Based on [android.widget.TextClock], This widget displays a constant time of day using
+ * format specifiers. [android.widget.TextClock] doesn't support a non-ticking clock.
+ */
+class TextTime @JvmOverloads constructor(
+ context: Context?,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : TextView(context, attrs, defStyle) {
+ private var mFormat12: CharSequence? = Utils.get12ModeFormat(0.3f, false)
+ private var mFormat24: CharSequence? = Utils.get24ModeFormat(false)
+ private var mFormat: CharSequence? = null
+
+ private var mAttached = false
+
+ private var mHour = 0
+ private var mMinute = 0
+
+ private val mFormatChangeObserver: ContentObserver =
+ object : ContentObserver(Handler(Looper.myLooper()!!)) {
+ override fun onChange(selfChange: Boolean) {
+ chooseFormat()
+ updateTime()
+ }
+
+ override fun onChange(selfChange: Boolean, uri: Uri?) {
+ chooseFormat()
+ updateTime()
+ }
+ }
+
+ var format12Hour: CharSequence?
+ get() = mFormat12
+ set(format) {
+ mFormat12 = format
+ chooseFormat()
+ updateTime()
+ }
+
+ var format24Hour: CharSequence?
+ get() = mFormat24
+ set(format) {
+ mFormat24 = format
+ chooseFormat()
+ updateTime()
+ }
+
+ init {
+ chooseFormat()
+ }
+
+ private fun chooseFormat() {
+ val format24Requested: Boolean = DataModel.dataModel.is24HourFormat()
+ mFormat = if (format24Requested) {
+ mFormat24 ?: DEFAULT_FORMAT_24_HOUR
+ } else {
+ mFormat12 ?: DEFAULT_FORMAT_12_HOUR
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (!mAttached) {
+ mAttached = true
+ registerObserver()
+ updateTime()
+ }
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ if (mAttached) {
+ unregisterObserver()
+ mAttached = false
+ }
+ }
+
+ private fun registerObserver() {
+ val resolver = context.contentResolver
+ resolver.registerContentObserver(Settings.System.CONTENT_URI, true, mFormatChangeObserver)
+ }
+
+ private fun unregisterObserver() {
+ val resolver = context.contentResolver
+ resolver.unregisterContentObserver(mFormatChangeObserver)
+ }
+
+ fun setTime(hour: Int, minute: Int) {
+ mHour = hour
+ mMinute = minute
+ updateTime()
+ }
+
+ private fun updateTime() {
+ // Format the time relative to UTC to ensure hour and minute are not adjusted for DST.
+ val calendar: Calendar = DataModel.dataModel.calendar
+ calendar.timeZone = UTC
+ calendar[Calendar.HOUR_OF_DAY] = mHour
+ calendar[Calendar.MINUTE] = mMinute
+ val text = DateFormat.format(mFormat, calendar)
+ setText(text)
+ // Strip away the spans from text so talkback is not confused
+ contentDescription = text.toString()
+ }
+
+ companion object {
+ /** UTC does not have DST rules and will not alter the [.mHour] and [.mMinute]. */
+ private val UTC = TimeZone.getTimeZone("UTC")
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ val DEFAULT_FORMAT_12_HOUR: CharSequence = "h:mm a"
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ val DEFAULT_FORMAT_24_HOUR: CharSequence = "H:mm"
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/selector/AlarmSelection.java b/src/com/android/deskclock/widget/selector/AlarmSelection.java
deleted file mode 100644
index 0cddc1d..0000000
--- a/src/com/android/deskclock/widget/selector/AlarmSelection.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.widget.selector;
-
-import com.android.deskclock.provider.Alarm;
-
-public class AlarmSelection {
- private final String mLabel;
- private final Alarm mAlarm;
-
- /**
- * Created a new selectable item with a visual label and an id.
- * id corresponds to the Alarm id
- */
- public AlarmSelection(String label, Alarm alarm) {
- mLabel = label;
- mAlarm = alarm;
- }
-
- public String getLabel() {
- return mLabel;
- }
-
- public Alarm getAlarm() {
- return mAlarm;
- }
-}
diff --git a/src/com/android/deskclock/data/CityListener.java b/src/com/android/deskclock/widget/selector/AlarmSelection.kt
similarity index 64%
copy from src/com/android/deskclock/data/CityListener.java
copy to src/com/android/deskclock/widget/selector/AlarmSelection.kt
index 91f66b3..bee3285 100644
--- a/src/com/android/deskclock/data/CityListener.java
+++ b/src/com/android/deskclock/widget/selector/AlarmSelection.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package com.android.deskclock.data;
+package com.android.deskclock.widget.selector
-import java.util.List;
+import com.android.deskclock.provider.Alarm
/**
- * The interface through which interested parties are notified of changes to the world cities list.
+ * Created a new selectable item with a visual label and an id.
+ * id corresponds to the Alarm id
*/
-public interface CityListener {
- void citiesChanged(List<City> oldCities, List<City> newCities);
-}
\ No newline at end of file
+class AlarmSelection(val label: String, val alarm: Alarm)
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java
deleted file mode 100644
index 1361e72..0000000
--- a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.deskclock.widget.selector;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.TextView;
-
-import com.android.deskclock.R;
-import com.android.deskclock.data.DataModel;
-import com.android.deskclock.data.Weekdays;
-import com.android.deskclock.provider.Alarm;
-import com.android.deskclock.widget.TextTime;
-
-import java.util.Calendar;
-import java.util.List;
-
-public class AlarmSelectionAdapter extends ArrayAdapter<AlarmSelection> {
-
- public AlarmSelectionAdapter(Context context, int id, List<AlarmSelection> alarms) {
- super(context, id, alarms);
- }
-
- @Override
- public @NonNull View getView(int position, @Nullable View convertView,
- @NonNull ViewGroup parent) {
- final Context context = getContext();
- View row = convertView;
- if (row == null) {
- final LayoutInflater inflater = LayoutInflater.from(context);
- row = inflater.inflate(R.layout.alarm_row, parent, false);
- }
-
- final AlarmSelection selection = getItem(position);
- final Alarm alarm = selection.getAlarm();
-
- final TextTime alarmTime = (TextTime) row.findViewById(R.id.digital_clock);
- alarmTime.setTime(alarm.hour, alarm.minutes);
-
- final TextView alarmLabel = (TextView) row.findViewById(R.id.label);
- alarmLabel.setText(alarm.label);
-
- // find days when alarm is firing
- final String daysOfWeek;
- if (!alarm.daysOfWeek.isRepeating()) {
- daysOfWeek = Alarm.isTomorrow(alarm, Calendar.getInstance()) ?
- context.getResources().getString(R.string.alarm_tomorrow) :
- context.getResources().getString(R.string.alarm_today);
- } else {
- final Weekdays.Order weekdayOrder = DataModel.getDataModel().getWeekdayOrder();
- daysOfWeek = alarm.daysOfWeek.toString(context, weekdayOrder);
- }
-
- final TextView daysOfWeekView = (TextView) row.findViewById(R.id.daysOfWeek);
- daysOfWeekView.setText(daysOfWeek);
-
- return row;
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.kt b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.kt
new file mode 100644
index 0000000..8858d64
--- /dev/null
+++ b/src/com/android/deskclock/widget/selector/AlarmSelectionAdapter.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.deskclock.widget.selector
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.TextView
+
+import com.android.deskclock.R
+import com.android.deskclock.data.DataModel
+import com.android.deskclock.data.Weekdays
+import com.android.deskclock.provider.Alarm
+import com.android.deskclock.widget.TextTime
+
+import java.util.Calendar
+
+class AlarmSelectionAdapter(
+ context: Context,
+ id: Int,
+ alarms: List<AlarmSelection>
+) : ArrayAdapter<AlarmSelection?>(context, id, alarms) {
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val context = context
+ var row = convertView
+ if (row == null) {
+ val inflater = LayoutInflater.from(context)
+ row = inflater.inflate(R.layout.alarm_row, parent, false)
+ }
+
+ val selection = getItem(position)
+ val alarm = selection?.alarm
+
+ val alarmTime = row!!.findViewById<View>(R.id.digital_clock) as TextTime
+ alarmTime.setTime(alarm!!.hour, alarm.minutes)
+
+ val alarmLabel = row.findViewById<View>(R.id.label) as TextView
+ alarmLabel.text = alarm.label
+
+ // find days when alarm is firing
+ val daysOfWeek: String
+ daysOfWeek = if (!alarm.daysOfWeek.isRepeating) {
+ if (Alarm.isTomorrow(alarm, Calendar.getInstance())) {
+ context.resources.getString(R.string.alarm_tomorrow)
+ } else {
+ context.resources.getString(R.string.alarm_today)
+ }
+ } else {
+ val weekdayOrder: Weekdays.Order = DataModel.dataModel.weekdayOrder
+ alarm.daysOfWeek.toString(context, weekdayOrder)
+ }
+
+ val daysOfWeekView = row.findViewById<View>(R.id.daysOfWeek) as TextView
+ daysOfWeekView.text = daysOfWeek
+
+ return row
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/SnackbarManager.java b/src/com/android/deskclock/widget/toast/SnackbarManager.java
deleted file mode 100644
index 95d91b2..0000000
--- a/src/com/android/deskclock/widget/toast/SnackbarManager.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget.toast;
-
-import com.google.android.material.snackbar.Snackbar;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Manages visibility of Snackbar and allow preemptive dismiss of current displayed Snackbar.
- */
-public final class SnackbarManager {
-
- private static WeakReference<Snackbar> sSnackbar = null;
-
- private SnackbarManager() {}
-
- public static void show(Snackbar snackbar) {
- sSnackbar = new WeakReference<>(snackbar);
- snackbar.show();
- }
-
- public static void dismiss() {
- final Snackbar snackbar = sSnackbar == null ? null : sSnackbar.get();
- if (snackbar != null) {
- snackbar.dismiss();
- sSnackbar = null;
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/SnackbarManager.kt b/src/com/android/deskclock/widget/toast/SnackbarManager.kt
new file mode 100644
index 0000000..0d242f2
--- /dev/null
+++ b/src/com/android/deskclock/widget/toast/SnackbarManager.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget.toast
+
+import com.google.android.material.snackbar.Snackbar
+
+import java.lang.ref.WeakReference
+
+/**
+ * Manages visibility of Snackbar and allow preemptive dismiss of current displayed Snackbar.
+ */
+object SnackbarManager {
+ private var sSnackbar: WeakReference<Snackbar>? = null
+
+ @JvmStatic
+ fun show(snackbar: Snackbar) {
+ sSnackbar = WeakReference<Snackbar>(snackbar)
+ snackbar.show()
+ }
+
+ @JvmStatic
+ fun dismiss() {
+ val snackbar: Snackbar? = sSnackbar?.get()
+ if (snackbar != null) {
+ snackbar.dismiss()
+ sSnackbar = null
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.java b/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.java
deleted file mode 100644
index 84cbcff..0000000
--- a/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget.toast;
-
-import android.content.Context;
-import androidx.annotation.Keep;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import com.google.android.material.snackbar.Snackbar;
-import android.util.AttributeSet;
-import android.view.View;
-
-/**
- * Custom {@link CoordinatorLayout.Behavior} that slides with the {@link Snackbar}.
- */
-@Keep
-public final class SnackbarSlidingBehavior extends CoordinatorLayout.Behavior<View> {
-
- public SnackbarSlidingBehavior(Context context, AttributeSet attrs) {
- }
-
- @Override
- public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
- return dependency instanceof Snackbar.SnackbarLayout;
- }
-
- @Override
- public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
- updateTranslationY(parent, child);
- return false;
- }
-
- @Override
- public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
- updateTranslationY(parent, child);
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
- updateTranslationY(parent, child);
- return false;
- }
-
- private void updateTranslationY(CoordinatorLayout parent, View child) {
- float translationY = 0f;
- for (View dependency : parent.getDependencies(child)) {
- translationY = Math.min(translationY, dependency.getY() - child.getBottom());
- }
- child.setTranslationY(translationY);
- }
-}
diff --git a/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.kt b/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.kt
new file mode 100644
index 0000000..4ebfc7c
--- /dev/null
+++ b/src/com/android/deskclock/widget/toast/SnackbarSlidingBehavior.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget.toast
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.annotation.Keep
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+
+import com.google.android.material.snackbar.Snackbar
+
+import kotlin.math.min
+
+/**
+ * Custom [CoordinatorLayout.Behavior] that slides with the [Snackbar].
+ */
+@Keep
+class SnackbarSlidingBehavior(
+ context: Context?,
+ attrs: AttributeSet?
+) : CoordinatorLayout.Behavior<View?>() {
+ override fun layoutDependsOn(
+ parent: CoordinatorLayout,
+ child: View,
+ dependency: View
+ ): Boolean {
+ return dependency is Snackbar.SnackbarLayout
+ }
+
+ override fun onDependentViewChanged(
+ parent: CoordinatorLayout,
+ child: View,
+ dependency: View
+ ): Boolean {
+ updateTranslationY(parent, child)
+ return false
+ }
+
+ override fun onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) {
+ updateTranslationY(parent, child)
+ }
+
+ override fun onLayoutChild(
+ parent: CoordinatorLayout,
+ child: View,
+ layoutDirection: Int
+ ): Boolean {
+ updateTranslationY(parent, child)
+ return false
+ }
+
+ private fun updateTranslationY(parent: CoordinatorLayout, child: View) {
+ var translationY = 0f
+ for (dependency in parent.getDependencies(child)) {
+ translationY = min(translationY, dependency.y - child.bottom)
+ }
+ child.translationY = translationY
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/deskclock/widget/toast/ToastManager.java b/src/com/android/deskclock/widget/toast/ToastManager.java
deleted file mode 100644
index 09be715..0000000
--- a/src/com/android/deskclock/widget/toast/ToastManager.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.widget.toast;
-
-import android.widget.Toast;
-
-public final class ToastManager {
-
- private static Toast sToast = null;
-
- private ToastManager() {
-
- }
-
- public static void setToast(Toast toast) {
- if (sToast != null)
- sToast.cancel();
- sToast = toast;
- }
-
- public static void cancelToast() {
- if (sToast != null)
- sToast.cancel();
- sToast = null;
- }
-
-}
diff --git a/src/com/android/deskclock/ringtone/SystemRingtoneHolder.java b/src/com/android/deskclock/widget/toast/ToastManager.kt
similarity index 60%
rename from src/com/android/deskclock/ringtone/SystemRingtoneHolder.java
rename to src/com/android/deskclock/widget/toast/ToastManager.kt
index ff531ff..49ef9b7 100644
--- a/src/com/android/deskclock/ringtone/SystemRingtoneHolder.java
+++ b/src/com/android/deskclock/widget/toast/ToastManager.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,18 +14,22 @@
* limitations under the License.
*/
-package com.android.deskclock.ringtone;
+package com.android.deskclock.widget.toast
-import android.net.Uri;
+import android.widget.Toast
-final class SystemRingtoneHolder extends RingtoneHolder {
+object ToastManager {
+ private var sToast: Toast? = null
- SystemRingtoneHolder(Uri uri, String name) {
- super(uri, name);
+ @JvmStatic
+ fun setToast(toast: Toast) {
+ sToast?.cancel()
+ sToast = toast
}
- @Override
- public int getItemViewType() {
- return RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND;
+ @JvmStatic
+ fun cancelToast() {
+ sToast?.cancel()
+ sToast = null
}
}
\ No newline at end of file
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.java b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
deleted file mode 100644
index d8954d0..0000000
--- a/src/com/android/deskclock/worldclock/CitySelectionActivity.java
+++ /dev/null
@@ -1,650 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.worldclock;
-
-import android.content.Context;
-import android.os.Bundle;
-import androidx.appcompat.widget.SearchView;
-import android.text.TextUtils;
-import android.text.format.DateFormat;
-import android.util.ArraySet;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.ListView;
-import android.widget.SectionIndexer;
-import android.widget.TextView;
-
-import com.android.deskclock.BaseActivity;
-import com.android.deskclock.DropShadowController;
-import com.android.deskclock.R;
-import com.android.deskclock.Utils;
-import com.android.deskclock.actionbarmenu.MenuItemController;
-import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
-import com.android.deskclock.actionbarmenu.NavUpMenuItemController;
-import com.android.deskclock.actionbarmenu.OptionsMenuManager;
-import com.android.deskclock.actionbarmenu.SearchMenuItemController;
-import com.android.deskclock.actionbarmenu.SettingsMenuItemController;
-import com.android.deskclock.data.City;
-import com.android.deskclock.data.DataModel;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.TimeZone;
-
-import static android.view.Menu.NONE;
-
-/**
- * This activity allows the user to alter the cities selected for display.
- * <p/>
- * Note, it is possible for two instances of this Activity to exist simultaneously:
- * <p/>
- * <ul>
- * <li>Clock Tab-> Tap Floating Action Button</li>
- * <li>Digital Widget -> Tap any city clock</li>
- * </ul>
- * <p/>
- * As a result, {@link #onResume()} conservatively refreshes itself from the backing
- * {@link DataModel} which may have changed since this activity was last displayed.
- */
-public final class CitySelectionActivity extends BaseActivity {
-
- /**
- * The list of all selected and unselected cities, indexed and possibly filtered.
- */
- private ListView mCitiesList;
-
- /**
- * The adapter that presents all of the selected and unselected cities.
- */
- private CityAdapter mCitiesAdapter;
-
- /**
- * Manages all action bar menu display and click handling.
- */
- private final OptionsMenuManager mOptionsMenuManager = new OptionsMenuManager();
-
- /**
- * Menu item controller for search view.
- */
- private SearchMenuItemController mSearchMenuItemController;
-
- /**
- * The controller that shows the drop shadow when content is not scrolled to the top.
- */
- private DropShadowController mDropShadowController;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.cities_activity);
- mSearchMenuItemController =
- new SearchMenuItemController(getSupportActionBar().getThemedContext(),
- new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String query) {
- mCitiesAdapter.filter(query);
- updateFastScrolling();
- return true;
- }
- }, savedInstanceState);
- mCitiesAdapter = new CityAdapter(this, mSearchMenuItemController);
- mOptionsMenuManager.addMenuItemController(new NavUpMenuItemController(this))
- .addMenuItemController(mSearchMenuItemController)
- .addMenuItemController(new SortOrderMenuItemController())
- .addMenuItemController(new SettingsMenuItemController(this))
- .addMenuItemController(MenuItemControllerFactory.getInstance()
- .buildMenuItemControllers(this));
- mCitiesList = (ListView) findViewById(R.id.cities_list);
- mCitiesList.setAdapter(mCitiesAdapter);
-
- updateFastScrolling();
- }
-
- @Override
- public void onSaveInstanceState(Bundle bundle) {
- super.onSaveInstanceState(bundle);
- mSearchMenuItemController.saveInstance(bundle);
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- // Recompute the contents of the adapter before displaying on screen.
- mCitiesAdapter.refresh();
-
- final View dropShadow = findViewById(R.id.drop_shadow);
- mDropShadowController = new DropShadowController(dropShadow, mCitiesList);
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- mDropShadowController.stop();
-
- // Save the selected cities.
- DataModel.getDataModel().setSelectedCities(mCitiesAdapter.getSelectedCities());
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- mOptionsMenuManager.onCreateOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- mOptionsMenuManager.onPrepareOptionsMenu(menu);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- return mOptionsMenuManager.onOptionsItemSelected(item)
- || super.onOptionsItemSelected(item);
- }
-
- /**
- * Fast scrolling is only enabled while no filtering is happening.
- */
- private void updateFastScrolling() {
- final boolean enabled = !mCitiesAdapter.isFiltering();
- mCitiesList.setFastScrollAlwaysVisible(enabled);
- mCitiesList.setFastScrollEnabled(enabled);
- }
-
- /**
- * This adapter presents data in 2 possible modes. If selected cities exist the format is:
- * <p/>
- * <pre>
- * Selected Cities
- * City 1 (alphabetically first)
- * City 2 (alphabetically second)
- * ...
- * A City A1 (alphabetically first starting with A)
- * City A2 (alphabetically second starting with A)
- * ...
- * B City B1 (alphabetically first starting with B)
- * City B2 (alphabetically second starting with B)
- * ...
- * </pre>
- * <p/>
- * If selected cities do not exist, that section is removed and all that remains is:
- * <p/>
- * <pre>
- * A City A1 (alphabetically first starting with A)
- * City A2 (alphabetically second starting with A)
- * ...
- * B City B1 (alphabetically first starting with B)
- * City B2 (alphabetically second starting with B)
- * ...
- * </pre>
- */
- private static final class CityAdapter extends BaseAdapter implements View.OnClickListener,
- CompoundButton.OnCheckedChangeListener, SectionIndexer {
-
- /**
- * The type of the single optional "Selected Cities" header entry.
- */
- private static final int VIEW_TYPE_SELECTED_CITIES_HEADER = 0;
-
- /**
- * The type of each city entry.
- */
- private static final int VIEW_TYPE_CITY = 1;
-
- private final Context mContext;
-
- private final LayoutInflater mInflater;
-
- /**
- * The 12-hour time pattern for the current locale.
- */
- private final String mPattern12;
-
- /**
- * The 24-hour time pattern for the current locale.
- */
- private final String mPattern24;
-
- /**
- * {@code true} time should honor {@link #mPattern24}; {@link #mPattern12} otherwise.
- */
- private boolean mIs24HoursMode;
-
- /**
- * A calendar used to format time in a particular timezone.
- */
- private final Calendar mCalendar;
-
- /**
- * The list of cities which may be filtered by a search term.
- */
- private List<City> mFilteredCities = Collections.emptyList();
-
- /**
- * A mutable set of cities currently selected by the user.
- */
- private final Set<City> mUserSelectedCities = new ArraySet<>();
-
- /**
- * The number of user selections at the top of the adapter to avoid indexing.
- */
- private int mOriginalUserSelectionCount;
-
- /**
- * The precomputed section headers.
- */
- private String[] mSectionHeaders;
-
- /**
- * The corresponding location of each precomputed section header.
- */
- private Integer[] mSectionHeaderPositions;
-
- /**
- * Menu item controller for search. Search query is maintained here.
- */
- private final SearchMenuItemController mSearchMenuItemController;
-
- public CityAdapter(Context context, SearchMenuItemController searchMenuItemController) {
- mContext = context;
- mSearchMenuItemController = searchMenuItemController;
- mInflater = LayoutInflater.from(context);
-
- mCalendar = Calendar.getInstance();
- mCalendar.setTimeInMillis(System.currentTimeMillis());
-
- final Locale locale = Locale.getDefault();
- mPattern24 = DateFormat.getBestDateTimePattern(locale, "Hm");
-
- String pattern12 = DateFormat.getBestDateTimePattern(locale, "hma");
- if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
- // There's an RTL layout bug that causes jank when fast-scrolling through
- // the list in 12-hour mode in an RTL locale. We can work around this by
- // ensuring the strings are the same length by using "hh" instead of "h".
- pattern12 = pattern12.replaceAll("h", "hh");
- }
- mPattern12 = pattern12;
- }
-
- @Override
- public int getCount() {
- final int headerCount = hasHeader() ? 1 : 0;
- return headerCount + mFilteredCities.size();
- }
-
- @Override
- public City getItem(int position) {
- if (hasHeader()) {
- final int itemViewType = getItemViewType(position);
- switch (itemViewType) {
- case VIEW_TYPE_SELECTED_CITIES_HEADER:
- return null;
- case VIEW_TYPE_CITY:
- return mFilteredCities.get(position - 1);
- }
- throw new IllegalStateException("unexpected item view type: " + itemViewType);
- }
-
- return mFilteredCities.get(position);
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public View getView(int position, View view, ViewGroup parent) {
- final int itemViewType = getItemViewType(position);
- switch (itemViewType) {
- case VIEW_TYPE_SELECTED_CITIES_HEADER:
- if (view == null) {
- view = mInflater.inflate(R.layout.city_list_header, parent, false);
- }
- return view;
-
- case VIEW_TYPE_CITY:
- final City city = getItem(position);
- if (city == null) {
- throw new IllegalStateException("The desired city does not exist");
- }
- final TimeZone timeZone = city.getTimeZone();
-
- // Inflate a new view if necessary.
- if (view == null) {
- view = mInflater.inflate(R.layout.city_list_item, parent, false);
- final TextView index = (TextView) view.findViewById(R.id.index);
- final TextView name = (TextView) view.findViewById(R.id.city_name);
- final TextView time = (TextView) view.findViewById(R.id.city_time);
- final CheckBox selected = (CheckBox) view.findViewById(R.id.city_onoff);
- view.setTag(new CityItemHolder(index, name, time, selected));
- }
-
- // Bind data into the child views.
- final CityItemHolder holder = (CityItemHolder) view.getTag();
- holder.selected.setTag(city);
- holder.selected.setChecked(mUserSelectedCities.contains(city));
- holder.selected.setContentDescription(city.getName());
- holder.selected.setOnCheckedChangeListener(this);
- holder.name.setText(city.getName(), TextView.BufferType.SPANNABLE);
- holder.time.setText(getTimeCharSequence(timeZone));
-
- final boolean showIndex = getShowIndex(position);
- holder.index.setVisibility(showIndex ? View.VISIBLE : View.INVISIBLE);
- if (showIndex) {
- switch (getCitySort()) {
- case NAME:
- holder.index.setText(city.getIndexString());
- holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
- break;
-
- case UTC_OFFSET:
- holder.index.setText(Utils.getGMTHourOffset(timeZone, false));
- holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
- break;
- }
- }
-
- // skip checkbox and other animations
- view.jumpDrawablesToCurrentState();
- view.setOnClickListener(this);
- return view;
- }
-
- throw new IllegalStateException("unexpected item view type: " + itemViewType);
- }
-
- @Override
- public int getViewTypeCount() {
- return 2;
- }
-
- @Override
- public int getItemViewType(int position) {
- return hasHeader() && position == 0 ? VIEW_TYPE_SELECTED_CITIES_HEADER : VIEW_TYPE_CITY;
- }
-
- @Override
- public void onCheckedChanged(CompoundButton b, boolean checked) {
- final City city = (City) b.getTag();
- if (checked) {
- mUserSelectedCities.add(city);
- b.announceForAccessibility(mContext.getString(R.string.city_checked,
- city.getName()));
- } else {
- mUserSelectedCities.remove(city);
- b.announceForAccessibility(mContext.getString(R.string.city_unchecked,
- city.getName()));
- }
- }
-
- @Override
- public void onClick(View v) {
- final CheckBox b = (CheckBox) v.findViewById(R.id.city_onoff);
- b.setChecked(!b.isChecked());
- }
-
- @Override
- public Object[] getSections() {
- if (mSectionHeaders == null) {
- // Make an educated guess at the expected number of sections.
- final int approximateSectionCount = getCount() / 5;
- final List<String> sections = new ArrayList<>(approximateSectionCount);
- final List<Integer> positions = new ArrayList<>(approximateSectionCount);
-
- // Add a section for the "Selected Cities" header if it exists.
- if (hasHeader()) {
- sections.add("+");
- positions.add(0);
- }
-
- for (int position = 0; position < getCount(); position++) {
- // Add a section if this position should show the section index.
- if (getShowIndex(position)) {
- final City city = getItem(position);
- if (city == null) {
- throw new IllegalStateException("The desired city does not exist");
- }
- switch (getCitySort()) {
- case NAME:
- sections.add(city.getIndexString());
- break;
- case UTC_OFFSET:
- final TimeZone timezone = city.getTimeZone();
- sections.add(Utils.getGMTHourOffset(timezone, Utils.isPreL()));
- break;
- }
- positions.add(position);
- }
- }
-
- mSectionHeaders = sections.toArray(new String[sections.size()]);
- mSectionHeaderPositions = positions.toArray(new Integer[positions.size()]);
- }
- return mSectionHeaders;
- }
-
- @Override
- public int getPositionForSection(int sectionIndex) {
- return getSections().length == 0 ? 0 : mSectionHeaderPositions[sectionIndex];
- }
-
- @Override
- public int getSectionForPosition(int position) {
- if (getSections().length == 0) {
- return 0;
- }
-
- for (int i = 0; i < mSectionHeaderPositions.length - 2; i++) {
- if (position < mSectionHeaderPositions[i]) continue;
- if (position >= mSectionHeaderPositions[i + 1]) continue;
-
- return i;
- }
-
- return mSectionHeaderPositions.length - 1;
- }
-
- /**
- * Clear the section headers to force them to be recomputed if they are now stale.
- */
- private void clearSectionHeaders() {
- mSectionHeaders = null;
- mSectionHeaderPositions = null;
- }
-
- /**
- * Rebuilds all internal data structures from scratch.
- */
- private void refresh() {
- // Update the 12/24 hour mode.
- mIs24HoursMode = DateFormat.is24HourFormat(mContext);
-
- // Refresh the user selections.
- final List<City> selected = DataModel.getDataModel().getSelectedCities();
- mUserSelectedCities.clear();
- mUserSelectedCities.addAll(selected);
- mOriginalUserSelectionCount = selected.size();
-
- // Recompute section headers.
- clearSectionHeaders();
-
- // Recompute filtered cities.
- filter(mSearchMenuItemController.getQueryText());
- }
-
- /**
- * Filter the cities using the given {@code queryText}.
- */
- private void filter(String queryText) {
- mSearchMenuItemController.setQueryText(queryText);
- final String query = City.removeSpecialCharacters(queryText.toUpperCase());
-
- // Compute the filtered list of cities.
- final List<City> filteredCities;
- if (TextUtils.isEmpty(query)) {
- filteredCities = DataModel.getDataModel().getAllCities();
- } else {
- final List<City> unselected = DataModel.getDataModel().getUnselectedCities();
- filteredCities = new ArrayList<>(unselected.size());
- for (City city : unselected) {
- if (city.matches(query)) {
- filteredCities.add(city);
- }
- }
- }
-
- // Swap in the filtered list of cities and notify of the data change.
- mFilteredCities = filteredCities;
- notifyDataSetChanged();
- }
-
- private boolean isFiltering() {
- return !TextUtils.isEmpty(mSearchMenuItemController.getQueryText().trim());
- }
-
- private Collection<City> getSelectedCities() {
- return mUserSelectedCities;
- }
-
- private boolean hasHeader() {
- return !isFiltering() && mOriginalUserSelectionCount > 0;
- }
-
- private DataModel.CitySort getCitySort() {
- return DataModel.getDataModel().getCitySort();
- }
-
- private Comparator<City> getCitySortComparator() {
- return DataModel.getDataModel().getCityIndexComparator();
- }
-
- private CharSequence getTimeCharSequence(TimeZone timeZone) {
- mCalendar.setTimeZone(timeZone);
- return DateFormat.format(mIs24HoursMode ? mPattern24 : mPattern12, mCalendar);
- }
-
- private boolean getShowIndex(int position) {
- // Indexes are never displayed on filtered cities.
- if (isFiltering()) {
- return false;
- }
-
- if (hasHeader()) {
- // None of the original user selections should show their index.
- if (position <= mOriginalUserSelectionCount) {
- return false;
- }
-
- // The first item after the original user selections must always show its index.
- if (position == mOriginalUserSelectionCount + 1) {
- return true;
- }
- } else {
- // None of the original user selections should show their index.
- if (position < mOriginalUserSelectionCount) {
- return false;
- }
-
- // The first item after the original user selections must always show its index.
- if (position == mOriginalUserSelectionCount) {
- return true;
- }
- }
-
- // Otherwise compare the city with its predecessor to test if it is a header.
- final City priorCity = getItem(position - 1);
- final City city = getItem(position);
- return getCitySortComparator().compare(priorCity, city) != 0;
- }
-
- /**
- * Cache the child views of each city item view.
- */
- private static final class CityItemHolder {
-
- private final TextView index;
- private final TextView name;
- private final TextView time;
- private final CheckBox selected;
-
- public CityItemHolder(TextView index, TextView name, TextView time, CheckBox selected) {
- this.index = index;
- this.name = name;
- this.time = time;
- this.selected = selected;
- }
- }
- }
-
- private final class SortOrderMenuItemController implements MenuItemController {
-
- private static final int SORT_MENU_RES_ID = R.id.menu_item_sort;
-
- @Override
- public int getId() {
- return SORT_MENU_RES_ID;
- }
-
- @Override
- public void onCreateOptionsItem(Menu menu) {
- menu.add(NONE, R.id.menu_item_sort, NONE, R.string.menu_item_sort_by_gmt_offset)
- .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- }
-
- @Override
- public void onPrepareOptionsItem(MenuItem item) {
- item.setTitle(DataModel.getDataModel().getCitySort() == DataModel.CitySort.NAME
- ? R.string.menu_item_sort_by_gmt_offset : R.string.menu_item_sort_by_name);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // Save the new sort order.
- DataModel.getDataModel().toggleCitySort();
-
- // Section headers are influenced by sort order and must be cleared.
- mCitiesAdapter.clearSectionHeaders();
-
- // Honor the new sort order in the adapter.
- mCitiesAdapter.filter(mSearchMenuItemController.getQueryText());
- return true;
- }
- }
-}
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.kt b/src/com/android/deskclock/worldclock/CitySelectionActivity.kt
new file mode 100644
index 0000000..998faa5
--- /dev/null
+++ b/src/com/android/deskclock/worldclock/CitySelectionActivity.kt
@@ -0,0 +1,593 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.worldclock
+
+import android.content.Context
+import android.os.Bundle
+import androidx.appcompat.widget.SearchView
+import android.text.TextUtils
+import android.text.format.DateFormat
+import android.util.ArraySet
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.CheckBox
+import android.widget.CompoundButton
+import android.widget.ListView
+import android.widget.SectionIndexer
+import android.widget.TextView
+
+import com.android.deskclock.BaseActivity
+import com.android.deskclock.DropShadowController
+import com.android.deskclock.R
+import com.android.deskclock.Utils
+import com.android.deskclock.actionbarmenu.MenuItemController
+import com.android.deskclock.actionbarmenu.MenuItemControllerFactory
+import com.android.deskclock.actionbarmenu.NavUpMenuItemController
+import com.android.deskclock.actionbarmenu.OptionsMenuManager
+import com.android.deskclock.actionbarmenu.SearchMenuItemController
+import com.android.deskclock.actionbarmenu.SettingsMenuItemController
+import com.android.deskclock.data.City
+import com.android.deskclock.data.DataModel
+
+import java.util.ArrayList
+import java.util.Calendar
+import java.util.Comparator
+import java.util.Locale
+import java.util.TimeZone
+
+/**
+ * This activity allows the user to alter the cities selected for display.
+ *
+ * Note, it is possible for two instances of this Activity to exist simultaneously:
+ * <ul>
+ * <li>Clock Tab-> Tap Floating Action Button</li>
+ * <li>Digital Widget -> Tap any city clock</li>
+ * </ul>
+ *
+ * As a result, [.onResume] conservatively refreshes itself from the backing
+ * [DataModel] which may have changed since this activity was last displayed.
+ */
+class CitySelectionActivity : BaseActivity() {
+ /**
+ * The list of all selected and unselected cities, indexed and possibly filtered.
+ */
+ private lateinit var mCitiesList: ListView
+
+ /**
+ * The adapter that presents all of the selected and unselected cities.
+ */
+ private lateinit var mCitiesAdapter: CityAdapter
+
+ /**
+ * Manages all action bar menu display and click handling.
+ */
+ private val mOptionsMenuManager = OptionsMenuManager()
+
+ /**
+ * Menu item controller for search view.
+ */
+ private lateinit var mSearchMenuItemController: SearchMenuItemController
+
+ /**
+ * The controller that shows the drop shadow when content is not scrolled to the top.
+ */
+ private lateinit var mDropShadowController: DropShadowController
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.cities_activity)
+ mSearchMenuItemController = SearchMenuItemController(
+ getSupportActionBar()!!.getThemedContext(),
+ object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return false
+ }
+
+ override fun onQueryTextChange(query: String): Boolean {
+ mCitiesAdapter.filter(query)
+ updateFastScrolling()
+ return true
+ }
+ }, savedInstanceState)
+ mCitiesAdapter = CityAdapter(this, mSearchMenuItemController)
+ mOptionsMenuManager.addMenuItemController(NavUpMenuItemController(this))
+ .addMenuItemController(mSearchMenuItemController)
+ .addMenuItemController(SortOrderMenuItemController())
+ .addMenuItemController(SettingsMenuItemController(this))
+ .addMenuItemController(*MenuItemControllerFactory.buildMenuItemControllers(this))
+ mCitiesList = findViewById(R.id.cities_list) as ListView
+ mCitiesList.adapter = mCitiesAdapter
+
+ updateFastScrolling()
+ }
+
+ override fun onSaveInstanceState(bundle: Bundle) {
+ super.onSaveInstanceState(bundle)
+ mSearchMenuItemController.saveInstance(bundle)
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // Recompute the contents of the adapter before displaying on screen.
+ mCitiesAdapter.refresh()
+
+ val dropShadow: View = findViewById(R.id.drop_shadow)
+ mDropShadowController = DropShadowController(dropShadow, mCitiesList)
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ mDropShadowController.stop()
+
+ // Save the selected cities.
+ DataModel.dataModel.selectedCities = mCitiesAdapter.selectedCities
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ mOptionsMenuManager.onCreateOptionsMenu(menu)
+ return true
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu): Boolean {
+ mOptionsMenuManager.onPrepareOptionsMenu(menu)
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ return (mOptionsMenuManager.onOptionsItemSelected(item) ||
+ super.onOptionsItemSelected(item))
+ }
+
+ /**
+ * Fast scrolling is only enabled while no filtering is happening.
+ */
+ private fun updateFastScrolling() {
+ val enabled: Boolean = !mCitiesAdapter.isFiltering
+ mCitiesList.isFastScrollAlwaysVisible = enabled
+ mCitiesList.isFastScrollEnabled = enabled
+ }
+
+ /**
+ * This adapter presents data in 2 possible modes. If selected cities exist the format is:
+ *
+ * <pre>
+ * Selected Cities
+ * City 1 (alphabetically first)
+ * City 2 (alphabetically second)
+ * ...
+ * A City A1 (alphabetically first starting with A)
+ * City A2 (alphabetically second starting with A)
+ * ...
+ * B City B1 (alphabetically first starting with B)
+ * City B2 (alphabetically second starting with B)
+ * ...
+ * </pre>
+ *
+ * If selected cities do not exist, that section is removed and all that remains is:
+ *
+ * <pre>
+ * A City A1 (alphabetically first starting with A)
+ * City A2 (alphabetically second starting with A)
+ * ...
+ * B City B1 (alphabetically first starting with B)
+ * City B2 (alphabetically second starting with B)
+ * ...
+ * </pre>
+ */
+ private class CityAdapter(
+ private val mContext: Context,
+ /** Menu item controller for search. Search query is maintained here. */
+ private val mSearchMenuItemController: SearchMenuItemController
+ ) : BaseAdapter(), View.OnClickListener,
+ CompoundButton.OnCheckedChangeListener, SectionIndexer {
+ private val mInflater: LayoutInflater = LayoutInflater.from(mContext)
+
+ /**
+ * The 12-hour time pattern for the current locale.
+ */
+ private val mPattern12: String
+
+ /**
+ * The 24-hour time pattern for the current locale.
+ */
+ private val mPattern24: String
+
+ /**
+ * `true` time should honor [.mPattern24]; [.mPattern12] otherwise.
+ */
+ private var mIs24HoursMode = false
+
+ /**
+ * A calendar used to format time in a particular timezone.
+ */
+ private val mCalendar: Calendar = Calendar.getInstance()
+
+ /**
+ * The list of cities which may be filtered by a search term.
+ */
+ private var mFilteredCities: List<City> = emptyList()
+
+ /**
+ * A mutable set of cities currently selected by the user.
+ */
+ private val mUserSelectedCities: MutableSet<City> = ArraySet()
+
+ /**
+ * The number of user selections at the top of the adapter to avoid indexing.
+ */
+ private var mOriginalUserSelectionCount = 0
+
+ /**
+ * The precomputed section headers.
+ */
+ private var mSectionHeaders: Array<String>? = null
+
+ /**
+ * The corresponding location of each precomputed section header.
+ */
+ private var mSectionHeaderPositions: Array<Int>? = null
+
+ init {
+ mCalendar.timeInMillis = System.currentTimeMillis()
+
+ val locale = Locale.getDefault()
+ mPattern24 = DateFormat.getBestDateTimePattern(locale, "Hm")
+
+ var pattern12 = DateFormat.getBestDateTimePattern(locale, "hma")
+ if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
+ // There's an RTL layout bug that causes jank when fast-scrolling through
+ // the list in 12-hour mode in an RTL locale. We can work around this by
+ // ensuring the strings are the same length by using "hh" instead of "h".
+ pattern12 = pattern12.replace("h".toRegex(), "hh")
+ }
+ mPattern12 = pattern12
+ }
+
+ override fun getCount(): Int {
+ val headerCount = if (hasHeader()) 1 else 0
+ return headerCount + mFilteredCities.size
+ }
+
+ override fun getItem(position: Int): City? {
+ if (hasHeader()) {
+ val itemViewType = getItemViewType(position)
+ when (itemViewType) {
+ VIEW_TYPE_SELECTED_CITIES_HEADER -> return null
+ VIEW_TYPE_CITY -> return mFilteredCities[position - 1]
+ }
+ throw IllegalStateException("unexpected item view type: $itemViewType")
+ }
+
+ return mFilteredCities[position]
+ }
+
+ override fun getItemId(position: Int): Long {
+ return position.toLong()
+ }
+
+ override fun getView(position: Int, view: View?, parent: ViewGroup): View {
+ var variableView = view
+ val itemViewType = getItemViewType(position)
+ when (itemViewType) {
+ VIEW_TYPE_SELECTED_CITIES_HEADER -> {
+ return variableView
+ ?: mInflater.inflate(R.layout.city_list_header, parent, false)
+ }
+ VIEW_TYPE_CITY -> {
+ val city = getItem(position)
+ ?: throw IllegalStateException("The desired city does not exist")
+ val timeZone: TimeZone = city.timeZone
+
+ // Inflate a new view if necessary.
+ if (variableView == null) {
+ variableView = mInflater.inflate(R.layout.city_list_item, parent, false)
+ val index = variableView.findViewById<View>(R.id.index) as TextView
+ val name = variableView.findViewById<View>(R.id.city_name) as TextView
+ val time = variableView.findViewById<View>(R.id.city_time) as TextView
+ val selected = variableView.findViewById<View>(R.id.city_onoff) as CheckBox
+ variableView.tag = CityItemHolder(index, name, time, selected)
+ }
+
+ // Bind data into the child views.
+ val holder = variableView!!.tag as CityItemHolder
+ holder.selected.tag = city
+ holder.selected.isChecked = mUserSelectedCities.contains(city)
+ holder.selected.contentDescription = city.name
+ holder.selected.setOnCheckedChangeListener(this)
+ holder.name.setText(city.name, TextView.BufferType.SPANNABLE)
+ holder.time.text = getTimeCharSequence(timeZone)
+
+ val showIndex = getShowIndex(position)
+ holder.index.visibility = if (showIndex) View.VISIBLE else View.INVISIBLE
+ if (showIndex) {
+ when (citySort) {
+ DataModel.CitySort.NAME -> {
+ holder.index.setText(city.indexString)
+ holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f)
+ }
+ DataModel.CitySort.UTC_OFFSET -> {
+ holder.index.text = Utils.getGMTHourOffset(timeZone, false)
+ holder.index.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
+ }
+ }
+ }
+
+ // skip checkbox and other animations
+ variableView.jumpDrawablesToCurrentState()
+ variableView.setOnClickListener(this)
+ return variableView
+ }
+ else -> throw IllegalStateException("unexpected item view type: $itemViewType")
+ }
+ }
+
+ override fun getViewTypeCount(): Int {
+ return 2
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return if (hasHeader() && position == 0) {
+ VIEW_TYPE_SELECTED_CITIES_HEADER
+ } else {
+ VIEW_TYPE_CITY
+ }
+ }
+
+ override fun onCheckedChanged(b: CompoundButton, checked: Boolean) {
+ val city = b.tag as City
+ if (checked) {
+ mUserSelectedCities.add(city)
+ b.announceForAccessibility(mContext.getString(R.string.city_checked,
+ city.name))
+ } else {
+ mUserSelectedCities.remove(city)
+ b.announceForAccessibility(mContext.getString(R.string.city_unchecked,
+ city.name))
+ }
+ }
+
+ override fun onClick(v: View) {
+ val b = v.findViewById<View>(R.id.city_onoff) as CheckBox
+ b.isChecked = !b.isChecked
+ }
+
+ override fun getSections(): Array<String>? {
+ if (mSectionHeaders == null) {
+ // Make an educated guess at the expected number of sections.
+ val approximateSectionCount = count / 5
+ val sections: MutableList<String> = ArrayList(approximateSectionCount)
+ val positions: MutableList<Int> = ArrayList(approximateSectionCount)
+
+ // Add a section for the "Selected Cities" header if it exists.
+ if (hasHeader()) {
+ sections.add("+")
+ positions.add(0)
+ }
+
+ for (position in 0 until count) {
+ // Add a section if this position should show the section index.
+ if (getShowIndex(position)) {
+ val city = getItem(position)
+ ?: throw IllegalStateException("The desired city does not exist")
+ when (citySort) {
+ DataModel.CitySort.NAME -> sections.add(city.indexString.orEmpty())
+ DataModel.CitySort.UTC_OFFSET -> {
+ val timezone: TimeZone = city.timeZone
+ sections.add(Utils.getGMTHourOffset(timezone, Utils.isPreL))
+ }
+ }
+ positions.add(position)
+ }
+ }
+
+ mSectionHeaders = sections.toTypedArray()
+ mSectionHeaderPositions = positions.toTypedArray()
+ }
+ return mSectionHeaders
+ }
+
+ override fun getPositionForSection(sectionIndex: Int): Int {
+ return if (sections!!.isEmpty()) 0 else mSectionHeaderPositions!![sectionIndex]
+ }
+
+ override fun getSectionForPosition(position: Int): Int {
+ if (sections!!.isEmpty()) {
+ return 0
+ }
+
+ for (i in 0 until mSectionHeaderPositions!!.size - 2) {
+ if (position < mSectionHeaderPositions!![i]) continue
+ if (position >= mSectionHeaderPositions!![i + 1]) continue
+ return i
+ }
+
+ return mSectionHeaderPositions!!.size - 1
+ }
+
+ /**
+ * Clear the section headers to force them to be recomputed if they are now stale.
+ */
+ fun clearSectionHeaders() {
+ mSectionHeaders = null
+ mSectionHeaderPositions = null
+ }
+
+ /**
+ * Rebuilds all internal data structures from scratch.
+ */
+ fun refresh() {
+ // Update the 12/24 hour mode.
+ mIs24HoursMode = DateFormat.is24HourFormat(mContext)
+
+ // Refresh the user selections.
+ val selected = DataModel.dataModel.selectedCities as List<City>
+ mUserSelectedCities.clear()
+ mUserSelectedCities.addAll(selected)
+ mOriginalUserSelectionCount = selected.size
+
+ // Recompute section headers.
+ clearSectionHeaders()
+
+ // Recompute filtered cities.
+ filter(mSearchMenuItemController.queryText)
+ }
+
+ /**
+ * Filter the cities using the given `queryText`.
+ */
+ fun filter(queryText: String) {
+ mSearchMenuItemController.queryText = queryText
+ val query = City.removeSpecialCharacters(queryText.toUpperCase())
+
+ // Compute the filtered list of cities.
+ val filteredCities = if (TextUtils.isEmpty(query)) {
+ DataModel.dataModel.allCities
+ } else {
+ val unselected: List<City> = DataModel.dataModel.unselectedCities
+ val queriedCities: MutableList<City> = ArrayList(unselected.size)
+ for (city in unselected) {
+ if (city.matches(query)) {
+ queriedCities.add(city)
+ }
+ }
+ queriedCities
+ }
+
+ // Swap in the filtered list of cities and notify of the data change.
+ mFilteredCities = filteredCities
+ notifyDataSetChanged()
+ }
+
+ val isFiltering: Boolean
+ get() = !TextUtils.isEmpty(mSearchMenuItemController.queryText.trim({ it <= ' ' }))
+
+ val selectedCities: Collection<City>
+ get() = mUserSelectedCities
+
+ private fun hasHeader(): Boolean {
+ return !isFiltering && mOriginalUserSelectionCount > 0
+ }
+
+ private val citySort: DataModel.CitySort
+ get() = DataModel.dataModel.citySort
+
+ private val citySortComparator: Comparator<City>
+ get() = DataModel.dataModel.cityIndexComparator
+
+ private fun getTimeCharSequence(timeZone: TimeZone): CharSequence {
+ mCalendar.timeZone = timeZone
+ return DateFormat.format(if (mIs24HoursMode) mPattern24 else mPattern12, mCalendar)
+ }
+
+ private fun getShowIndex(position: Int): Boolean {
+ // Indexes are never displayed on filtered cities.
+ if (isFiltering) {
+ return false
+ }
+
+ if (hasHeader()) {
+ // None of the original user selections should show their index.
+ if (position <= mOriginalUserSelectionCount) {
+ return false
+ }
+
+ // The first item after the original user selections must always show its index.
+ if (position == mOriginalUserSelectionCount + 1) {
+ return true
+ }
+ } else {
+ // None of the original user selections should show their index.
+ if (position < mOriginalUserSelectionCount) {
+ return false
+ }
+
+ // The first item after the original user selections must always show its index.
+ if (position == mOriginalUserSelectionCount) {
+ return true
+ }
+ }
+
+ // Otherwise compare the city with its predecessor to test if it is a header.
+ val priorCity = getItem(position - 1)
+ val city = getItem(position)
+ return citySortComparator.compare(priorCity, city) != 0
+ }
+
+ /**
+ * Cache the child views of each city item view.
+ */
+ private class CityItemHolder(
+ val index: TextView,
+ val name: TextView,
+ val time: TextView,
+ val selected: CheckBox
+ )
+
+ companion object {
+ /**
+ * The type of the single optional "Selected Cities" header entry.
+ */
+ private const val VIEW_TYPE_SELECTED_CITIES_HEADER = 0
+
+ /**
+ * The type of each city entry.
+ */
+ private const val VIEW_TYPE_CITY = 1
+ }
+ }
+
+ private inner class SortOrderMenuItemController : MenuItemController {
+ private val SORT_MENU_RES_ID = R.id.menu_item_sort
+
+ override val id: Int
+ get() = SORT_MENU_RES_ID
+
+ override fun onCreateOptionsItem(menu: Menu) {
+ menu.add(Menu.NONE, R.id.menu_item_sort, Menu.NONE,
+ R.string.menu_item_sort_by_gmt_offset)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+ }
+
+ override fun onPrepareOptionsItem(item: MenuItem) {
+ item.setTitle(if (DataModel.dataModel.citySort == DataModel.CitySort.NAME) {
+ R.string.menu_item_sort_by_gmt_offset
+ } else {
+ R.string.menu_item_sort_by_name
+ })
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ // Save the new sort order.
+ DataModel.dataModel.toggleCitySort()
+
+ // Section headers are influenced by sort order and must be cleared.
+ mCitiesAdapter.clearSectionHeaders()
+
+ // Honor the new sort order in the adapter.
+ mCitiesAdapter.filter(mSearchMenuItemController.queryText)
+ return true
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 0000000..c8e807e
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,17 @@
+android_test {
+ name: "DeskClockTests",
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ static_libs: [
+ "junit",
+ "androidx.test.core",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ ],
+ // Include all test java files.
+ srcs: ["src/**/*.java"],
+ platform_apis: true,
+ instrumentation_for: "DeskClock",
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..64cc6e2
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.deskclock.tests">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.deskclock"
+ android:label="Tests for DeskClock application."/>
+</manifest>
diff --git a/tests/src/com/android/deskclock/ringtone/RingtonePickerActivityTest.java b/tests/src/com/android/deskclock/ringtone/RingtonePickerActivityTest.java
new file mode 100644
index 0000000..1e7bf28
--- /dev/null
+++ b/tests/src/com/android/deskclock/ringtone/RingtonePickerActivityTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.ringtone;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.ItemAdapter;
+import com.android.deskclock.ItemAdapter.ItemHolder;
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.provider.Alarm;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Iterator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Exercise the user interface that adjusts the selected ringtone.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class RingtonePickerActivityTest {
+
+ private RingtonePickerActivity activity;
+ private RecyclerView ringtoneList;
+ private ItemAdapter<ItemHolder<Uri>> ringtoneAdapter;
+
+ public static final Uri ALERT = Uri.parse("content://settings/system/alarm_alert");
+ public static final Uri CUSTOM_RINGTONE_1 = Uri.parse("content://media/external/audio/one.ogg");
+
+ @Rule
+ public ActivityTestRule<RingtonePickerActivity> rule =
+ new ActivityTestRule<>(RingtonePickerActivity.class, true, false);
+
+ @Before
+ @After
+ public void setUp() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.edit().clear().commit();
+ }
+
+ @Test
+ public void validateDefaultState_TimerRingtonePicker() {
+ createTimerRingtonePickerActivity();
+
+ final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.getItems();
+ final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+ final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+ final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+ assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+ final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+ final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+ final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+ Runnable assertRunnable = () -> {
+ assertEquals("Silent", silentHolder.getName());
+ assertEquals("Timer Expired", defaultHolder.getName());
+ assertEquals(DataModel.getDataModel().getDefaultTimerRingtoneUri(),
+ defaultHolder.getUri());
+ // Verify initial selection.
+ assertEquals(
+ DataModel.getDataModel().getTimerRingtoneUri(),
+ DataModel.getDataModel().getDefaultTimerRingtoneUri());
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+ }
+
+ @Test
+ public void validateDefaultState_AlarmRingtonePicker() {
+ createAlarmRingtonePickerActivity(ALERT);
+
+ final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.getItems();
+ final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+ final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+ final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+ assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+ final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+ final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+ final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+ Runnable assertRunnable = () -> {
+ assertEquals("Silent", silentHolder.getName());
+ assertEquals("Default alarm sound", defaultHolder.getName());
+ assertEquals(DataModel.getDataModel().getDefaultAlarmRingtoneUri(),
+ defaultHolder.getUri());
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+ }
+
+ @Test
+ public void validateDefaultState_TimerRingtonePicker_WithCustomRingtones() {
+ Runnable customRingtoneRunnable = () -> {
+ DataModel.getDataModel().addCustomRingtone(CUSTOM_RINGTONE_1, "CustomSound");
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(customRingtoneRunnable);
+ createTimerRingtonePickerActivity();
+
+ final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.getItems();
+ final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+ final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+ final CustomRingtoneHolder customRingtoneHolder = (CustomRingtoneHolder) itemsIter.next();
+ assertEquals("CustomSound", customRingtoneHolder.getName());
+ assertEquals(CUSTOM_RINGTONE_1, customRingtoneHolder.getUri());
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND,
+ customRingtoneHolder.getItemViewType());
+
+ final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+ assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+ final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+ final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+ final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+ Runnable assertRunnable = () -> {
+ assertEquals("Silent", silentHolder.getName());
+ assertEquals("Timer Expired", defaultHolder.getName());
+ assertEquals(DataModel.getDataModel().getDefaultTimerRingtoneUri(),
+ defaultHolder.getUri());
+ // Verify initial selection.
+ assertEquals(
+ DataModel.getDataModel().getTimerRingtoneUri(),
+ DataModel.getDataModel().getDefaultTimerRingtoneUri());
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+
+ Runnable removeCustomRingtoneRunnable = () -> {
+ DataModel.getDataModel().removeCustomRingtone(CUSTOM_RINGTONE_1);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(removeCustomRingtoneRunnable);
+ }
+
+ @Test
+ public void validateDefaultState_AlarmRingtonePicker_WithCustomRingtones() {
+ Runnable customRingtoneRunnable = () -> {
+ DataModel.getDataModel().addCustomRingtone(CUSTOM_RINGTONE_1, "CustomSound");
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(customRingtoneRunnable);
+ createAlarmRingtonePickerActivity(ALERT);
+
+ final List<ItemHolder<Uri>> systemRingtoneHolders = ringtoneAdapter.getItems();
+ final Iterator<ItemHolder<Uri>> itemsIter = systemRingtoneHolders.iterator();
+
+ final HeaderHolder filesHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.your_sounds, filesHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, filesHeaderHolder.getItemViewType());
+
+ final CustomRingtoneHolder customRingtoneHolder = (CustomRingtoneHolder) itemsIter.next();
+ assertEquals("CustomSound", customRingtoneHolder.getName());
+ assertEquals(CUSTOM_RINGTONE_1, customRingtoneHolder.getUri());
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_CUSTOM_SOUND,
+ customRingtoneHolder.getItemViewType());
+
+ final AddCustomRingtoneHolder addNewHolder = (AddCustomRingtoneHolder) itemsIter.next();
+ assertEquals(AddCustomRingtoneViewHolder.VIEW_TYPE_ADD_NEW, addNewHolder.getItemViewType());
+
+ final HeaderHolder systemHeaderHolder = (HeaderHolder) itemsIter.next();
+ assertEquals(R.string.device_sounds, systemHeaderHolder.getTextResId());
+ assertEquals(HeaderViewHolder.VIEW_TYPE_ITEM_HEADER, systemHeaderHolder.getItemViewType());
+
+ final RingtoneHolder silentHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(Utils.RINGTONE_SILENT, silentHolder.getUri());
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, silentHolder.getItemViewType());
+
+ final RingtoneHolder defaultHolder = (RingtoneHolder) itemsIter.next();
+ assertEquals(RingtoneViewHolder.VIEW_TYPE_SYSTEM_SOUND, defaultHolder.getItemViewType());
+
+ Runnable assertRunnable = () -> {
+ assertEquals("Silent", silentHolder.getName());
+ assertEquals("Default alarm sound", defaultHolder.getName());
+ assertEquals(DataModel.getDataModel().getDefaultAlarmRingtoneUri(),
+ defaultHolder.getUri());
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+
+ Runnable removeCustomRingtoneRunnable = () -> {
+ DataModel.getDataModel().removeCustomRingtone(CUSTOM_RINGTONE_1);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(removeCustomRingtoneRunnable);
+ }
+
+ private void createTimerRingtonePickerActivity() {
+ final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final Intent newIntent = new Intent();
+
+ Runnable createIntentRunnable = () -> {
+ final Intent intent = RingtonePickerActivity.createTimerRingtonePickerIntent(context);
+ newIntent.fillIn(intent, 0);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(createIntentRunnable);
+
+ createRingtonePickerActivity(newIntent);
+ }
+
+ private void createAlarmRingtonePickerActivity(Uri ringtone) {
+ final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final Intent newIntent = new Intent();
+
+ Runnable createIntentRunnable = () -> {
+ // Use the custom ringtone in some alarms.
+ final Alarm alarm = new Alarm(1, 1);
+ alarm.enabled = true;
+ alarm.vibrate = true;
+ alarm.alert = ringtone;
+ alarm.deleteAfterUse = true;
+
+ final Intent intent =
+ RingtonePickerActivity.createAlarmRingtonePickerIntent(context, alarm);
+ newIntent.fillIn(intent, 0);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(createIntentRunnable);
+
+ createRingtonePickerActivity(newIntent);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void createRingtonePickerActivity(Intent intent) {
+ activity = rule.launchActivity(intent);
+ ringtoneList = activity.findViewById(R.id.ringtone_content);
+ ringtoneAdapter = (ItemAdapter<ItemHolder<Uri>>) ringtoneList.getAdapter();
+ }
+}
diff --git a/tests/src/com/android/deskclock/timer/ExpiredTimersActivityTest.java b/tests/src/com/android/deskclock/timer/ExpiredTimersActivityTest.java
new file mode 100644
index 0000000..1fd7b0f
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/ExpiredTimersActivityTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer;
+
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertSame;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class ExpiredTimersActivityTest {
+
+ @Rule
+ public ActivityTestRule<ExpiredTimersActivity> rule =
+ new ActivityTestRule<>(ExpiredTimersActivity.class, true, false);
+
+ @Test
+ public void configurationChanges_DoNotResetFiringTimer() {
+ // Construct an ExpiredTimersActivity to display the firing timer.
+ final Context context = ApplicationProvider.getApplicationContext();
+ final Intent intent = new Intent()
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+ final ExpiredTimersActivity activity = rule.launchActivity(intent);
+
+ Runnable fireTimerRunnable = () -> {
+ // Create a firing timer.
+ final DataModel dm = DataModel.getDataModel();
+ Timer timer = dm.addTimer(60000L, "", false);
+ dm.startTimer(timer);
+ dm.expireTimer(null, dm.getTimer(timer.getId()));
+ timer = dm.getTimer(timer.getId());
+
+ // Make the ExpiredTimersActivity believe it has been displayed to the user.
+ activity.getWindow().getCallback().onWindowFocusChanged(true);
+
+ // Simulate a configuration change by recreating the activity.
+ activity.recreate();
+
+ // Verify that the recreation did not alter the firing timer.
+ assertSame(timer, dm.getTimer(timer.getId()));
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(fireTimerRunnable);
+ }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerFragmentTest.java b/tests/src/com/android/deskclock/timer/TimerFragmentTest.java
new file mode 100644
index 0000000..9728db6
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerFragmentTest.java
@@ -0,0 +1,718 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+import com.android.deskclock.widget.MockFabContainer;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerFragmentTest {
+
+ private static int LIGHT;
+ private static int DARK;
+ private static int TOP;
+ private static int BOTTOM;
+
+ private static final int GONE = 0;
+
+ private TimerFragment fragment;
+ private View timersView;
+ private View timerSetupView;
+ private ViewPager viewPager;
+ private TimerPagerAdapter adapter;
+
+ private ImageView fab;
+ private Button leftButton;
+ private Button rightButton;
+
+ @Rule
+ public ActivityTestRule<DeskClock> rule = new ActivityTestRule<>(DeskClock.class, true);
+
+ @BeforeClass
+ public static void staticSetUp() {
+ LIGHT = R.drawable.ic_swipe_circle_light;
+ DARK = R.drawable.ic_swipe_circle_dark;
+ TOP = R.drawable.ic_swipe_circle_top;
+ BOTTOM = R.drawable.ic_swipe_circle_bottom;
+ }
+
+ private void setUpSingleTimer() {
+ Runnable addTimerRunnable = () -> {
+ DataModel.getDataModel().addTimer(60000L, null, false);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+ setUpFragment();
+ }
+
+ private void setUpTwoTimers() {
+ Runnable addTimerRunnable = () -> {
+ DataModel.getDataModel().addTimer(60000L, null, false);
+ DataModel.getDataModel().addTimer(90000L, null, false);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+ setUpFragment();
+ }
+
+ private void setUpFragment() {
+ Runnable setUpFragmentRunnable = () -> {
+ ViewPager deskClockPager =
+ (ViewPager) rule.getActivity().findViewById(R.id.desk_clock_pager);
+ PagerAdapter tabPagerAdapter = (PagerAdapter) deskClockPager.getAdapter();
+ fragment = (TimerFragment) tabPagerAdapter.instantiateItem(deskClockPager, 2);
+ fragment.onStart();
+ fragment.selectTab();
+ final MockFabContainer fabContainer =
+ new MockFabContainer(fragment, ApplicationProvider.getApplicationContext());
+ fragment.setFabContainer(fabContainer);
+
+ final View view = fragment.getView();
+ assertNotNull(view);
+
+ timersView = view.findViewById(R.id.timer_view);
+ timerSetupView = view.findViewById(R.id.timer_setup);
+ viewPager = view.findViewById(R.id.vertical_view_pager);
+ adapter = (TimerPagerAdapter) viewPager.getAdapter();
+
+ fab = fabContainer.getFab();
+ leftButton = fabContainer.getLeftButton();
+ rightButton = fabContainer.getRightButton();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(setUpFragmentRunnable);
+ }
+
+ @After
+ public void tearDown() {
+ clearTimers();
+ fragment = null;
+ fab = null;
+ timerSetupView = null;
+ timersView = null;
+ adapter = null;
+ viewPager = null;
+ leftButton = null;
+ rightButton = null;
+ }
+
+ private void clearTimers() {
+ Runnable clearTimersRunnable = () -> {
+ final List<Timer> timers = new ArrayList<>(DataModel.getDataModel().getTimers());
+ for (Timer timer : timers) {
+ DataModel.getDataModel().removeTimer(timer);
+ }
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(clearTimersRunnable);
+ }
+
+ @Test
+ public void initialStateNoTimers() {
+ setUpFragment();
+ assertEquals(View.VISIBLE, timerSetupView.getVisibility());
+ assertEquals(View.GONE, timersView.getVisibility());
+ assertAdapter(0);
+ }
+
+ @Test
+ public void initialStateOneTimer() {
+ setUpSingleTimer();
+ assertEquals(View.VISIBLE, timersView.getVisibility());
+ assertEquals(View.GONE, timerSetupView.getVisibility());
+ assertAdapter(1);
+ }
+
+ @Test
+ public void initialStateTwoTimers() {
+ setUpTwoTimers();
+ assertEquals(View.VISIBLE, timersView.getVisibility());
+ assertEquals(View.GONE, timerSetupView.getVisibility());
+ assertAdapter(2);
+ }
+
+ @Test
+ public void timeClick_startsTimer() {
+ setUpSingleTimer();
+
+ setCurrentItem(0);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+ final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 0);
+ }
+
+ @Test
+ public void timeClick_startsSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+ final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void timeClick_pausesTimer() {
+ setUpSingleTimer();
+
+ setCurrentItem(0);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+ final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.PAUSED, 0);
+ }
+
+ @Test
+ public void timeClick_pausesSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+ final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.PAUSED, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void timeClick_restartsTimer() {
+ setUpSingleTimer();
+
+ setCurrentItem(0);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+ final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.PAUSED, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 0);
+ }
+
+ @Test
+ public void timeClick_restartsSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+ final TextView timeText = timerItem.findViewById(R.id.timer_time_text);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.PAUSED, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(timeText);
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void fabClick_startsTimer() {
+ setUpSingleTimer();
+
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 0);
+ }
+
+ @Test
+ public void fabClick_startsSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void fabClick_pausesTimer() {
+ setUpSingleTimer();
+
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 0);
+ clickFab();
+ assertStateEquals(Timer.State.PAUSED, 0);
+ }
+
+ @Test
+ public void fabClick_pausesSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.PAUSED, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void fabClick_restartsTimer() {
+ setUpSingleTimer();
+
+ setCurrentItem(0);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 0);
+ clickFab();
+ assertStateEquals(Timer.State.PAUSED, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 0);
+ }
+
+ @Test
+ public void fabClick_restartsSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.PAUSED, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void fabClick_resetsTimer() {
+ setUpSingleTimer();
+
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 0);
+ final Context context = fab.getContext();
+ Runnable expireTimerRunnable = () -> {
+ DataModel.getDataModel().expireTimer(null, DataModel.getDataModel().getTimers().get(0));
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(expireTimerRunnable);
+ clickFab();
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void fabClick_resetsSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ final Context context = fab.getContext();
+ Runnable expireTimerRunnable = () -> {
+ DataModel.getDataModel().expireTimer(null, DataModel.getDataModel().getTimers().get(1));
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(expireTimerRunnable);
+ clickFab();
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void clickAdd_addsOneMinuteToTimer() {
+ setUpSingleTimer();
+
+ setCurrentItem(0);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+ final Button addMinute = timerItem.findViewById(R.id.reset_add);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 0);
+ Runnable getTimersRunnable = () -> {
+ long remainingTime1 = DataModel.getDataModel().getTimers().get(0).getRemainingTime();
+ addMinute.performClick();
+ long remainingTime2 = DataModel.getDataModel().getTimers().get(0).getRemainingTime();
+ assertSame(Timer.State.RUNNING, DataModel.getDataModel().getTimers().get(0).getState());
+ long expectedSeconds =
+ TimeUnit.MILLISECONDS.toSeconds(remainingTime1 + DateUtils.MINUTE_IN_MILLIS);
+ long observedSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime2);
+ assertEquals(expectedSeconds, observedSeconds);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(getTimersRunnable);
+ }
+
+ @Test
+ public void clickAdd_addsOneMinuteToSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+ final Button addMinute = timerItem.findViewById(R.id.reset_add);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ Runnable getTimersRunnable = () -> {
+ long remainingTime1 = DataModel.getDataModel().getTimers().get(1).getRemainingTime();
+ addMinute.performClick();
+ long remainingTime2 = DataModel.getDataModel().getTimers().get(1).getRemainingTime();
+ assertSame(Timer.State.RUNNING, DataModel.getDataModel().getTimers().get(1).getState());
+ assertSame(Timer.State.RESET, DataModel.getDataModel().getTimers().get(0).getState());
+ long expectedSeconds =
+ TimeUnit.MILLISECONDS.toSeconds(remainingTime1 + DateUtils.MINUTE_IN_MILLIS);
+ long observedSeconds = TimeUnit.MILLISECONDS.toSeconds(remainingTime2);
+ assertEquals(expectedSeconds, observedSeconds);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(getTimersRunnable);
+ }
+
+ @Test
+ public void clickReset_resetsTimer() {
+ setUpSingleTimer();
+
+ setCurrentItem(0);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+ final Button reset = timerItem.findViewById(R.id.reset_add);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 0);
+ clickFab();
+ assertStateEquals(Timer.State.PAUSED, 0);
+ clickView(reset);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void clickReset_resetsSecondTimer() {
+ setUpTwoTimers();
+
+ setCurrentItem(1);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(1);
+ final Button reset = timerItem.findViewById(R.id.reset_add);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.RUNNING, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickFab();
+ assertStateEquals(Timer.State.PAUSED, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(reset);
+ assertStateEquals(Timer.State.RESET, 1);
+ assertStateEquals(Timer.State.RESET, 0);
+ }
+
+ @Test
+ public void labelClick_opensLabel() {
+ setUpSingleTimer();
+
+ setCurrentItem(0);
+ final TimerItem timerItem = (TimerItem) viewPager.getChildAt(0);
+ final TextView label = timerItem.findViewById(R.id.timer_label);
+ assertStateEquals(Timer.State.RESET, 0);
+ clickView(label);
+ }
+
+ //
+ // 3 Indicators
+ //
+
+ @Test
+ public void verify3Indicators0Pages() {
+ assertIndicatorsEquals(0, 3, 0, GONE, GONE, GONE);
+ }
+
+ @Test
+ public void verify3Indicators1Page() {
+ assertIndicatorsEquals(0, 3, 1, GONE, GONE, GONE);
+ }
+
+ @Test
+ public void verify3Indicators2Pages() {
+ assertIndicatorsEquals(0, 3, 2, LIGHT, DARK, GONE);
+ assertIndicatorsEquals(1, 3, 2, DARK, LIGHT, GONE);
+ }
+
+ @Test
+ public void verify3Indicators3Pages() {
+ assertIndicatorsEquals(0, 3, 3, LIGHT, DARK, DARK);
+ assertIndicatorsEquals(1, 3, 3, DARK, LIGHT, DARK);
+ assertIndicatorsEquals(2, 3, 3, DARK, DARK, LIGHT);
+ }
+
+ @Test
+ public void verify3Indicators4Pages() {
+ assertIndicatorsEquals(0, 3, 4, LIGHT, DARK, BOTTOM);
+ assertIndicatorsEquals(1, 3, 4, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(2, 3, 4, TOP, LIGHT, DARK);
+ assertIndicatorsEquals(3, 3, 4, TOP, DARK, LIGHT);
+ }
+
+ @Test
+ public void verify3Indicators5Pages() {
+ assertIndicatorsEquals(0, 3, 5, LIGHT, DARK, BOTTOM);
+ assertIndicatorsEquals(1, 3, 5, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(2, 3, 5, TOP, LIGHT, BOTTOM);
+ assertIndicatorsEquals(3, 3, 5, TOP, LIGHT, DARK);
+ assertIndicatorsEquals(4, 3, 5, TOP, DARK, LIGHT);
+ }
+
+ @Test
+ public void verify3Indicators6Pages() {
+ assertIndicatorsEquals(0, 3, 6, LIGHT, DARK, BOTTOM);
+ assertIndicatorsEquals(1, 3, 6, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(2, 3, 6, TOP, LIGHT, BOTTOM);
+ assertIndicatorsEquals(3, 3, 6, TOP, LIGHT, BOTTOM);
+ assertIndicatorsEquals(4, 3, 6, TOP, LIGHT, DARK);
+ assertIndicatorsEquals(5, 3, 6, TOP, DARK, LIGHT);
+ }
+
+ @Test
+ public void verify3Indicators7Pages() {
+ assertIndicatorsEquals(0, 3, 7, LIGHT, DARK, BOTTOM);
+ assertIndicatorsEquals(1, 3, 7, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(2, 3, 7, TOP, LIGHT, BOTTOM);
+ assertIndicatorsEquals(3, 3, 7, TOP, LIGHT, BOTTOM);
+ assertIndicatorsEquals(4, 3, 7, TOP, LIGHT, BOTTOM);
+ assertIndicatorsEquals(5, 3, 7, TOP, LIGHT, DARK);
+ assertIndicatorsEquals(6, 3, 7, TOP, DARK, LIGHT);
+ }
+
+ //
+ // 4 Indicators
+ //
+
+ @Test
+ public void verify4Indicators0Pages() {
+ assertIndicatorsEquals(0, 4, 0, GONE, GONE, GONE, GONE);
+ }
+
+ @Test
+ public void verify4Indicators1Page() {
+ assertIndicatorsEquals(0, 4, 1, GONE, GONE, GONE, GONE);
+ }
+
+ @Test
+ public void verify4Indicators2Pages() {
+ assertIndicatorsEquals(0, 4, 2, LIGHT, DARK, GONE, GONE);
+ assertIndicatorsEquals(1, 4, 2, DARK, LIGHT, GONE, GONE);
+ }
+
+ @Test
+ public void verify4Indicators3Pages() {
+ assertIndicatorsEquals(0, 4, 3, LIGHT, DARK, DARK, GONE);
+ assertIndicatorsEquals(1, 4, 3, DARK, LIGHT, DARK, GONE);
+ assertIndicatorsEquals(2, 4, 3, DARK, DARK, LIGHT, GONE);
+ }
+
+ @Test
+ public void verify4Indicators4Pages() {
+ assertIndicatorsEquals(0, 4, 4, LIGHT, DARK, DARK, DARK);
+ assertIndicatorsEquals(1, 4, 4, DARK, LIGHT, DARK, DARK);
+ assertIndicatorsEquals(2, 4, 4, DARK, DARK, LIGHT, DARK);
+ assertIndicatorsEquals(3, 4, 4, DARK, DARK, DARK, LIGHT);
+ }
+
+ @Test
+ public void verify4Indicators5Pages() {
+ assertIndicatorsEquals(0, 4, 5, LIGHT, DARK, DARK, BOTTOM);
+ assertIndicatorsEquals(1, 4, 5, DARK, LIGHT, DARK, BOTTOM);
+ assertIndicatorsEquals(2, 4, 5, DARK, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(3, 4, 5, TOP, DARK, LIGHT, DARK);
+ assertIndicatorsEquals(4, 4, 5, TOP, DARK, DARK, LIGHT);
+ }
+
+ @Test
+ public void verify4Indicators6Pages() {
+ assertIndicatorsEquals(0, 4, 6, LIGHT, DARK, DARK, BOTTOM);
+ assertIndicatorsEquals(1, 4, 6, DARK, LIGHT, DARK, BOTTOM);
+ assertIndicatorsEquals(2, 4, 6, DARK, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(3, 4, 6, TOP, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(4, 4, 6, TOP, DARK, LIGHT, DARK);
+ assertIndicatorsEquals(5, 4, 6, TOP, DARK, DARK, LIGHT);
+ }
+
+ @Test
+ public void verify4Indicators7Pages() {
+ assertIndicatorsEquals(0, 4, 7, LIGHT, DARK, DARK, BOTTOM);
+ assertIndicatorsEquals(1, 4, 7, DARK, LIGHT, DARK, BOTTOM);
+ assertIndicatorsEquals(2, 4, 7, DARK, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(3, 4, 7, TOP, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(4, 4, 7, TOP, DARK, LIGHT, BOTTOM);
+ assertIndicatorsEquals(5, 4, 7, TOP, DARK, LIGHT, DARK);
+ assertIndicatorsEquals(6, 4, 7, TOP, DARK, DARK, LIGHT);
+ }
+
+ @Test
+ public void showTimerSetupView_fromIntent() {
+ setUpSingleTimer();
+
+ assertEquals(View.VISIBLE, timersView.getVisibility());
+ assertEquals(View.GONE, timerSetupView.getVisibility());
+
+ final Intent intent = TimerFragment.createTimerSetupIntent(fragment.getContext());
+ rule.getActivity().setIntent(intent);
+ restartFragment();
+
+ assertEquals(View.GONE, timersView.getVisibility());
+ assertEquals(View.VISIBLE, timerSetupView.getVisibility());
+ }
+
+ @Test
+ public void showTimerSetupView_usesLabel_fromIntent() {
+ setUpSingleTimer();
+
+ assertEquals(View.VISIBLE, timersView.getVisibility());
+ assertEquals(View.GONE, timerSetupView.getVisibility());
+
+ final Intent intent = TimerFragment.createTimerSetupIntent(fragment.getContext());
+ rule.getActivity().setIntent(intent);
+ restartFragment();
+
+ assertEquals(View.GONE, timersView.getVisibility());
+ assertEquals(View.VISIBLE, timerSetupView.getVisibility());
+ clickView(timerSetupView.findViewById(R.id.timer_setup_digit_3));
+
+ clickFab();
+ }
+
+ @Test
+ public void showTimer_fromIntent() {
+ setUpTwoTimers();
+
+ assertEquals(View.VISIBLE, timersView.getVisibility());
+ assertEquals(View.GONE, timerSetupView.getVisibility());
+ assertEquals(0, viewPager.getCurrentItem());
+
+ final Intent intent =
+ new Intent(ApplicationProvider.getApplicationContext(), TimerService.class)
+ .setAction(TimerService.ACTION_SHOW_TIMER)
+ .putExtra(TimerService.EXTRA_TIMER_ID, 0);
+ rule.getActivity().setIntent(intent);
+ restartFragment();
+
+ assertEquals(View.VISIBLE, timersView.getVisibility());
+ assertEquals(View.GONE, timerSetupView.getVisibility());
+ assertEquals(1, viewPager.getCurrentItem());
+ }
+
+ private void assertIndicatorsEquals(
+ int page, int indicatorCount, int pageCount, int... expected) {
+ int[] actual = TimerFragment.computePageIndicatorStates(page, indicatorCount, pageCount);
+ if (!Arrays.equals(expected, actual)) {
+ final String expectedString = Arrays.toString(expected);
+ final String actualString = Arrays.toString(actual);
+ fail(String.format("Expected %s, found %s", expectedString, actualString));
+ }
+ }
+
+ private void assertStateEquals(Timer.State expectedState, int index) {
+ Runnable timerRunnable = () -> {
+ final Timer.State actualState =
+ DataModel.getDataModel().getTimers().get(index).getState();
+ assertSame(expectedState, actualState);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(timerRunnable);
+ }
+
+ private void assertAdapter(int count) {
+ Runnable assertRunnable = () -> {
+ assertEquals(count, adapter.getCount());
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(assertRunnable);
+ }
+
+ private void restartFragment() {
+ Runnable onStartRunnable = () -> {
+ fragment.onStart();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(onStartRunnable);
+ }
+
+ private void setCurrentItem(int position) {
+ Runnable setCurrentItemRunnable = () -> {
+ viewPager.setCurrentItem(position);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(setCurrentItemRunnable);
+ }
+
+ private void clickView(View view) {
+ Runnable clickRunnable = () -> {
+ view.performClick();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(clickRunnable);
+ }
+
+ private void clickFab() {
+ Runnable clickRunnable = () -> {
+ fab.performClick();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(clickRunnable);
+ }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerItemFragmentTest.java b/tests/src/com/android/deskclock/timer/TimerItemFragmentTest.java
new file mode 100644
index 0000000..1152b91
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerItemFragmentTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Exercise the user interface that shows current timers.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerItemFragmentTest {
+
+ @Rule
+ public ActivityTestRule<DeskClock> rule = new ActivityTestRule<>(DeskClock.class, true);
+
+ @Test
+ public void ensureTimerIsHeldSuccessfully_whenOneTimerIsRunning() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final TimerFragment timerFragment = new TimerFragment();
+ rule.getActivity().getSupportFragmentManager()
+ .beginTransaction().add(timerFragment, null).commit();
+ Runnable selectTabRunnable = () -> {
+ timerFragment.selectTab();
+ Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+
+ // Get the view held by the TimerFragment
+ final View view = timerFragment.getView();
+ assertNotNull(view);
+
+ // Get the TimerPagerAdapter associated with this view
+ ViewPager viewPager = (ViewPager) view.findViewById(R.id.vertical_view_pager);
+ TimerPagerAdapter adapter = (TimerPagerAdapter) viewPager.getAdapter();
+ ViewGroup viewGroup = view.findViewById(R.id.timer_view);
+
+ // Retrieve the TimerItemFragment from the adapter
+ TimerItemFragment timerItemFragment =
+ (TimerItemFragment) adapter.instantiateItem(viewGroup, 0);
+
+ // Assert that the correct timer is set
+ assertEquals(timerItemFragment.getTimer(), timer);
+ DataModel.getDataModel().removeTimer(timer);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(selectTabRunnable);
+ }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerServiceTest.java b/tests/src/com/android/deskclock/timer/TimerServiceTest.java
new file mode 100644
index 0000000..985fd0d
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerServiceTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer;
+
+import android.content.ComponentName;
+import android.content.Intent;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.app.Service.START_NOT_STICKY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerServiceTest {
+
+ private TimerService timerService;
+ private DataModel dataModel;
+
+ @Before
+ public void setUp() {
+ dataModel = DataModel.getDataModel();
+ timerService = new TimerService();
+ timerService.onCreate();
+ }
+
+ @After
+ public void tearDown() {
+ clearTimers();
+ dataModel = null;
+ timerService = null;
+ }
+
+ private void clearTimers() {
+ Runnable clearTimersRunnable = () -> {
+ final List<Timer> timers = new ArrayList<>(DataModel.getDataModel().getTimers());
+ for (Timer timer : timers) {
+ DataModel.getDataModel().removeTimer(timer);
+ }
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(clearTimersRunnable);
+ }
+
+ @Test
+ public void verifyIntentsHonored_whileTimersFire() {
+ Runnable testRunnable = () -> {
+ Timer timer1 = dataModel.addTimer(60000L, null, false);
+ Timer timer2 = dataModel.addTimer(60000L, null, false);
+ dataModel.startTimer(timer1);
+ dataModel.startTimer(timer2);
+ timer1 = dataModel.getTimer(timer1.getId());
+ timer2 = dataModel.getTimer(timer2.getId());
+
+ // Expire the first timer.
+ dataModel.expireTimer(null, timer1);
+
+ // Have TimerService honor the Intent.
+ assertEquals(START_NOT_STICKY,
+ timerService.onStartCommand(getTimerServiceIntent(), 0, 0));
+
+ // Expire the second timer.
+ dataModel.expireTimer(null, timer2);
+
+ // Have TimerService honor the Intent which updates the firing timers.
+ assertEquals(START_NOT_STICKY,
+ timerService.onStartCommand(getTimerServiceIntent(), 0, 1));
+
+ // Reset timer 1.
+ dataModel.resetTimer(dataModel.getTimer(timer1.getId()));
+
+ // Have TimerService honor the Intent which updates the firing timers.
+ assertEquals(START_NOT_STICKY,
+ timerService.onStartCommand(getTimerServiceIntent(), 0, 2));
+
+ // Remove timer 2.
+ dataModel.removeTimer(dataModel.getTimer(timer2.getId()));
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunnable);
+ }
+
+ @Test
+ public void verifyIntentsHonored_ifNoTimersAreExpired() {
+ Runnable testRunnable = () -> {
+ Timer timer = dataModel.addTimer(60000L, null, false);
+ dataModel.startTimer(timer);
+ timer = dataModel.getTimer(timer.getId());
+
+ // Expire the timer.
+ dataModel.expireTimer(null, timer);
+
+ final Intent timerServiceIntent = getTimerServiceIntent();
+
+ // Reset the timer before TimerService starts.
+ dataModel.resetTimer(dataModel.getTimer(timer.getId()));
+
+ // Have TimerService honor the Intent.
+ assertEquals(START_NOT_STICKY, timerService.onStartCommand(timerServiceIntent, 0, 0));
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunnable);
+ }
+
+ private Intent getTimerServiceIntent() {
+ Intent serviceIntent = new Intent(ApplicationProvider.getApplicationContext(),
+ TimerService.class);
+
+ final ComponentName component = serviceIntent.getComponent();
+ assertNotNull(component);
+ assertEquals(TimerService.class.getName(), component.getClassName());
+
+ return serviceIntent;
+ }
+}
diff --git a/tests/src/com/android/deskclock/timer/TimerSetupViewTest.java b/tests/src/com/android/deskclock/timer/TimerSetupViewTest.java
new file mode 100644
index 0000000..ab0e61e
--- /dev/null
+++ b/tests/src/com/android/deskclock/timer/TimerSetupViewTest.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.timer;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.IdRes;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Timer;
+import com.android.deskclock.widget.MockFabContainer;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Exercise the user interface that collects new timer lengths.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TimerSetupViewTest {
+
+ private MockFabContainer fabContainer;
+ private TimerSetupView timerSetupView;
+
+ private TextView timeView;
+ private View deleteView;
+
+ private Locale defaultLocale;
+
+ @Rule
+ public ActivityTestRule<DeskClock> rule = new ActivityTestRule<>(DeskClock.class, true);
+
+ @Before
+ public void setUp() {
+ defaultLocale = Locale.getDefault();
+ Locale.setDefault(new Locale("en", "US"));
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ final TimerFragment fragment = new TimerFragment();
+ rule.getActivity().getSupportFragmentManager()
+ .beginTransaction().add(fragment, null).commit();
+ Runnable selectTabRunnable = () -> {
+ fragment.selectTab();
+ fabContainer = new MockFabContainer(fragment, context);
+ fragment.setFabContainer(fabContainer);
+
+ // Fetch the child views the tests will manipulate.
+ final View view = fragment.getView();
+ assertNotNull(view);
+
+ timerSetupView = view.findViewById(R.id.timer_setup);
+ assertNotNull(timerSetupView);
+ assertEquals(VISIBLE, timerSetupView.getVisibility());
+
+ timeView = timerSetupView.findViewById(R.id.timer_setup_time);
+ timeView.setActivated(true);
+ deleteView = timerSetupView.findViewById(R.id.timer_setup_delete);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(selectTabRunnable);
+ }
+
+ @After
+ public void tearDown() {
+ fabContainer = null;
+ timerSetupView = null;
+
+ timeView = null;
+ deleteView = null;
+
+ Locale.setDefault(defaultLocale);
+ }
+
+ private void validateDefaultState() {
+ assertIsReset();
+ }
+
+ @Test
+ public void validateDefaultState_TimersExist() {
+ Runnable addTimerRunnable = () -> {
+ Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+ validateDefaultState();
+ DataModel.getDataModel().removeTimer(timer);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+ }
+
+ @Test
+ public void validateDefaultState_NoTimersExist() {
+ Runnable runnable = () -> {
+ validateDefaultState();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ }
+
+ private void type0InDefaultState() {
+ performClick(R.id.timer_setup_digit_0);
+ assertIsReset();
+ }
+
+ @Test
+ public void type0InDefaultState_TimersExist() {
+ Runnable addTimerRunnable = () -> {
+ Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+ type0InDefaultState();
+ DataModel.getDataModel().removeTimer(timer);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+ }
+
+ @Test
+ public void type0InDefaultState_NoTimersExist() {
+ Runnable runnable = () -> {
+ type0InDefaultState();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ }
+
+ private void fillDisplayThenDeleteAll() {
+ assertIsReset();
+ // Fill the display.
+ performClick(R.id.timer_setup_digit_1);
+ assertHasValue(0, 0, 1);
+ performClick(R.id.timer_setup_digit_2);
+ assertHasValue(0, 0, 12);
+ performClick(R.id.timer_setup_digit_3);
+ assertHasValue(0, 1, 23);
+ performClick(R.id.timer_setup_digit_4);
+ assertHasValue(0, 12, 34);
+ performClick(R.id.timer_setup_digit_5);
+ assertHasValue(1, 23, 45);
+ performClick(R.id.timer_setup_digit_6);
+ assertHasValue(12, 34, 56);
+
+ // Typing another character is ignored.
+ performClick(R.id.timer_setup_digit_7);
+ assertHasValue(12, 34, 56);
+ performClick(R.id.timer_setup_digit_8);
+ assertHasValue(12, 34, 56);
+
+ // Delete everything in the display.
+ performClick(R.id.timer_setup_delete);
+ assertHasValue(1, 23, 45);
+ performClick(R.id.timer_setup_delete);
+ assertHasValue(0, 12, 34);
+ performClick(R.id.timer_setup_delete);
+ assertHasValue(0, 1, 23);
+ performClick(R.id.timer_setup_delete);
+ assertHasValue(0, 0, 12);
+ performClick(R.id.timer_setup_delete);
+ assertHasValue(0, 0, 1);
+ performClick(R.id.timer_setup_delete);
+ assertIsReset();
+ }
+
+ @Test
+ public void fillDisplayThenDeleteAll_TimersExist() {
+ Runnable addTimerRunnable = () -> {
+ Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+ fillDisplayThenDeleteAll();
+ DataModel.getDataModel().removeTimer(timer);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+ }
+
+ @Test
+ public void fillDisplayThenDeleteAll_NoTimersExist() {
+ Runnable runnable = () -> {
+ fillDisplayThenDeleteAll();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ }
+
+ private void fillDisplayWith9s() {
+ performClick(R.id.timer_setup_digit_9);
+ performClick(R.id.timer_setup_digit_9);
+ performClick(R.id.timer_setup_digit_9);
+ performClick(R.id.timer_setup_digit_9);
+ performClick(R.id.timer_setup_digit_9);
+ performClick(R.id.timer_setup_digit_9);
+ assertHasValue(99, 99, 99);
+ }
+
+ @Test
+ public void fillDisplayWith9s_TimersExist() {
+ Runnable addTimerRunnable = () -> {
+ Timer timer = DataModel.getDataModel().addTimer(5000L, "", false);
+ fillDisplayWith9s();
+ DataModel.getDataModel().removeTimer(timer);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(addTimerRunnable);
+ }
+
+ @Test
+ public void fillDisplayWith9s_NoTimersExist() {
+ Runnable runnable = () -> {
+ fillDisplayWith9s();
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable);
+ }
+
+ private void assertIsReset() {
+ assertStateEquals(0, 0, 0);
+ assertFalse(timerSetupView.hasValidInput());
+ assertEquals(0, timerSetupView.getTimeInMillis());
+
+ assertTrue(TextUtils.equals("00h 00m 00s", timeView.getText()));
+ assertTrue(TextUtils.equals("0 hours, 0 minutes, 0 seconds",
+ timeView.getContentDescription()));
+
+ assertFalse(deleteView.isEnabled());
+ assertTrue(TextUtils.equals("Delete", deleteView.getContentDescription()));
+
+ final View fab = fabContainer.getFab();
+ final TextView leftButton = fabContainer.getLeftButton();
+ final TextView rightButton = fabContainer.getRightButton();
+
+ if (DataModel.getDataModel().getTimers().isEmpty()) {
+ assertEquals(INVISIBLE, leftButton.getVisibility());
+ } else {
+ assertEquals(VISIBLE, leftButton.getVisibility());
+ assertTrue(TextUtils.equals("Cancel", leftButton.getText()));
+ }
+
+ assertNull(fab.getContentDescription());
+ assertEquals(INVISIBLE, fab.getVisibility());
+ assertEquals(INVISIBLE, rightButton.getVisibility());
+ }
+
+ private void assertHasValue(int hours, int minutes, int seconds) {
+ final long time =
+ hours * HOUR_IN_MILLIS + minutes * MINUTE_IN_MILLIS + seconds * SECOND_IN_MILLIS;
+ assertStateEquals(hours, minutes, seconds);
+ assertTrue(timerSetupView.hasValidInput());
+ assertEquals(time, timerSetupView.getTimeInMillis());
+
+ final String timeString =
+ String.format(Locale.US, "%02dh %02dm %02ds", hours, minutes, seconds);
+ assertTrue(TextUtils.equals(timeString, timeView.getText()));
+
+ assertTrue(deleteView.isEnabled());
+ assertTrue(TextUtils.equals("Delete " + seconds % 10, deleteView.getContentDescription()));
+
+ final View fab = fabContainer.getFab();
+ final TextView leftButton = fabContainer.getLeftButton();
+ final TextView rightButton = fabContainer.getRightButton();
+
+ if (DataModel.getDataModel().getTimers().isEmpty()) {
+ assertEquals(INVISIBLE, leftButton.getVisibility());
+ } else {
+ assertEquals(VISIBLE, leftButton.getVisibility());
+ assertTrue(TextUtils.equals("Cancel", leftButton.getText()));
+ }
+
+ assertEquals(VISIBLE, fab.getVisibility());
+ assertTrue(TextUtils.equals("Start", fab.getContentDescription()));
+ assertEquals(INVISIBLE, rightButton.getVisibility());
+ }
+
+ private void assertStateEquals(int hours, int minutes, int seconds) {
+ final int[] expected = {
+ seconds % 10, seconds / 10, minutes % 10, minutes / 10, hours % 10, hours / 10
+ };
+ final int[] actual = (int[]) timerSetupView.getState();
+ assertArrayEquals(expected, actual);
+ }
+
+ private void performClick(@IdRes int id) {
+ final View view = timerSetupView.findViewById(id);
+ assertNotNull(view);
+ assertTrue(view.performClick());
+ }
+}
diff --git a/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java b/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java
new file mode 100644
index 0000000..9893545
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/FormattedStringModelTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class FormattedStringModelTest {
+
+ private FormattedStringModel model;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ model = new FormattedStringModel(context);
+ }
+
+ @After
+ public void tearDown() {
+ model = null;
+ }
+
+ @Test
+ public void positiveFormattedNumberWithNoPadding() {
+ assertEquals("0", model.getFormattedNumber(0));
+ assertEquals("9", model.getFormattedNumber(9));
+ assertEquals("10", model.getFormattedNumber(10));
+ assertEquals("99", model.getFormattedNumber(99));
+ assertEquals("100", model.getFormattedNumber(100));
+ }
+
+ @Test
+ public void positiveFormattedNumber() {
+ assertEquals("0", model.getFormattedNumber(false, 0, 1));
+ assertEquals("00", model.getFormattedNumber(false, 0, 2));
+ assertEquals("000", model.getFormattedNumber(false, 0, 3));
+
+ assertEquals("9", model.getFormattedNumber(false, 9, 1));
+ assertEquals("09", model.getFormattedNumber(false, 9, 2));
+ assertEquals("009", model.getFormattedNumber(false, 9, 3));
+
+ assertEquals("90", model.getFormattedNumber(false, 90, 2));
+ assertEquals("090", model.getFormattedNumber(false, 90, 3));
+
+ assertEquals("999", model.getFormattedNumber(false, 999, 3));
+ }
+
+ @Test
+ public void negativeFormattedNumber() {
+ assertEquals("−0", model.getFormattedNumber(true, 0, 1));
+ assertEquals("−00", model.getFormattedNumber(true, 0, 2));
+ assertEquals("−000", model.getFormattedNumber(true, 0, 3));
+
+ assertEquals("−9", model.getFormattedNumber(true, 9, 1));
+ assertEquals("−09", model.getFormattedNumber(true, 9, 2));
+ assertEquals("−009", model.getFormattedNumber(true, 9, 3));
+
+ assertEquals("−90", model.getFormattedNumber(true, 90, 2));
+ assertEquals("−090", model.getFormattedNumber(true, 90, 3));
+
+ assertEquals("−999", model.getFormattedNumber(true, 999, 3));
+ }
+}
diff --git a/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java b/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java
new file mode 100644
index 0000000..3df7404
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/PeriodicCallbackModelTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata;
+
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.HOUR;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.MIDNIGHT;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.MINUTE;
+import static com.android.deskclock.uidata.PeriodicCallbackModel.Period.QUARTER_HOUR;
+
+import static java.util.Calendar.MILLISECOND;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class PeriodicCallbackModelTest {
+
+ @Test
+ public void getMinuteDelay() {
+ assertEquals(1, PeriodicCallbackModel.getDelay(56999, MINUTE, -3000));
+ assertEquals(60000, PeriodicCallbackModel.getDelay(57000, MINUTE, -3000));
+ assertEquals(59999, PeriodicCallbackModel.getDelay(57001, MINUTE, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(59999, MINUTE, 0));
+ assertEquals(60000, PeriodicCallbackModel.getDelay(60000, MINUTE, 0));
+ assertEquals(59999, PeriodicCallbackModel.getDelay(60001, MINUTE, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(59999, MINUTE, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(60000, MINUTE, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(62999, MINUTE, 3000));
+ assertEquals(60000, PeriodicCallbackModel.getDelay(63000, MINUTE, 3000));
+ assertEquals(59999, PeriodicCallbackModel.getDelay(63001, MINUTE, 3000));
+ }
+
+ @Test
+ public void getQuarterHourDelay() {
+ assertEquals(1, PeriodicCallbackModel.getDelay(896999, QUARTER_HOUR, -3000));
+ assertEquals(900000, PeriodicCallbackModel.getDelay(897000, QUARTER_HOUR, -3000));
+ assertEquals(899999, PeriodicCallbackModel.getDelay(897001, QUARTER_HOUR, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(899999, QUARTER_HOUR, 0));
+ assertEquals(900000, PeriodicCallbackModel.getDelay(900000, QUARTER_HOUR, 0));
+ assertEquals(899999, PeriodicCallbackModel.getDelay(900001, QUARTER_HOUR, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(899999, QUARTER_HOUR, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(900000, QUARTER_HOUR, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(902999, QUARTER_HOUR, 3000));
+ assertEquals(900000, PeriodicCallbackModel.getDelay(903000, QUARTER_HOUR, 3000));
+ assertEquals(899999, PeriodicCallbackModel.getDelay(903001, QUARTER_HOUR, 3000));
+ }
+
+ @Test
+ public void getHourDelay() {
+ assertEquals(1, PeriodicCallbackModel.getDelay(3596999, HOUR, -3000));
+ assertEquals(3600000, PeriodicCallbackModel.getDelay(3597000, HOUR, -3000));
+ assertEquals(3599999, PeriodicCallbackModel.getDelay(3597001, HOUR, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(3599999, HOUR, 0));
+ assertEquals(3600000, PeriodicCallbackModel.getDelay(3600000, HOUR, 0));
+ assertEquals(3599999, PeriodicCallbackModel.getDelay(3600001, HOUR, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(3599999, HOUR, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(3600000, HOUR, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(3602999, HOUR, 3000));
+ assertEquals(3600000, PeriodicCallbackModel.getDelay(3603000, HOUR, 3000));
+ assertEquals(3599999, PeriodicCallbackModel.getDelay(3603001, HOUR, 3000));
+ }
+
+ @Test
+ public void getMidnightDelay() {
+ final Calendar c = Calendar.getInstance();
+ c.set(2016, 0, 20, 0, 0, 0);
+ c.set(MILLISECOND, 0);
+ final long now = c.getTimeInMillis();
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(now - 3001, MIDNIGHT, -3000));
+ assertEquals(86400000, PeriodicCallbackModel.getDelay(now - 3000, MIDNIGHT, -3000));
+ assertEquals(86399999, PeriodicCallbackModel.getDelay(now - 2999, MIDNIGHT, -3000));
+
+ assertEquals(1, PeriodicCallbackModel.getDelay(now - 1, MIDNIGHT, 0));
+ assertEquals(86400000, PeriodicCallbackModel.getDelay(now, MIDNIGHT, 0));
+ assertEquals(86399999, PeriodicCallbackModel.getDelay(now + 1, MIDNIGHT, 0));
+
+ assertEquals(3001, PeriodicCallbackModel.getDelay(now - 1, MIDNIGHT, 3000));
+ assertEquals(3000, PeriodicCallbackModel.getDelay(now, MIDNIGHT, 3000));
+ assertEquals(1, PeriodicCallbackModel.getDelay(now + 2999, MIDNIGHT, 3000));
+ assertEquals(86400000, PeriodicCallbackModel.getDelay(now + 3000, MIDNIGHT, 3000));
+ assertEquals(86399999, PeriodicCallbackModel.getDelay(now + 3001, MIDNIGHT, 3000));
+ }
+}
diff --git a/tests/src/com/android/deskclock/uidata/TabModelTest.java b/tests/src/com/android/deskclock/uidata/TabModelTest.java
new file mode 100644
index 0000000..dbe9344
--- /dev/null
+++ b/tests/src/com/android/deskclock/uidata/TabModelTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.uidata;
+
+import android.content.Context;
+import android.preference.PreferenceManager;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Locale;
+
+import static com.android.deskclock.uidata.UiDataModel.Tab.ALARMS;
+import static com.android.deskclock.uidata.UiDataModel.Tab.CLOCKS;
+import static com.android.deskclock.uidata.UiDataModel.Tab.STOPWATCH;
+import static com.android.deskclock.uidata.UiDataModel.Tab.TIMERS;
+
+import static org.junit.Assert.assertSame;
+
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class TabModelTest {
+
+ private Locale defaultLocale;
+ private TabModel tabModel;
+
+ @Test
+ public void ltrTabLayoutIndex() {
+ setUpTabModel(new Locale("en", "US"));
+ assertSame(ALARMS, tabModel.getTabAt(0));
+ assertSame(CLOCKS, tabModel.getTabAt(1));
+ assertSame(TIMERS, tabModel.getTabAt(2));
+ assertSame(STOPWATCH, tabModel.getTabAt(3));
+ Locale.setDefault(defaultLocale);
+ }
+
+ @Test
+ public void rtlTabLayoutIndex() {
+ setUpTabModel(new Locale("ar", "EG"));
+ assertSame(STOPWATCH, tabModel.getTabAt(0));
+ assertSame(TIMERS, tabModel.getTabAt(1));
+ assertSame(CLOCKS, tabModel.getTabAt(2));
+ assertSame(ALARMS, tabModel.getTabAt(3));
+ Locale.setDefault(defaultLocale);
+ }
+
+ private void setUpTabModel(Locale testLocale) {
+ defaultLocale = Locale.getDefault();
+ Locale.setDefault(testLocale);
+ final Context context = ApplicationProvider.getApplicationContext();
+ tabModel = new TabModel(PreferenceManager.getDefaultSharedPreferences(context));
+ }
+}
diff --git a/tests/src/com/android/deskclock/widget/MockFabContainer.java b/tests/src/com/android/deskclock/widget/MockFabContainer.java
new file mode 100644
index 0000000..bdfbf7d
--- /dev/null
+++ b/tests/src/com/android/deskclock/widget/MockFabContainer.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import com.android.deskclock.DeskClockFragment;
+import com.android.deskclock.FabContainer;
+
+/**
+ * DeskClock is the normal container for the fab and its left and right buttons. In order to test
+ * each tab in isolation, tests avoid inflating all of DeskClock and instead set this mock fab
+ * container into the DeskClockFragment under test. It mimics the behavior of a fab container,
+ * albeit without animation, so fragment tests can verify the state of the fab any time they like.
+ */
+public final class MockFabContainer implements FabContainer {
+
+ private final DeskClockFragment deskClockFragment;
+
+ private ImageView fab;
+ private Button leftButton;
+ private Button rightButton;
+
+ public MockFabContainer(DeskClockFragment fragment, Context context) {
+ deskClockFragment = fragment;
+ fab = new ImageView(context);
+ leftButton = new Button(context);
+ rightButton = new Button(context);
+
+ updateFab(FabContainer.FAB_AND_BUTTONS_IMMEDIATE);
+
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ deskClockFragment.onFabClick(fab);
+ }
+ });
+ leftButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ deskClockFragment.onLeftButtonClick(leftButton);
+ }
+ });
+ rightButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ deskClockFragment.onRightButtonClick(rightButton);
+ }
+ });
+ }
+
+ @Override
+ public void updateFab(@UpdateFabFlag int updateType) {
+ if ((updateType & FabContainer.FAB_ANIMATION_MASK) != 0) {
+ deskClockFragment.onUpdateFab(fab);
+ }
+ if ((updateType & FabContainer.FAB_REQUEST_FOCUS_MASK) != 0) {
+ fab.requestFocus();
+ }
+ if ((updateType & FabContainer.BUTTONS_ANIMATION_MASK) != 0) {
+ deskClockFragment.onUpdateFabButtons(leftButton, rightButton);
+ }
+ if ((updateType & FabContainer.BUTTONS_DISABLE_MASK) != 0) {
+ leftButton.setClickable(false);
+ rightButton.setClickable(false);
+ }
+ }
+
+ public ImageView getFab() {
+ return fab;
+ }
+
+ public Button getLeftButton() {
+ return leftButton;
+ }
+
+ public Button getRightButton() {
+ return rightButton;
+ }
+}
diff --git a/tests/src/com/android/deskclock/worldclock/CitySelectionActivityTest.java b/tests/src/com/android/deskclock/worldclock/CitySelectionActivityTest.java
new file mode 100644
index 0000000..46e0030
--- /dev/null
+++ b/tests/src/com/android/deskclock/worldclock/CitySelectionActivityTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.worldclock;
+
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import androidx.appcompat.widget.SearchView;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.deskclock.R;
+import com.android.deskclock.data.City;
+import com.android.deskclock.data.DataModel;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.Locale;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Exercise the user interface that adjusts the selected cities.
+ */
+@RunWith(AndroidJUnit4ClassRunner.class)
+public class CitySelectionActivityTest {
+
+ private CitySelectionActivity activity;
+ private ListView cities;
+ private ListAdapter citiesAdapter;
+ private SearchView searchView;
+ private Locale defaultLocale;
+
+ @Rule
+ public ActivityTestRule<CitySelectionActivity> rule =
+ new ActivityTestRule<>(CitySelectionActivity.class, true);
+
+ @Before
+ public void setUp() {
+ defaultLocale = Locale.getDefault();
+ Locale.setDefault(new Locale("en", "US"));
+ Runnable setUpRunnable = () -> {
+ final City city = DataModel.getDataModel().getAllCities().get(0);
+ DataModel.getDataModel().setSelectedCities(Collections.singletonList(city));
+ activity = rule.getActivity();
+ cities = activity.findViewById(R.id.cities_list);
+ citiesAdapter = (ListAdapter) cities.getAdapter();
+ searchView = activity.findViewById(R.id.menu_item_search);
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(setUpRunnable);
+ }
+
+ @After
+ public void tearDown() {
+ activity = null;
+ cities = null;
+ searchView = null;
+ Locale.setDefault(defaultLocale);
+ }
+
+ @Test
+ public void validateDefaultState() {
+ Runnable testRunable = () -> {
+ assertNotNull(searchView);
+ assertNotNull(cities);
+ assertNotNull(citiesAdapter);
+ assertEquals(340, citiesAdapter.getCount());
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunable);
+ }
+
+ @Test
+ public void searchCities() {
+ Runnable testRunable = () -> {
+ // Search for cities starting with Z.
+ searchView.setQuery("Z", true);
+ assertEquals(2, citiesAdapter.getCount());
+ assertItemContent(0, "Zagreb");
+ assertItemContent(1, "Zurich");
+
+ // Clear the filter query.
+ searchView.setQuery("", true);
+ assertEquals(340, citiesAdapter.getCount());
+ };
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(testRunable);
+ }
+
+ private void assertItemContent(int index, String cityName) {
+ final City city = (City) citiesAdapter.getItem(index);
+ assertEquals(cityName, city.getName());
+ }
+}