Update 'next alarm' tracking
Also improves logging and adds tests.
Test: runtest systemui-notification
Bug: 67028535
Bug: 69440234
Change-Id: I259fdc2d253d2a4ac415e23bd66a0b9d7c69b053
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 735b822..1ec2406 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -890,7 +890,17 @@
}
public static boolean isValidScheduleConditionId(Uri conditionId) {
- return tryParseScheduleConditionId(conditionId) != null;
+ ScheduleInfo info;
+ try {
+ info = tryParseScheduleConditionId(conditionId);
+ } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
+ return false;
+ }
+
+ if (info == null || info.days == null || info.days.length == 0) {
+ return false;
+ }
+ return true;
}
public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index fac254a..863f17b 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.systemui.HardwareUiLayout
+<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -45,4 +45,4 @@
</LinearLayout>
</RelativeLayout>
-</com.android.systemui.HardwareUiLayout>
+</RelativeLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 0d41e20..ca23980 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -19,6 +19,9 @@
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
import static com.android.systemui.volume.Events.DISMISS_REASON_TOUCH_OUTSIDE;
import android.accessibilityservice.AccessibilityServiceInfo;
@@ -42,6 +45,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.provider.Settings;
import android.provider.Settings.Global;
import android.util.Log;
import android.util.Slog;
@@ -67,6 +71,7 @@
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
+import com.android.systemui.HardwareBgDrawable;
import com.android.systemui.HardwareUiLayout;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
@@ -74,6 +79,7 @@
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.plugins.VolumeDialogController.State;
import com.android.systemui.plugins.VolumeDialogController.StreamState;
+import com.android.systemui.util.leak.RotationUtils;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -97,9 +103,13 @@
private final VolumeDialogController mController;
private Window mWindow;
- private HardwareUiLayout mHardwareLayout;
+ //private HardwareUiLayout mHardwareLayout;
private CustomDialog mDialog;
private ViewGroup mDialogView;
+ private boolean mEdgeBleed;
+ private boolean mRoundedDivider;
+ private HardwareBgDrawable mBackground;
+ private int mRotation = ROTATION_NONE;
private ViewGroup mDialogRowsView;
private ViewGroup mDialogContentView;
private final List<VolumeRow> mRows = new ArrayList<>();
@@ -111,6 +121,8 @@
private final Accessibility mAccessibility = new Accessibility();
private final ColorStateList mActiveSliderTint;
private final ColorStateList mInactiveSliderTint;
+ private static final String EDGE_BLEED = "sysui_hwui_edge_bleed";
+ private static final String ROUNDED_DIVIDER = "sysui_hwui_rounded_divider";
private boolean mShowing;
private boolean mShowA11yStream;
@@ -181,8 +193,16 @@
return true;
}
});
- mHardwareLayout = HardwareUiLayout.get(mDialogView);
- mHardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
+
+ mEdgeBleed = Settings.Secure.getInt(mContext.getContentResolver(),
+ EDGE_BLEED, 0) != 0;
+ mRoundedDivider = Settings.Secure.getInt(mContext.getContentResolver(),
+ ROUNDED_DIVIDER, 1) != 0;
+ updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
+ mBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, mContext);
+ mDialogView.setBackground(mBackground);
+ //mHardwareLayout = HardwareUiLayout.get(mDialogView);
+ //mHardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
mDialogContentView = mDialog.findViewById(R.id.volume_dialog_content);
mDialogRowsView = mDialogContentView.findViewById(R.id.volume_dialog_rows);
@@ -210,6 +230,25 @@
updateRowsH(getActiveRow());
}
+ private int getEdgePadding() {
+ return mContext.getResources().getDimensionPixelSize(R.dimen.edge_margin);
+ }
+
+ private void updateEdgeMargin(int edge) {
+ if (mDialogView != null) {
+ mRotation = RotationUtils.getRotation(mContext);
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mDialogView.getLayoutParams();
+ if (mRotation == ROTATION_LANDSCAPE) {
+ params.topMargin = edge;
+ } else if (mRotation == ROTATION_SEASCAPE) {
+ params.bottomMargin = edge;
+ } else {
+ params.rightMargin = edge;
+ }
+ mDialogView.setLayoutParams(params);
+ }
+ }
+
private ColorStateList loadColorStateList(int colorResId) {
return ColorStateList.valueOf(mContext.getColor(colorResId));
}
@@ -389,11 +428,11 @@
rescheduleTimeoutH();
if (mShowing) return;
mShowing = true;
- mHardwareLayout.setTranslationX(getAnimTranslation());
- mHardwareLayout.setAlpha(0);
- mHardwareLayout.animate()
+ mDialogView.setTranslationY(getAnimTranslation());
+ mDialogView.setAlpha(0);
+ mDialogView.animate()
.alpha(1)
- .translationX(0)
+ .translationY(0)
.setDuration(300)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.withEndAction(() -> {
@@ -432,9 +471,9 @@
mHandler.removeMessages(H.SHOW);
if (!mShowing) return;
mShowing = false;
- mHardwareLayout.setTranslationX(0);
- mHardwareLayout.setAlpha(1);
- mHardwareLayout.animate()
+ mDialogView.setTranslationX(0);
+ mDialogView.setAlpha(1);
+ mDialogView.animate()
.alpha(0)
.translationX(getAnimTranslation())
.setDuration(300)
diff --git a/services/core/java/com/android/server/notification/ScheduleCalendar.java b/services/core/java/com/android/server/notification/ScheduleCalendar.java
index 40230bd..5ff0e21 100644
--- a/services/core/java/com/android/server/notification/ScheduleCalendar.java
+++ b/services/core/java/com/android/server/notification/ScheduleCalendar.java
@@ -18,6 +18,7 @@
import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.util.ArraySet;
+import android.util.Log;
import java.util.Calendar;
import java.util.Objects;
@@ -41,10 +42,25 @@
}
public void maybeSetNextAlarm(long now, long nextAlarm) {
- if (mSchedule != null) {
- if (mSchedule.exitAtAlarm
- && (now > mSchedule.nextAlarm || nextAlarm < mSchedule.nextAlarm)) {
- mSchedule.nextAlarm = nextAlarm;
+ if (mSchedule != null && mSchedule.exitAtAlarm) {
+ // alarm canceled
+ if (nextAlarm == 0) {
+ mSchedule.nextAlarm = 0;
+ }
+ // only allow alarms in the future
+ if (nextAlarm > now) {
+ // store earliest alarm
+ if (mSchedule.nextAlarm == 0) {
+ mSchedule.nextAlarm = nextAlarm;
+ } else {
+ mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
+ }
+ } else if (mSchedule.nextAlarm < now) {
+ if (ScheduleConditionProvider.DEBUG) {
+ Log.d(ScheduleConditionProvider.TAG,
+ "All alarms are in the past " + mSchedule.nextAlarm);
+ }
+ mSchedule.nextAlarm = 0;
}
}
}
@@ -87,6 +103,9 @@
}
public boolean shouldExitForAlarm(long time) {
+ if (mSchedule == null) {
+ return false;
+ }
return mSchedule.exitAtAlarm
&& mSchedule.nextAlarm != 0
&& time >= mSchedule.nextAlarm;
diff --git a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
index 50a51b2..c5f80bb 100644
--- a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
+++ b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
@@ -37,6 +37,8 @@
import android.util.Log;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
@@ -62,10 +64,9 @@
private static final String SEPARATOR = ";";
private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
-
private final Context mContext = this;
private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
- private ArraySet<Uri> mSnoozed = new ArraySet<>();
+ private ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
private AlarmManager mAlarmManager;
private boolean mConnected;
@@ -102,7 +103,7 @@
pw.println(mSubscriptions.get(conditionId).toString());
}
}
- pw.println(" snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozed));
+ pw.println(" snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozedForAlarm));
dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
}
@@ -129,7 +130,7 @@
public void onSubscribe(Uri conditionId) {
if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
- notifyCondition(createCondition(conditionId, Condition.STATE_FALSE, "badCondition"));
+ notifyCondition(createCondition(conditionId, Condition.STATE_ERROR, "invalidId"));
return;
}
synchronized (mSubscriptions) {
@@ -169,32 +170,11 @@
synchronized (mSubscriptions) {
setRegistered(!mSubscriptions.isEmpty());
for (Uri conditionId : mSubscriptions.keySet()) {
- final ScheduleCalendar cal = mSubscriptions.get(conditionId);
- if (cal != null && cal.isInSchedule(now)) {
- if (conditionSnoozed(conditionId) || cal.shouldExitForAlarm(now)) {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_FALSE, "alarmCanceled"));
- addSnoozed(conditionId);
- } else {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_TRUE, "meetsSchedule"));
- }
- cal.maybeSetNextAlarm(now, nextUserAlarmTime);
- } else {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_FALSE, "!meetsSchedule"));
- removeSnoozed(conditionId);
- if (cal != null && nextUserAlarmTime == 0) {
- cal.maybeSetNextAlarm(now, nextUserAlarmTime);
- }
- }
- if (cal != null) {
- final long nextChangeTime = cal.getNextChangeTime(now);
- if (nextChangeTime > 0 && nextChangeTime > now) {
- if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
- mNextAlarmTime = nextChangeTime;
- }
- }
+ Condition condition =
+ evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
+ now, nextUserAlarmTime);
+ if (condition != null) {
+ conditionsToNotify.add(condition);
}
}
}
@@ -202,6 +182,39 @@
updateAlarm(now, mNextAlarmTime);
}
+ @VisibleForTesting
+ @GuardedBy("mSubscriptions")
+ Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
+ long now, long nextUserAlarmTime) {
+ Condition condition;
+ if (cal == null) {
+ condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
+ removeSnoozed(conditionId);
+ return condition;
+ }
+ if (cal.isInSchedule(now)) {
+ if (conditionSnoozed(conditionId)) {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
+ } else if (cal.shouldExitForAlarm(now)) {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
+ addSnoozed(conditionId);
+ } else {
+ condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
+ }
+ } else {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
+ removeSnoozed(conditionId);
+ }
+ cal.maybeSetNextAlarm(now, nextUserAlarmTime);
+ final long nextChangeTime = cal.getNextChangeTime(now);
+ if (nextChangeTime > 0 && nextChangeTime > now) {
+ if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
+ mNextAlarmTime = nextChangeTime;
+ }
+ }
+ return condition;
+ }
+
private void updateAlarm(long now, long time) {
final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
@@ -266,27 +279,28 @@
}
private boolean conditionSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- return mSnoozed.contains(conditionId);
+ synchronized (mSnoozedForAlarm) {
+ return mSnoozedForAlarm.contains(conditionId);
}
}
- private void addSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- mSnoozed.add(conditionId);
+ @VisibleForTesting
+ void addSnoozed(Uri conditionId) {
+ synchronized (mSnoozedForAlarm) {
+ mSnoozedForAlarm.add(conditionId);
saveSnoozedLocked();
}
}
private void removeSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- mSnoozed.remove(conditionId);
+ synchronized (mSnoozedForAlarm) {
+ mSnoozedForAlarm.remove(conditionId);
saveSnoozedLocked();
}
}
- public void saveSnoozedLocked() {
- final String setting = TextUtils.join(SEPARATOR, mSnoozed);
+ private void saveSnoozedLocked() {
+ final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
final int currentUser = ActivityManager.getCurrentUser();
Settings.Secure.putStringForUser(mContext.getContentResolver(),
SCP_SETTING,
@@ -294,8 +308,8 @@
currentUser);
}
- public void readSnoozed() {
- synchronized (mSnoozed) {
+ private void readSnoozed() {
+ synchronized (mSnoozedForAlarm) {
long identity = Binder.clearCallingIdentity();
try {
final String setting = Settings.Secure.getStringForUser(
@@ -312,7 +326,7 @@
if (TextUtils.isEmpty(token)) {
continue;
}
- mSnoozed.add(Uri.parse(token));
+ mSnoozedForAlarm.add(Uri.parse(token));
}
}
} finally {
diff --git a/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java b/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java
new file mode 100644
index 0000000..cbda12d
--- /dev/null
+++ b/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java
@@ -0,0 +1,327 @@
+/*
+ * 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.server.notification;
+
+import static junit.framework.Assert.assertFalse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.service.notification.ZenModeConfig;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Slog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScheduleCalendarTest extends NotificationTestCase {
+
+ private ScheduleCalendar mScheduleCalendar;
+ private ZenModeConfig.ScheduleInfo mScheduleInfo;
+
+ @Before
+ public void setUp() throws Exception {
+ mScheduleCalendar = new ScheduleCalendar();
+ mScheduleInfo = new ZenModeConfig.ScheduleInfo();
+ mScheduleInfo.days = new int[] {1, 2, 3, 4, 5};
+ mScheduleCalendar.setSchedule(mScheduleInfo);
+ }
+
+ @Test
+ public void testNullScheduleInfo() throws Exception {
+ mScheduleCalendar.setSchedule(null);
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 1999);
+ assertEquals(0, mScheduleCalendar.getNextChangeTime(1000));
+ assertFalse(mScheduleCalendar.isInSchedule(100));
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(100));
+ }
+
+ @Test
+ public void testGetNextChangeTime_startToday() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 1);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = cal.get(Calendar.HOUR_OF_DAY) + 1;
+ mScheduleInfo.endHour = cal.get(Calendar.HOUR_OF_DAY) + 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+ mScheduleCalendar.setSchedule(mScheduleInfo);
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testGetNextChangeTime_endToday() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 2);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = cal.get(Calendar.HOUR_OF_DAY) - 1;
+ mScheduleInfo.endHour = cal.get(Calendar.HOUR_OF_DAY) + 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.endHour);
+ expected.set(Calendar.MINUTE, mScheduleInfo.endMinute);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testGetNextChangeTime_startTomorrow() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay(), getTodayDay() + 1};
+ mScheduleInfo.startHour = 1;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.add(Calendar.DATE, 1);
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour);
+ expected.set(Calendar.MINUTE, mScheduleInfo.startMinute);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testGetNextChangeTime_endTomorrow() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay(), getTodayDay() + 1};
+ mScheduleInfo.startHour = 22;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.add(Calendar.DATE, 1);
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.endHour);
+ expected.set(Calendar.MINUTE, mScheduleInfo.endMinute);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testShouldExitForAlarm_settingOff() {
+ mScheduleInfo.exitAtAlarm = false;
+ mScheduleInfo.nextAlarm = 1000;
+
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(1000));
+ }
+
+ @Test
+ public void testShouldExitForAlarm_beforeAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 1000;
+
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(999));
+ }
+
+ @Test
+ public void testShouldExitForAlarm_noAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 0;
+
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(999));
+ }
+
+ @Test
+ public void testShouldExitForAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 1000;
+
+ assertTrue(mScheduleCalendar.shouldExitForAlarm(1000));
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_settingOff() {
+ mScheduleInfo.exitAtAlarm = false;
+ mScheduleInfo.nextAlarm = 0;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 2000);
+
+ assertEquals(0, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_settingOn() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 0;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 2000);
+
+ assertEquals(2000, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_alarmCanceled() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 10000;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 0);
+
+ assertEquals(0, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_earlierAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 2000;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 1500);
+
+ assertEquals(1500, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_laterAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 2000;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 3000);
+
+ assertEquals(2000, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_expiredAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 998;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 999);
+
+ assertEquals(0, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testIsInSchedule_inScheduleOvernight() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = 22;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+
+ assertTrue(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ @Test
+ public void testIsInSchedule_inScheduleSingleDay() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 14);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = 12;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 16;
+ mScheduleInfo.endMinute = 15;
+
+ assertTrue(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ @Test
+ public void testIsInSchedule_notToday() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 14);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
+ mScheduleInfo.days = new int[] {Calendar.FRIDAY, Calendar.SUNDAY};
+ mScheduleInfo.startHour = 12;
+ mScheduleInfo.startMinute = 16;
+ mScheduleInfo.endHour = 15;
+ mScheduleInfo.endMinute = 15;
+
+ assertFalse(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ @Test
+ public void testIsInSchedule_startingSoon() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 14);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 59);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = 14;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 16;
+ mScheduleInfo.endMinute = 15;
+
+ assertFalse(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ private int getTodayDay() {
+ return new GregorianCalendar().get(Calendar.DAY_OF_WEEK);
+ }
+}
diff --git a/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java b/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java
new file mode 100644
index 0000000..ddf46a0
--- /dev/null
+++ b/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java
@@ -0,0 +1,337 @@
+package com.android.server.notification;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Looper;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.support.test.InstrumentationRegistry;
+import android.test.ServiceTestCase;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class ScheduleConditionProviderTest extends ServiceTestCase<ScheduleConditionProvider> {
+
+ ScheduleConditionProvider mService;
+
+ @Rule
+ public final TestableContext mContext =
+ new TestableContext(InstrumentationRegistry.getContext(), null);
+
+ public ScheduleConditionProviderTest() {
+ super(ScheduleConditionProvider.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ Looper.prepare();
+
+ Intent startIntent =
+ new Intent("com.android.server.notification.ScheduleConditionProvider");
+ startIntent.setPackage("android");
+ bindService(startIntent);
+ mService = spy(getService());
+ }
+
+ @Test
+ public void testIsValidConditionId_incomplete() throws Exception {
+ Uri badConditionId = Uri.EMPTY;
+ assertFalse(mService.isValidConditionId(badConditionId));
+ assertEquals(Condition.STATE_ERROR,
+ mService.evaluateSubscriptionLocked(badConditionId, null, 0, 1000).state);
+ }
+
+ @Test
+ public void testIsValidConditionId() throws Exception {
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {1, 2, 4};
+ info.startHour = 8;
+ info.startMinute = 56;
+ info.nextAlarm = 1000;
+ info.exitAtAlarm = true;
+ info.endHour = 12;
+ info.endMinute = 9;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ assertTrue(mService.isValidConditionId(conditionId));
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_InSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = info.startMinute;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+ assertTrue(cal.isInSchedule(now.getTimeInMillis()));
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_TRUE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_InScheduleSnoozed() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = info.startMinute;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+ assertTrue(cal.isInSchedule(now.getTimeInMillis()));
+
+ mService.addSnoozed(conditionId);
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_beforeSchedule() {
+ Calendar now = new GregorianCalendar();
+ now.set(Calendar.HOUR_OF_DAY, 14);
+ now.set(Calendar.MINUTE, 15);
+ now.set(Calendar.SECOND, 59);
+ now.set(Calendar.MILLISECOND, 0);
+ now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+
+ // Schedule - 1 hour long; starts in 1 second
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE) + 1;
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = info.startMinute;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_endSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; ends now
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY) - 1;
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY);
+ info.endMinute = now.get(Calendar.MINUTE);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_alarmSetBeforeInSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // an hour before start, update with an alarm that will fire during the schedule
+ mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() - 1000, now.getTimeInMillis() + 1000);
+
+ // at start, should be in dnd
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 1000, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_alarmSetInSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), 0);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, update with alarm time, should be in dnd
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 1000, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_earlierAlarmSet() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd, alarm in 2000 ms
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 2000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, update with earlier alarm time, should be in dnd
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at earliest alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 1000, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_laterAlarmSet() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd, alarm in 500 ms
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, update with later alarm time, should be in dnd
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 250, now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at earliest alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 500));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 500));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_alarmCanceled() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd, alarm in 500 ms
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, cancel alarm
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 250, 0);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at previous alarm time, should not exit DND
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 500));
+ assertFalse(cal.shouldExitForAlarm(now.getTimeInMillis() + 500));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, 0);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // end of schedule, exit DND
+ now.add(Calendar.HOUR_OF_DAY, 1);
+ condition = mService.evaluateSubscriptionLocked(conditionId, cal, now.getTimeInMillis(), 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ private Calendar getNow() {
+ Calendar now = new GregorianCalendar();
+ now.set(Calendar.HOUR_OF_DAY, 14);
+ now.set(Calendar.MINUTE, 16);
+ now.set(Calendar.SECOND, 0);
+ now.set(Calendar.MILLISECOND, 0);
+ now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+ return now;
+ }
+
+ private ZenModeConfig.ScheduleInfo getScheduleEndsInHour(Calendar now) {
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.exitAtAlarm = true;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = now.get(Calendar.MINUTE);
+ return info;
+ }
+}