summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Matías Hernández <matiashe@google.com> 2024-10-22 18:21:16 +0200
committer Matías Hernández <matiashe@google.com> 2024-10-22 18:24:55 +0200
commit2c190cb02b94db14b208feee5cd3e0aff1d766ea (patch)
treec29c0c18082a9cb0f8836e5f114b5ed1f6b9d8b9
parent5025d6854ee7798c7b7ea152e549354a1919500c (diff)
Listen to alarm changes for all users in ScheduleConditionProvider
Previously, the system was only registering for broadcasts on user 0 (SYSTEM) but AlarmManager sends broadcasts only to the user that owns the alarm that is changing. This meant that zen rules with "alarm can override end time" schedules didn't work correctly for secondary users (or for any users, in the case of HSUM). Fixes: 366141808 Test: atest ScheduleConditionProviderTest + manually Flag: android.app.modes_hsum Change-Id: I5f471522db22e8779a9837bbac340d0a7c2b60be
-rw-r--r--services/core/java/com/android/server/notification/ScheduleConditionProvider.java49
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java157
2 files changed, 191 insertions, 15 deletions
diff --git a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
index 24b090c6ffab..734c61b6b2a8 100644
--- a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
+++ b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
@@ -42,6 +42,7 @@ import com.android.server.notification.NotificationManagerService.DumpFilter;
import com.android.server.pm.PackageManagerService;
import java.io.PrintWriter;
+import java.time.Clock;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
@@ -62,6 +63,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
private final Context mContext = this;
+ private final Clock mClock;
private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
@GuardedBy("mSnoozedForAlarm")
private final ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
@@ -72,7 +74,13 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
private long mNextAlarmTime;
public ScheduleConditionProvider() {
+ this(Clock.systemUTC());
+ }
+
+ @VisibleForTesting
+ ScheduleConditionProvider(Clock clock) {
if (DEBUG) Slog.d(TAG, "new " + SIMPLE_NAME + "()");
+ mClock = clock;
}
@Override
@@ -86,7 +94,7 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
pw.print(" mConnected="); pw.println(mConnected);
pw.print(" mRegistered="); pw.println(mRegistered);
pw.println(" mSubscriptions=");
- final long now = System.currentTimeMillis();
+ final long now = mClock.millis();
synchronized (mSubscriptions) {
for (Uri conditionId : mSubscriptions.keySet()) {
pw.print(" ");
@@ -117,7 +125,9 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
@Override
public void onUserSwitched(UserHandle user) {
- // Nothing to do because time-based schedules are not tied to any user data.
+ // Nothing to do here because evaluateSubscriptions() is called for the new configuration
+ // when users switch, and that will reevaluate the next alarm, which is the only piece that
+ // is user-dependent.
}
@Override
@@ -151,12 +161,9 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
}
private void evaluateSubscriptions() {
- if (mAlarmManager == null) {
- mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
- }
- final long now = System.currentTimeMillis();
+ final long now = mClock.millis();
mNextAlarmTime = 0;
- long nextUserAlarmTime = getNextAlarm();
+ long nextUserAlarmTime = getNextAlarmClockAlarm();
List<Condition> conditionsToNotify = new ArrayList<>();
synchronized (mSubscriptions) {
setRegistered(!mSubscriptions.isEmpty());
@@ -232,7 +239,10 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
- public long getNextAlarm() {
+ private long getNextAlarmClockAlarm() {
+ if (mAlarmManager == null) {
+ mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ }
final AlarmManager.AlarmClockInfo info = mAlarmManager.getNextAlarmClock(
ActivityManager.getCurrentUser());
return info != null ? info.getTriggerTime() : 0;
@@ -252,8 +262,13 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
filter.addAction(ACTION_EVALUATE);
filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
- registerReceiver(mReceiver, filter,
- Context.RECEIVER_EXPORTED_UNAUDITED);
+ if (android.app.Flags.modesHsum()) {
+ registerReceiverForAllUsers(mReceiver, filter, /* broadcastPermission= */ null,
+ /* scheduler= */ null);
+ } else {
+ registerReceiver(mReceiver, filter,
+ Context.RECEIVER_EXPORTED_UNAUDITED);
+ }
} else {
unregisterReceiver(mReceiver);
}
@@ -327,10 +342,18 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
}
}
- private BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Slog.d(TAG, "onReceive " + intent.getAction());
+ if (android.app.Flags.modesHsum()) {
+ if (AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equals(intent.getAction())
+ && getSendingUserId() != ActivityManager.getCurrentUser()) {
+ // A different user changed their next alarm.
+ return;
+ }
+ }
+
if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
synchronized (mSubscriptions) {
for (Uri conditionId : mSubscriptions.keySet()) {
@@ -345,4 +368,8 @@ public class ScheduleConditionProvider extends SystemConditionProviderService {
}
};
+ @VisibleForTesting // otherwise = NONE
+ public ArrayMap<Uri, ScheduleCalendar> getSubscriptions() {
+ return mSubscriptions;
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java
index fe4ce465e9be..52c34889a1ec 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java
@@ -1,18 +1,38 @@
package com.android.server.notification;
+import static android.app.AlarmManager.RTC_WAKEUP;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.time.temporal.ChronoUnit.HOURS;
+import static java.time.temporal.ChronoUnit.MINUTES;
+import android.app.ActivityManager;
+import android.app.AlarmManager;
import android.app.Application;
import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Intent;
+import android.content.IntentFilter;
import android.net.Uri;
+import android.os.Bundle;
+import android.os.SimpleClock;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.Condition;
import android.service.notification.ScheduleCalendar;
import android.service.notification.ZenModeConfig;
@@ -24,11 +44,20 @@ import androidx.test.filters.SmallTest;
import com.android.server.UiServiceTestCase;
import com.android.server.pm.PackageManagerService;
+import com.google.common.collect.ImmutableList;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.GregorianCalendar;
@@ -36,17 +65,22 @@ import java.util.GregorianCalendar;
@SmallTest
@RunWithLooper
public class ScheduleConditionProviderTest extends UiServiceTestCase {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
- ScheduleConditionProvider mService;
+ private ScheduleConditionProvider mService;
+ private TestClock mClock = new TestClock();
+ @Mock private AlarmManager mAlarmManager;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ mContext.addMockSystemService(AlarmManager.class, mAlarmManager);
Intent startIntent =
new Intent("com.android.server.notification.ScheduleConditionProvider");
startIntent.setPackage("android");
- ScheduleConditionProvider service = new ScheduleConditionProvider();
+ ScheduleConditionProvider service = new ScheduleConditionProvider(mClock);
service.attach(
getContext(),
null, // ActivityThread not actually used in Service
@@ -57,7 +91,7 @@ public class ScheduleConditionProviderTest extends UiServiceTestCase {
);
service.onCreate();
service.onBind(startIntent);
- mService = spy(service);
+ mService = service;
}
@Test
@@ -343,6 +377,87 @@ public class ScheduleConditionProviderTest extends UiServiceTestCase {
assertEquals(PackageManagerService.PLATFORM_PACKAGE_NAME, pi.getIntent().getPackage());
}
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_HSUM)
+ public void onSubscribe_registersReceiverForAllUsers() {
+ Calendar now = getNow();
+ Uri condition = ZenModeConfig.toScheduleConditionId(getScheduleEndsInHour(now));
+
+ mService.onSubscribe(condition);
+
+ ArgumentCaptor<IntentFilter> filterCaptor = ArgumentCaptor.forClass(IntentFilter.class);
+ verify(mContext).registerReceiverForAllUsers(any(), filterCaptor.capture(), any(), any());
+ IntentFilter filter = filterCaptor.getValue();
+ assertThat(filter.actionsIterator()).isNotNull();
+ assertThat(ImmutableList.copyOf(filter.actionsIterator()))
+ .contains(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_HSUM)
+ public void onAlarmClockChanged_storesNextAlarm() {
+ Instant scheduleStart = Instant.parse("2024-10-22T16:00:00Z");
+ Instant scheduleEnd = scheduleStart.plus(1, HOURS);
+
+ Instant now = scheduleStart.plus(15, MINUTES);
+ mClock.setNowMillis(now.toEpochMilli());
+
+ Uri condition = ZenModeConfig.toScheduleConditionId(
+ getOneHourSchedule(scheduleStart.atZone(ZoneId.systemDefault())));
+ mService.onSubscribe(condition);
+
+ // Now prepare to send an "alarm set for 16:30" broadcast.
+ Instant alarm = scheduleStart.plus(30, MINUTES);
+ ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass(
+ BroadcastReceiver.class);
+ verify(mContext).registerReceiverForAllUsers(receiverCaptor.capture(), any(), any(), any());
+ BroadcastReceiver receiver = receiverCaptor.getValue();
+ receiver.setPendingResult(pendingResultForUserBroadcast(ActivityManager.getCurrentUser()));
+ when(mAlarmManager.getNextAlarmClock(anyInt())).thenReturn(
+ new AlarmManager.AlarmClockInfo(alarm.toEpochMilli(), null));
+
+ Intent intent = new Intent(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+ receiver.onReceive(mContext, intent);
+
+ // The time for the alarm was stored in the ScheduleCalendar, meaning the rule will end when
+ // the next evaluation after that point happens.
+ ScheduleCalendar scheduleCalendar =
+ mService.getSubscriptions().values().stream().findFirst().get();
+ assertThat(scheduleCalendar.shouldExitForAlarm(alarm.toEpochMilli() - 1)).isFalse();
+ assertThat(scheduleCalendar.shouldExitForAlarm(alarm.toEpochMilli() + 1)).isTrue();
+
+ // But the next wakeup is unchanged, at the time of the schedule end (17:00).
+ verify(mAlarmManager, times(2)).setExact(eq(RTC_WAKEUP), eq(scheduleEnd.toEpochMilli()),
+ any());
+ }
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_MODES_HSUM)
+ public void onAlarmClockChanged_forAnotherUser_isIgnored() {
+ Instant scheduleStart = Instant.parse("2024-10-22T16:00:00Z");
+ Instant now = scheduleStart.plus(15, MINUTES);
+ mClock.setNowMillis(now.toEpochMilli());
+
+ Uri condition = ZenModeConfig.toScheduleConditionId(
+ getOneHourSchedule(scheduleStart.atZone(ZoneId.systemDefault())));
+ mService.onSubscribe(condition);
+
+ // Now prepare to send an "alarm set for a different user" broadcast.
+ ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass(
+ BroadcastReceiver.class);
+ verify(mContext).registerReceiverForAllUsers(receiverCaptor.capture(), any(), any(), any());
+ BroadcastReceiver receiver = receiverCaptor.getValue();
+
+ reset(mAlarmManager);
+ int anotherUser = ActivityManager.getCurrentUser() + 1;
+ receiver.setPendingResult(pendingResultForUserBroadcast(anotherUser));
+ Intent intent = new Intent(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
+ receiver.onReceive(mContext, intent);
+
+ // The alarm data was not read.
+ verify(mAlarmManager, never()).getNextAlarmClock(anyInt());
+ }
+
private Calendar getNow() {
Calendar now = new GregorianCalendar();
now.set(Calendar.HOUR_OF_DAY, 14);
@@ -363,4 +478,38 @@ public class ScheduleConditionProviderTest extends UiServiceTestCase {
info.endMinute = now.get(Calendar.MINUTE);
return info;
}
+
+ private static ZenModeConfig.ScheduleInfo getOneHourSchedule(ZonedDateTime start) {
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ // Note: DayOfWeek.MONDAY doesn't match Calendar.MONDAY
+ info.days = new int[] { (start.getDayOfWeek().getValue() % 7) + 1 };
+ info.startHour = start.getHour();
+ info.startMinute = start.getMinute();
+ info.endHour = start.plusHours(1).getHour();
+ info.endMinute = start.plusHours(1).getMinute();
+ info.exitAtAlarm = true;
+ return info;
+ }
+
+ private static BroadcastReceiver.PendingResult pendingResultForUserBroadcast(int userId) {
+ return new BroadcastReceiver.PendingResult(0, "", new Bundle(), 0, false, false, null,
+ userId, 0);
+ }
+
+ private static class TestClock extends SimpleClock {
+ private long mNowMillis = 441644400000L;
+
+ private TestClock() {
+ super(ZoneOffset.UTC);
+ }
+
+ @Override
+ public long millis() {
+ return mNowMillis;
+ }
+
+ private void setNowMillis(long millis) {
+ mNowMillis = millis;
+ }
+ }
}