| /* |
| * 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; |
| |
| import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP; |
| import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE; |
| import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT; |
| import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE; |
| import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET; |
| |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.timeout; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; |
| import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; |
| import static com.android.server.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_LONG_TIME; |
| import static com.android.server.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_SHORT_TIME; |
| import static com.android.server.AlarmManagerService.Constants |
| .KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION; |
| import static com.android.server.AlarmManagerService.Constants.KEY_LISTENER_TIMEOUT; |
| import static com.android.server.AlarmManagerService.Constants.KEY_MAX_INTERVAL; |
| import static com.android.server.AlarmManagerService.Constants.KEY_MIN_FUTURITY; |
| import static com.android.server.AlarmManagerService.Constants.KEY_MIN_INTERVAL; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyBoolean; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyLong; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.ArgumentMatchers.isNull; |
| |
| import android.app.ActivityManager; |
| import android.app.AlarmManager; |
| import android.app.IActivityManager; |
| import android.app.IUidObserver; |
| import android.app.PendingIntent; |
| import android.app.usage.UsageStatsManagerInternal; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.PowerManager; |
| import android.os.SystemClock; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.SmallTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.internal.annotations.GuardedBy; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Answers; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Mock; |
| import org.mockito.MockitoSession; |
| import org.mockito.quality.Strictness; |
| |
| import java.util.ArrayList; |
| |
| @SmallTest |
| @RunWith(AndroidJUnit4.class) |
| public class AlarmManagerServiceTest { |
| private static final String TAG = AlarmManagerServiceTest.class.getSimpleName(); |
| private static final String TEST_CALLING_PACKAGE = "com.android.framework.test-package"; |
| private static final int SYSTEM_UI_UID = 123456789; |
| private static final int TEST_CALLING_UID = 12345; |
| private static final long DEFAULT_TIMEOUT = 5_000; |
| |
| private AlarmManagerService mService; |
| @Mock |
| private ContentResolver mMockResolver; |
| @Mock |
| private IActivityManager mIActivityManager; |
| @Mock |
| private UsageStatsManagerInternal mUsageStatsManagerInternal; |
| @Mock |
| private AppStateTracker mAppStateTracker; |
| @Mock |
| private AlarmManagerService.ClockReceiver mClockReceiver; |
| @Mock |
| private PowerManager.WakeLock mWakeLock; |
| |
| private MockitoSession mMockingSession; |
| private Injector mInjector; |
| private volatile long mNowElapsedTest; |
| @GuardedBy("mTestTimer") |
| private TestTimer mTestTimer = new TestTimer(); |
| |
| static class TestTimer { |
| private long mElapsed; |
| boolean mExpired; |
| |
| synchronized long getElapsed() { |
| return mElapsed; |
| } |
| |
| synchronized void set(long millisElapsed) { |
| mElapsed = millisElapsed; |
| } |
| |
| synchronized long expire() { |
| mExpired = true; |
| notify(); |
| return mElapsed; |
| } |
| } |
| |
| public class Injector extends AlarmManagerService.Injector { |
| Injector(Context context) { |
| super(context); |
| } |
| |
| @Override |
| void init() { |
| // Do nothing. |
| } |
| |
| @Override |
| int waitForAlarm() { |
| synchronized (mTestTimer) { |
| if (!mTestTimer.mExpired) { |
| try { |
| mTestTimer.wait(); |
| } catch (InterruptedException ie) { |
| Log.e(TAG, "Wait interrupted!", ie); |
| return 0; |
| } |
| } |
| mTestTimer.mExpired = false; |
| } |
| return AlarmManagerService.IS_WAKEUP_MASK; // Doesn't matter, just evaluate. |
| } |
| |
| @Override |
| void setKernelTimezone(int minutesWest) { |
| // Do nothing. |
| } |
| |
| @Override |
| void setAlarm(int type, long millis) { |
| mTestTimer.set(millis); |
| } |
| |
| @Override |
| void setKernelTime(long millis) { |
| } |
| |
| @Override |
| int getSystemUiUid() { |
| return SYSTEM_UI_UID; |
| } |
| |
| @Override |
| boolean isAlarmDriverPresent() { |
| // Pretend the driver is present, so code does not fall back to handler |
| return true; |
| } |
| |
| @Override |
| long getElapsedRealtime() { |
| return mNowElapsedTest; |
| } |
| |
| @Override |
| AlarmManagerService.ClockReceiver getClockReceiver(AlarmManagerService service) { |
| return mClockReceiver; |
| } |
| |
| @Override |
| PowerManager.WakeLock getAlarmWakeLock() { |
| return mWakeLock; |
| } |
| } |
| |
| @Before |
| public final void setUp() throws Exception { |
| mMockingSession = mockitoSession() |
| .initMocks(this) |
| .spyStatic(ActivityManager.class) |
| .mockStatic(LocalServices.class) |
| .spyStatic(Looper.class) |
| .spyStatic(Settings.Global.class) |
| .strictness(Strictness.WARN) |
| .startMocking(); |
| doReturn(mIActivityManager).when(ActivityManager::getService); |
| doReturn(mAppStateTracker).when(() -> LocalServices.getService(AppStateTracker.class)); |
| doReturn(null) |
| .when(() -> LocalServices.getService(DeviceIdleController.LocalService.class)); |
| doReturn(mUsageStatsManagerInternal).when( |
| () -> LocalServices.getService(UsageStatsManagerInternal.class)); |
| when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), |
| eq(UserHandle.getUserId(TEST_CALLING_UID)), anyLong())) |
| .thenReturn(STANDBY_BUCKET_ACTIVE); |
| doReturn(Looper.getMainLooper()).when(Looper::myLooper); |
| |
| final Context context = spy(InstrumentationRegistry.getTargetContext()); |
| when(context.getContentResolver()).thenReturn(mMockResolver); |
| doNothing().when(mMockResolver).registerContentObserver(any(), anyBoolean(), any()); |
| doReturn("min_futurity=0").when(() -> |
| Settings.Global.getString(mMockResolver, Settings.Global.ALARM_MANAGER_CONSTANTS)); |
| mInjector = new Injector(context); |
| mService = new AlarmManagerService(context, mInjector); |
| spyOn(mService); |
| doNothing().when(mService).publishBinderService(any(), any()); |
| mService.onStart(); |
| mService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY); |
| |
| assertEquals(0, mService.mConstants.MIN_FUTURITY); |
| assertEquals(mService.mSystemUiUid, SYSTEM_UI_UID); |
| assertEquals(mService.mClockReceiver, mClockReceiver); |
| assertEquals(mService.mWakeLock, mWakeLock); |
| verify(mIActivityManager).registerUidObserver(any(IUidObserver.class), anyInt(), anyInt(), |
| isNull()); |
| } |
| |
| private void setTestAlarm(int type, long triggerTime, PendingIntent operation) { |
| mService.setImpl(type, triggerTime, AlarmManager.WINDOW_EXACT, 0, |
| operation, null, "test", AlarmManager.FLAG_STANDALONE, null, null, |
| TEST_CALLING_UID, TEST_CALLING_PACKAGE); |
| } |
| |
| private PendingIntent getNewMockPendingIntent() { |
| final PendingIntent mockPi = mock(PendingIntent.class, Answers.RETURNS_DEEP_STUBS); |
| when(mockPi.getCreatorUid()).thenReturn(TEST_CALLING_UID); |
| when(mockPi.getCreatorPackage()).thenReturn(TEST_CALLING_PACKAGE); |
| return mockPi; |
| } |
| |
| @Test |
| public void testSingleAlarmSet() { |
| final long triggerTime = mNowElapsedTest + 5000; |
| final PendingIntent alarmPi = getNewMockPendingIntent(); |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi); |
| assertEquals(triggerTime, mTestTimer.getElapsed()); |
| } |
| |
| @Test |
| public void testSingleAlarmExpiration() throws Exception { |
| final long triggerTime = mNowElapsedTest + 5000; |
| final PendingIntent alarmPi = getNewMockPendingIntent(); |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, alarmPi); |
| |
| mNowElapsedTest = mTestTimer.expire(); |
| |
| final ArgumentCaptor<PendingIntent.OnFinished> onFinishedCaptor = |
| ArgumentCaptor.forClass(PendingIntent.OnFinished.class); |
| verify(alarmPi, timeout(DEFAULT_TIMEOUT)).send(any(Context.class), eq(0), |
| any(Intent.class), onFinishedCaptor.capture(), any(Handler.class), isNull(), any()); |
| verify(mWakeLock, timeout(DEFAULT_TIMEOUT)).acquire(); |
| onFinishedCaptor.getValue().onSendFinished(alarmPi, null, 0, null, null); |
| verify(mWakeLock, timeout(DEFAULT_TIMEOUT)).release(); |
| } |
| |
| @Test |
| public void testUpdateConstants() { |
| final StringBuilder constantsBuilder = new StringBuilder(); |
| constantsBuilder.append(KEY_MIN_FUTURITY); |
| constantsBuilder.append("=5,"); |
| constantsBuilder.append(KEY_MIN_INTERVAL); |
| constantsBuilder.append("=10,"); |
| constantsBuilder.append(KEY_MAX_INTERVAL); |
| constantsBuilder.append("=15,"); |
| constantsBuilder.append(KEY_ALLOW_WHILE_IDLE_SHORT_TIME); |
| constantsBuilder.append("=20,"); |
| constantsBuilder.append(KEY_ALLOW_WHILE_IDLE_LONG_TIME); |
| constantsBuilder.append("=25,"); |
| constantsBuilder.append(KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION); |
| constantsBuilder.append("=30,"); |
| constantsBuilder.append(KEY_LISTENER_TIMEOUT); |
| constantsBuilder.append("=35,"); |
| |
| doReturn(constantsBuilder.toString()).when(() -> Settings.Global.getString(mMockResolver, |
| Settings.Global.ALARM_MANAGER_CONSTANTS)); |
| mService.mConstants.onChange(false, null); |
| assertEquals(5, mService.mConstants.MIN_FUTURITY); |
| assertEquals(10, mService.mConstants.MIN_INTERVAL); |
| assertEquals(15, mService.mConstants.MAX_INTERVAL); |
| assertEquals(20, mService.mConstants.ALLOW_WHILE_IDLE_SHORT_TIME); |
| assertEquals(25, mService.mConstants.ALLOW_WHILE_IDLE_LONG_TIME); |
| assertEquals(30, mService.mConstants.ALLOW_WHILE_IDLE_WHITELIST_DURATION); |
| assertEquals(35, mService.mConstants.LISTENER_TIMEOUT); |
| } |
| |
| @Test |
| public void testMinFuturity() { |
| doReturn("min_futurity=10").when(() -> |
| Settings.Global.getString(mMockResolver, Settings.Global.ALARM_MANAGER_CONSTANTS)); |
| mService.mConstants.onChange(false, null); |
| assertEquals(10, mService.mConstants.MIN_FUTURITY); |
| final long triggerTime = mNowElapsedTest + 1; |
| final long expectedTriggerTime = mNowElapsedTest + mService.mConstants.MIN_FUTURITY; |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, triggerTime, getNewMockPendingIntent()); |
| assertEquals(expectedTriggerTime, mTestTimer.getElapsed()); |
| } |
| |
| @Test |
| public void testEarliestAlarmSet() { |
| final PendingIntent pi6 = getNewMockPendingIntent(); |
| final PendingIntent pi8 = getNewMockPendingIntent(); |
| final PendingIntent pi9 = getNewMockPendingIntent(); |
| |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 8, pi8); |
| assertEquals(mNowElapsedTest + 8, mTestTimer.getElapsed()); |
| |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 9, pi9); |
| assertEquals(mNowElapsedTest + 8, mTestTimer.getElapsed()); |
| |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 6, pi6); |
| assertEquals(mNowElapsedTest + 6, mTestTimer.getElapsed()); |
| |
| mService.removeLocked(pi6, null); |
| assertEquals(mNowElapsedTest + 8, mTestTimer.getElapsed()); |
| |
| mService.removeLocked(pi8, null); |
| assertEquals(mNowElapsedTest + 9, mTestTimer.getElapsed()); |
| } |
| |
| @Test |
| public void testStandbyBucketDelay_workingSet() throws Exception { |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5, getNewMockPendingIntent()); |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 6, getNewMockPendingIntent()); |
| assertEquals(mNowElapsedTest + 5, mTestTimer.getElapsed()); |
| |
| when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), |
| anyLong())).thenReturn(STANDBY_BUCKET_WORKING_SET); |
| mNowElapsedTest = mTestTimer.expire(); |
| verify(mUsageStatsManagerInternal, timeout(DEFAULT_TIMEOUT).atLeastOnce()) |
| .getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), |
| eq(UserHandle.getUserId(TEST_CALLING_UID)), anyLong()); |
| final long expectedNextTrigger = mNowElapsedTest |
| + mService.getMinDelayForBucketLocked(STANDBY_BUCKET_WORKING_SET); |
| assertTrue("Incorrect next alarm trigger. Expected " + expectedNextTrigger + " found: " |
| + mTestTimer.getElapsed(), pollingCheck(DEFAULT_TIMEOUT, |
| () -> (mTestTimer.getElapsed() == expectedNextTrigger))); |
| } |
| |
| @Test |
| public void testStandbyBucketDelay_frequent() { |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5, getNewMockPendingIntent()); |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 6, getNewMockPendingIntent()); |
| assertEquals(mNowElapsedTest + 5, mTestTimer.getElapsed()); |
| |
| when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), |
| anyLong())).thenReturn(STANDBY_BUCKET_FREQUENT); |
| mNowElapsedTest = mTestTimer.expire(); |
| verify(mUsageStatsManagerInternal, timeout(DEFAULT_TIMEOUT).atLeastOnce()) |
| .getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), |
| eq(UserHandle.getUserId(TEST_CALLING_UID)), anyLong()); |
| final long expectedNextTrigger = mNowElapsedTest |
| + mService.getMinDelayForBucketLocked(STANDBY_BUCKET_FREQUENT); |
| assertTrue("Incorrect next alarm trigger. Expected " + expectedNextTrigger + " found: " |
| + mTestTimer.getElapsed(), pollingCheck(DEFAULT_TIMEOUT, |
| () -> (mTestTimer.getElapsed() == expectedNextTrigger))); |
| } |
| |
| @Test |
| public void testStandbyBucketDelay_rare() { |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 5, getNewMockPendingIntent()); |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 6, getNewMockPendingIntent()); |
| assertEquals(mNowElapsedTest + 5, mTestTimer.getElapsed()); |
| |
| when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), anyInt(), |
| anyLong())).thenReturn(STANDBY_BUCKET_RARE); |
| mNowElapsedTest = mTestTimer.expire(); |
| verify(mUsageStatsManagerInternal, timeout(DEFAULT_TIMEOUT).atLeastOnce()) |
| .getAppStandbyBucket(eq(TEST_CALLING_PACKAGE), |
| eq(UserHandle.getUserId(TEST_CALLING_UID)), anyLong()); |
| final long expectedNextTrigger = mNowElapsedTest |
| + mService.getMinDelayForBucketLocked(STANDBY_BUCKET_RARE); |
| assertTrue("Incorrect next alarm trigger. Expected " + expectedNextTrigger + " found: " |
| + mTestTimer.getElapsed(), pollingCheck(DEFAULT_TIMEOUT, |
| () -> (mTestTimer.getElapsed() == expectedNextTrigger))); |
| } |
| |
| @Test |
| public void testAlarmRestrictedInBatterSaver() throws PendingIntent.CanceledException { |
| final ArgumentCaptor<AppStateTracker.Listener> listenerArgumentCaptor = |
| ArgumentCaptor.forClass(AppStateTracker.Listener.class); |
| verify(mAppStateTracker).addListener(listenerArgumentCaptor.capture()); |
| |
| final PendingIntent alarmPi = getNewMockPendingIntent(); |
| when(mAppStateTracker.areAlarmsRestricted(TEST_CALLING_UID, TEST_CALLING_PACKAGE, |
| false)).thenReturn(true); |
| setTestAlarm(ELAPSED_REALTIME_WAKEUP, mNowElapsedTest + 2, alarmPi); |
| assertEquals(mNowElapsedTest + 2, mTestTimer.getElapsed()); |
| |
| final SparseArray<ArrayList<AlarmManagerService.Alarm>> restrictedAlarms = |
| mService.mPendingBackgroundAlarms; |
| assertNull(restrictedAlarms.get(TEST_CALLING_UID)); |
| |
| mNowElapsedTest = mTestTimer.expire(); |
| pollingCheck(DEFAULT_TIMEOUT, () -> (restrictedAlarms.get(TEST_CALLING_UID) != null)); |
| |
| listenerArgumentCaptor.getValue().unblockAlarmsForUid(TEST_CALLING_UID); |
| verify(alarmPi).send(any(Context.class), eq(0), any(Intent.class), any(), |
| any(Handler.class), isNull(), any()); |
| assertNull(restrictedAlarms.get(TEST_CALLING_UID)); |
| } |
| |
| @After |
| public void tearDown() { |
| if (mMockingSession != null) { |
| mMockingSession.finishMocking(); |
| } |
| } |
| |
| private boolean pollingCheck(long timeout, Condition condition) { |
| final long deadline = SystemClock.uptimeMillis() + timeout; |
| boolean interrupted = false; |
| while (!condition.check() && SystemClock.uptimeMillis() < deadline) { |
| try { |
| Thread.sleep(500); |
| } catch (InterruptedException ie) { |
| interrupted = true; |
| } |
| } |
| if (interrupted) { |
| Thread.currentThread().interrupt(); |
| } |
| return condition.check(); |
| } |
| |
| @FunctionalInterface |
| interface Condition { |
| boolean check(); |
| } |
| } |