Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ae501d7f authored by Matías Hernández's avatar Matías Hernández Committed by Android (Google) Code Review
Browse files

Merge "Listen to alarm changes for all users in ScheduleConditionProvider" into main

parents 5583df48 2c190cb0
Loading
Loading
Loading
Loading
+38 −11
Original line number Diff line number Diff line
@@ -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);
            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;
    }
}
+153 −4
Original line number Diff line number Diff line
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;
        }
    }
}