diff --git a/AndroidManifest.xml b/AndroidManifest.xml index e33bba8700c..d89f05c7991 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2983,6 +2983,26 @@ + + + + + + + + + + + + + + + + BootBroadcastReceiver.invokeJobRecheck(getContext())); Log.w(TAG, "query battery states in " + (mClock.millis() - timestamp) + "/ms"); return cursor; } diff --git a/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java new file mode 100644 index 00000000000..5b48c9f83fa --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiver.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batteryusage; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.time.Duration; + +/** Receives broadcasts to start or stop the periodic fetching job. */ +public final class BootBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = "BootBroadcastReceiver"; + private static final long RESCHEDULE_FOR_BOOT_ACTION = Duration.ofSeconds(6).toMillis(); + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + public static final String ACTION_PERIODIC_JOB_RECHECK = + "com.android.settings.battery.action.PERIODIC_JOB_RECHECK"; + public static final String ACTION_SETUP_WIZARD_FINISHED = + "com.google.android.setupwizard.SETUP_WIZARD_FINISHED"; + + /** Invokes periodic job rechecking process. */ + public static void invokeJobRecheck(Context context) { + context = context.getApplicationContext(); + final Intent intent = new Intent(ACTION_PERIODIC_JOB_RECHECK); + intent.setClass(context, BootBroadcastReceiver.class); + context.sendBroadcast(intent); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent == null ? "" : intent.getAction(); + if (DatabaseUtils.isWorkProfile(context)) { + Log.w(TAG, "do not start job for work profile action=" + action); + return; + } + + switch (action) { + case Intent.ACTION_BOOT_COMPLETED: + case Intent.ACTION_MY_PACKAGE_REPLACED: + case Intent.ACTION_MY_PACKAGE_UNSUSPENDED: + case ACTION_SETUP_WIZARD_FINISHED: + case ACTION_PERIODIC_JOB_RECHECK: + Log.d(TAG, "refresh periodic job from action=" + action); + refreshJobs(context); + break; + case Intent.ACTION_TIME_CHANGED: + Log.d(TAG, "refresh job and clear all data from action=" + action); + DatabaseUtils.clearAll(context); + PeriodicJobManager.getInstance(context).refreshJob(); + break; + default: + Log.w(TAG, "receive unsupported action=" + action); + } + + // Waits a while to recheck the scheduler to avoid AlarmManager is not ready. + if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + final Intent recheckIntent = new Intent(ACTION_PERIODIC_JOB_RECHECK); + recheckIntent.setClass(context, BootBroadcastReceiver.class); + mHandler.postDelayed(() -> context.sendBroadcast(recheckIntent), + RESCHEDULE_FOR_BOOT_ACTION); + } + } + + private static void refreshJobs(Context context) { + // Clears useless data from battery usage database if needed. + DatabaseUtils.clearExpiredDataIfNeeded(context); + PeriodicJobManager.getInstance(context).refreshJob(); + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java index f2c5c2b96df..394c154dc2b 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DatabaseUtils.java @@ -57,7 +57,9 @@ public final class DatabaseUtils { /** Clear memory threshold for device booting phase. **/ private static final long CLEAR_MEMORY_THRESHOLD_MS = Duration.ofMinutes(5).toMillis(); private static final long CLEAR_MEMORY_DELAYED_MS = Duration.ofSeconds(2).toMillis(); - private static final long DATA_RETENTION_INTERVAL_MS = Duration.ofDays(9).toMillis(); + + @VisibleForTesting + static final int DATA_RETENTION_INTERVAL_DAY = 9; /** An authority name of the battery content provider. */ public static final String AUTHORITY = "com.android.settings.battery.usage.provider"; @@ -65,8 +67,6 @@ public final class DatabaseUtils { public static final String BATTERY_STATE_TABLE = "BatteryState"; /** A class name for battery usage data provider. */ public static final String SETTINGS_PACKAGE_PATH = "com.android.settings"; - public static final String BATTERY_PROVIDER_CLASS_PATH = - "com.android.settings.fuelgauge.batteryusage.BatteryUsageContentProvider"; /** A content URI to access battery usage states data. */ public static final Uri BATTERY_CONTENT_URI = @@ -133,7 +133,8 @@ public final class DatabaseUtils { BatteryStateDatabase .getInstance(context.getApplicationContext()) .batteryStateDao() - .clearAllBefore(Clock.systemUTC().millis() - DATA_RETENTION_INTERVAL_MS); + .clearAllBefore(Clock.systemUTC().millis() + - Duration.ofDays(DATA_RETENTION_INTERVAL_DAY).toMillis()); } catch (RuntimeException e) { Log.e(TAG, "clearAllBefore() failed", e); } diff --git a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java new file mode 100644 index 00000000000..140ba5ff4f2 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManager.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batteryusage; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import java.text.SimpleDateFormat; +import java.time.Clock; +import java.time.Duration; +import java.util.Date; +import java.util.Locale; + +/** Manages the periodic job to schedule or cancel the next job. */ +public final class PeriodicJobManager { + private static final String TAG = "PeriodicJobManager"; + private static final int ALARM_MANAGER_REQUEST_CODE = TAG.hashCode(); + + private static PeriodicJobManager sSingleton; + + private final Context mContext; + private final AlarmManager mAlarmManager; + private final SimpleDateFormat mSimpleDateFormat = + new SimpleDateFormat("MMM dd,yyyy HH:mm:ss", Locale.ENGLISH); + + @VisibleForTesting + static final int DATA_FETCH_INTERVAL_MINUTE = 60; + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + void reset() { + sSingleton = null; // for testing only + } + + /** Gets or creates the new {@link PeriodicJobManager} instance. */ + public static synchronized PeriodicJobManager getInstance(Context context) { + if (sSingleton == null || sSingleton.mAlarmManager == null) { + sSingleton = new PeriodicJobManager(context); + } + return sSingleton; + } + + private PeriodicJobManager(Context context) { + this.mContext = context.getApplicationContext(); + this.mAlarmManager = context.getSystemService(AlarmManager.class); + } + + /** Schedules the next alarm job if it is available. */ + @SuppressWarnings("JavaUtilDate") + public void refreshJob() { + if (mAlarmManager == null) { + Log.e(TAG, "cannot schedule next alarm job"); + return; + } + // Cancels the previous alert job and schedules the next one. + final PendingIntent pendingIntent = getPendingIntent(); + cancelJob(pendingIntent); + if (!canScheduleExactAlarms()) { + Log.w(TAG, "cannot schedule exact alarm job"); + return; + } + // Uses UTC time to avoid scheduler is impacted by different timezone. + final long triggerAtMillis = getTriggerAtMillis(Clock.systemUTC()); + mAlarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent); + Log.d(TAG, "schedule next alarm job at " + + mSimpleDateFormat.format(new Date(triggerAtMillis))); + } + + void cancelJob(PendingIntent pendingIntent) { + if (mAlarmManager != null) { + mAlarmManager.cancel(pendingIntent); + } else { + Log.e(TAG, "cannot cancel the alarm job"); + } + } + + /** Gets the next alarm trigger UTC time in milliseconds. */ + static long getTriggerAtMillis(Clock clock) { + long currentTimeMillis = clock.millis(); + // Rounds to the previous nearest time slot and shifts to the next one. + long timeSlotUnit = Duration.ofMinutes(DATA_FETCH_INTERVAL_MINUTE).toMillis(); + return (currentTimeMillis / timeSlotUnit) * timeSlotUnit + timeSlotUnit; + } + + private PendingIntent getPendingIntent() { + final Intent broadcastIntent = + new Intent(mContext, PeriodicJobReceiver.class) + .setAction(PeriodicJobReceiver.ACTION_PERIODIC_JOB_UPDATE); + return PendingIntent.getBroadcast( + mContext.getApplicationContext(), + ALARM_MANAGER_REQUEST_CODE, + broadcastIntent, + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + + private boolean canScheduleExactAlarms() { + return canScheduleExactAlarms(mAlarmManager); + } + + /** Whether we can schedule exact alarm or not? */ + public static boolean canScheduleExactAlarms(AlarmManager alarmManager) { + return alarmManager.canScheduleExactAlarms(); + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java new file mode 100644 index 00000000000..d2345ab4f6d --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiver.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batteryusage; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +/** Receives the periodic alarm {@link PendingIntent} callback. */ +public final class PeriodicJobReceiver extends BroadcastReceiver { + private static final String TAG = "PeriodicJobReceiver"; + public static final String ACTION_PERIODIC_JOB_UPDATE = + "com.android.settings.battery.action.PERIODIC_JOB_UPDATE"; + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent == null ? "" : intent.getAction(); + if (!ACTION_PERIODIC_JOB_UPDATE.equals(action)) { + Log.w(TAG, "receive unexpected action=" + action); + return; + } + if (DatabaseUtils.isWorkProfile(context)) { + Log.w(TAG, "do not refresh job for work profile action=" + action); + return; + } + BatteryUsageDataLoader.enqueueWork(context); + Log.d(TAG, "refresh periodic job from action=" + action); + PeriodicJobManager.getInstance(context).refreshJob(); + DatabaseUtils.clearExpiredDataIfNeeded(context); + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java index 939654639a0..9d13d9f49d4 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java +++ b/src/com/android/settings/fuelgauge/batteryusage/db/BatteryStateDatabase.java @@ -40,7 +40,7 @@ public abstract class BatteryStateDatabase extends RoomDatabase { if (sBatteryStateDatabase == null) { sBatteryStateDatabase = Room.databaseBuilder( - context, BatteryStateDatabase.class, "battery-usage-db-v1") + context, BatteryStateDatabase.class, "battery-usage-db-v5") // Allows accessing data in the main thread for dumping bugreport. .allowMainThreadQueries() .fallbackToDestructiveMigration() diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java index 61d4efad5b7..713c2ee4beb 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryUsageContentProviderTest.java @@ -20,9 +20,11 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import android.app.Application; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -37,9 +39,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; import java.time.Duration; import java.util.List; +import java.util.concurrent.TimeUnit; /** Tests for {@link BatteryUsageContentProvider}. */ @RunWith(RobolectricTestRunner.class) @@ -301,6 +305,11 @@ public final class BatteryUsageContentProviderTest { final String actualPackageName3 = cursor.getString(packageNameIndex); assertThat(actualPackageName3).isEqualTo(packageName3); cursor.close(); - // TODO: add verification for recheck broadcast. + // Verifies the broadcast intent. + TimeUnit.SECONDS.sleep(1); + final List intents = Shadows.shadowOf((Application) mContext).getBroadcastIntents(); + assertThat(intents).hasSize(1); + assertThat(intents.get(0).getAction()).isEqualTo( + BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK); } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java new file mode 100644 index 00000000000..e42d6f58a73 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batteryusage; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.app.AlarmManager; +import android.app.Application; +import android.content.Context; +import android.content.Intent; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao; +import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; +import com.android.settings.testutils.BatteryTestUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowAlarmManager; + +import java.time.Clock; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** Tests of {@link BootBroadcastReceiver}. */ +@RunWith(RobolectricTestRunner.class) +public final class BootBroadcastReceiverTest { + private Context mContext; + private BatteryStateDao mDao; + private BootBroadcastReceiver mReceiver; + private ShadowAlarmManager mShadowAlarmManager; + private PeriodicJobManager mPeriodicJobManager; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + mPeriodicJobManager = PeriodicJobManager.getInstance(mContext); + mShadowAlarmManager = shadowOf(mContext.getSystemService(AlarmManager.class)); + ShadowAlarmManager.setCanScheduleExactAlarms(true); + mReceiver = new BootBroadcastReceiver(); + + // Inserts fake data into database for testing. + final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext); + BatteryTestUtils.insertDataToBatteryStateDatabase( + mContext, Clock.systemUTC().millis(), "com.android.systemui"); + mDao = database.batteryStateDao(); + } + + @After + public void tearDown() { + mPeriodicJobManager.reset(); + } + + @Test + public void onReceive_withWorkProfile_notRefreshesJob() { + BatteryTestUtils.setWorkProfile(mContext); + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_BOOT_COMPLETED)); + + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + + @Test + public void onReceive_withMyPackageReplacedIntent_refreshesJob() { + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_MY_PACKAGE_REPLACED)); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void onReceive_withMyPackageUnsuspendIntent_refreshesJob() { + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_MY_PACKAGE_UNSUSPENDED)); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void onReceive_withBootCompletedIntent_refreshesJob() { + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_BOOT_COMPLETED)); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void onReceive_withSetupWizardIntent_refreshesJob() { + mReceiver.onReceive( + mContext, new Intent(BootBroadcastReceiver.ACTION_SETUP_WIZARD_FINISHED)); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void onReceive_withRecheckIntent_refreshesJob() { + mReceiver.onReceive( + mContext, new Intent(BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK)); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void onReceive_unexpectedIntent_notRefreshesJob() { + mReceiver.onReceive(mContext, new Intent("invalid intent action")); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + + @Test + public void onReceive_nullIntent_notRefreshesJob() { + mReceiver.onReceive(mContext, /*intent=*/ null); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + + @Test + public void onReceive_containsExpiredData_clearsExpiredDataFromDatabase() + throws InterruptedException { + insertExpiredData(/*shiftDay=*/ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY); + + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_BOOT_COMPLETED)); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0)).hasSize(1); + } + + @Test + public void onReceive_withoutExpiredData_notClearsExpiredDataFromDatabase() + throws InterruptedException { + insertExpiredData(/*shiftDay=*/ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY - 1); + + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_BOOT_COMPLETED)); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0)).hasSize(3); + } + + @Test + public void onReceive_withTimeChangedIntent_clearsAllDataAndRefreshesJob() + throws InterruptedException { + mReceiver.onReceive(mContext, new Intent(Intent.ACTION_TIME_CHANGED)); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0)).isEmpty(); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void invokeJobRecheck_broadcastsIntent() { + BootBroadcastReceiver.invokeJobRecheck(mContext); + + final List intents = + Shadows.shadowOf((Application) mContext).getBroadcastIntents(); + assertThat(intents).hasSize(1); + assertThat(intents.get(0).getAction()).isEqualTo( + BootBroadcastReceiver.ACTION_PERIODIC_JOB_RECHECK); + } + + private void insertExpiredData(int shiftDay) { + final long expiredTimeInMs = + Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis(); + BatteryTestUtils.insertDataToBatteryStateDatabase( + mContext, expiredTimeInMs - 1, "com.android.systemui"); + BatteryTestUtils.insertDataToBatteryStateDatabase( + mContext, expiredTimeInMs, "com.android.systemui"); + // Ensures the testing environment is correct. + assertThat(mDao.getAllAfter(0)).hasSize(3); + } + +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManagerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManagerTest.java new file mode 100644 index 00000000000..2ee21f56af3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobManagerTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batteryusage; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.app.AlarmManager; +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.testutils.FakeClock; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowAlarmManager; + +import java.time.Duration; + +/** Tests of {@link PeriodicJobManager}. */ +@RunWith(RobolectricTestRunner.class) +public final class PeriodicJobManagerTest { + private Context mContext; + private ShadowAlarmManager mShadowAlarmManager; + private PeriodicJobManager mPeriodicJobManager; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + mPeriodicJobManager = PeriodicJobManager.getInstance(mContext); + mShadowAlarmManager = shadowOf(mContext.getSystemService(AlarmManager.class)); + ShadowAlarmManager.setCanScheduleExactAlarms(true); + } + + @After + public void tearDown() { + mPeriodicJobManager.reset(); + } + + @Test + public void refreshJob_refreshesAlarmJob() { + mPeriodicJobManager.refreshJob(); + + final ShadowAlarmManager.ScheduledAlarm alarm = + mShadowAlarmManager.peekNextScheduledAlarm(); + // Verifies the alarm manager type. + assertThat(alarm.type).isEqualTo(AlarmManager.RTC_WAKEUP); + // Verifies there is pending intent in the alarm. + assertThat(alarm.operation).isNotNull(); + } + + @Test + public void refreshJob_withoutPermission_notRefreshesAlarmJob() { + ShadowAlarmManager.setCanScheduleExactAlarms(false); + + mPeriodicJobManager.refreshJob(); + + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + + @Test + public void getTriggerAtMillis_withoutOffset_returnsExpectedResult() { + long timeSlotUnit = PeriodicJobManager.DATA_FETCH_INTERVAL_MINUTE; + // Sets the current time. + Duration currentTimeDuration = + Duration.ofMinutes(timeSlotUnit * 2); + FakeClock fakeClock = new FakeClock(); + fakeClock.setCurrentTime(currentTimeDuration); + + assertThat(PeriodicJobManager.getTriggerAtMillis(fakeClock)) + .isEqualTo(currentTimeDuration.plusMinutes(timeSlotUnit).toMillis()); + } + + @Test + public void getTriggerAtMillis_withOffset_returnsExpectedResult() { + long timeSlotUnit = PeriodicJobManager.DATA_FETCH_INTERVAL_MINUTE; + // Sets the current time. + Duration currentTimeDuration = Duration.ofMinutes(timeSlotUnit * 2); + FakeClock fakeClock = new FakeClock(); + fakeClock.setCurrentTime( + currentTimeDuration.plusMinutes(1L).plusMillis(51L)); + + assertThat(PeriodicJobManager.getTriggerAtMillis(fakeClock)) + .isEqualTo(currentTimeDuration.plusMinutes(timeSlotUnit).toMillis()); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java new file mode 100644 index 00000000000..b14ca80901b --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PeriodicJobReceiverTest.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batteryusage; + +import static com.google.common.truth.Truth.assertThat; + +import static org.robolectric.Shadows.shadowOf; + +import android.app.AlarmManager; +import android.content.Context; +import android.content.Intent; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDao; +import com.android.settings.fuelgauge.batteryusage.db.BatteryStateDatabase; +import com.android.settings.testutils.BatteryTestUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowAlarmManager; + +import java.time.Clock; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +/** Tests of {@link PeriodicJobReceiver}. */ +@RunWith(RobolectricTestRunner.class) +public final class PeriodicJobReceiverTest { + private static final Intent JOB_UPDATE_INTENT = + new Intent(PeriodicJobReceiver.ACTION_PERIODIC_JOB_UPDATE); + + private Context mContext; + private BatteryStateDao mDao; + private PeriodicJobReceiver mReceiver; + private PeriodicJobManager mPeriodicJobManager; + private ShadowAlarmManager mShadowAlarmManager; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + mPeriodicJobManager = PeriodicJobManager.getInstance(mContext); + mShadowAlarmManager = shadowOf(mContext.getSystemService(AlarmManager.class)); + ShadowAlarmManager.setCanScheduleExactAlarms(true); + mReceiver = new PeriodicJobReceiver(); + + // Inserts fake data into database for testing. + final BatteryStateDatabase database = BatteryTestUtils.setUpBatteryStateDatabase(mContext); + BatteryTestUtils.insertDataToBatteryStateDatabase( + mContext, Clock.systemUTC().millis(), "com.android.systemui"); + mDao = database.batteryStateDao(); + } + + @After + public void tearDown() { + mPeriodicJobManager.reset(); + } + + @Test + public void onReceive_validAction_refreshesJob() { + mReceiver.onReceive(mContext, JOB_UPDATE_INTENT); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull(); + } + + @Test + public void onReceive_invalidAction_notRefreshesJob() { + mReceiver.onReceive(mContext, new Intent("invalid request update intent")); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + + @Test + public void onReceive_nullIntent_notRefreshesJob() { + mReceiver.onReceive(mContext, /*intent=*/ null); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + + @Test + public void onReceive_containsExpiredData_clearsExpiredDataFromDatabase() + throws InterruptedException { + insertExpiredData(/*shiftDay=*/ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY); + + mReceiver.onReceive(mContext, JOB_UPDATE_INTENT); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0)).hasSize(1); + } + + @Test + public void onReceive_withoutExpiredData_notClearsExpiredDataFromDatabase() + throws InterruptedException { + insertExpiredData(/*shiftDay=*/ DatabaseUtils.DATA_RETENTION_INTERVAL_DAY - 1); + + mReceiver.onReceive(mContext, JOB_UPDATE_INTENT); + + TimeUnit.MILLISECONDS.sleep(100); + assertThat(mDao.getAllAfter(0)).hasSize(3); + } + + @Test + public void onReceive_inWorkProfileMode_notRefreshesJob() { + BatteryTestUtils.setWorkProfile(mContext); + mReceiver.onReceive(mContext, JOB_UPDATE_INTENT); + assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull(); + } + + private void insertExpiredData(int shiftDay) { + final long expiredTimeInMs = + Clock.systemUTC().millis() - Duration.ofDays(shiftDay).toMillis(); + BatteryTestUtils.insertDataToBatteryStateDatabase( + mContext, expiredTimeInMs - 1, "com.android.systemui"); + BatteryTestUtils.insertDataToBatteryStateDatabase( + mContext, expiredTimeInMs, "com.android.systemui"); + // Ensures the testing environment is correct. + assertThat(mDao.getAllAfter(0)).hasSize(3); + } + +}