From 4e1353064d95d5294d1a658504ea5189dfbfa151 Mon Sep 17 00:00:00 2001 From: Mihai-Cristian Condrea Date: Tue, 16 Sep 2025 21:21:44 +0300 Subject: [PATCH] Add tests for app usage notification worker --- .../workers/AppUsageNotificationWorker.java | 24 +- .../AppUsageNotificationWorkerTest.java | 213 ++++++++++++++++++ 2 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorkerTest.java diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorker.java b/app/src/main/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorker.java index bf840797..655442bc 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorker.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorker.java @@ -7,7 +7,9 @@ import android.os.Build; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; import androidx.work.Worker; @@ -22,6 +24,8 @@ public class AppUsageNotificationWorker extends Worker { private final SharedPreferences sharedPreferences; + @Nullable + private final NotificationManager notificationManager; /** * Constructor for {@link AppUsageNotificationWorker}. @@ -31,8 +35,20 @@ public class AppUsageNotificationWorker extends Worker { */ public AppUsageNotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + this(context, + workerParams, + PreferenceManager.getDefaultSharedPreferences(context), + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)); + } + + @VisibleForTesting + AppUsageNotificationWorker(@NonNull Context context, + @NonNull WorkerParameters workerParams, + @NonNull SharedPreferences sharedPreferences, + @Nullable NotificationManager notificationManager) { super(context, workerParams); - this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.sharedPreferences = sharedPreferences; + this.notificationManager = notificationManager; } /** @@ -49,10 +65,8 @@ public Result doWork() { long notificationThreshold = 3 * 24 * 60 * 60 * 1000; long lastUsedTimestamp = sharedPreferences.getLong("lastUsed", 0); - if (currentTimestamp - lastUsedTimestamp > notificationThreshold) { - NotificationManager notificationManager = - (NotificationManager) getApplicationContext().getSystemService( - Context.NOTIFICATION_SERVICE); + if (lastUsedTimestamp != 0 && notificationManager != null + && currentTimestamp - lastUsedTimestamp > notificationThreshold) { String appUsageChannelId = "app_usage_channel"; NotificationChannel appUsageChannel = new NotificationChannel( appUsageChannelId, diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorkerTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorkerTest.java new file mode 100644 index 00000000..b71861cf --- /dev/null +++ b/app/src/test/java/com/d4rk/androidtutorials/java/notifications/workers/AppUsageNotificationWorkerTest.java @@ -0,0 +1,213 @@ +package com.d4rk.androidtutorials.java.notifications.workers; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.res.Resources; +import android.os.Build; +import android.test.mock.MockContext; + +import androidx.work.WorkerParameters; + +import com.d4rk.androidtutorials.java.R; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.TimeUnit; + +/** + * Tests for {@link AppUsageNotificationWorker}. + */ +public class AppUsageNotificationWorkerTest { + + private static final String PACKAGE_NAME = "com.d4rk.androidtutorials.java"; + + private AutoCloseable closeable; + + @Mock + private SharedPreferences sharedPreferences; + + @Mock + private SharedPreferences.Editor editor; + + @Mock + private NotificationManager notificationManager; + + @Mock + private WorkerParameters workerParameters; + + @Mock + private Resources resources; + + @Before + public void setUp() { + closeable = MockitoAnnotations.openMocks(this); + + when(sharedPreferences.edit()).thenReturn(editor); + when(editor.putLong(anyString(), anyLong())).thenReturn(editor); + + when(resources.getString(R.string.app_usage_notifications)) + .thenReturn("App usage notifications"); + when(resources.getString(R.string.notification_last_time_used_title)) + .thenReturn("We miss you"); + when(resources.getString(R.string.summary_notification_last_time_used)) + .thenReturn("Come back and code with us"); + when(resources.getResourcePackageName(R.drawable.ic_notification_important)) + .thenReturn(PACKAGE_NAME); + when(resources.getResourceTypeName(R.drawable.ic_notification_important)) + .thenReturn("drawable"); + when(resources.getResourceEntryName(R.drawable.ic_notification_important)) + .thenReturn("ic_notification_important"); + when(resources.getResourceName(R.drawable.ic_notification_important)) + .thenReturn(PACKAGE_NAME + ":drawable/ic_notification_important"); + } + + @After + public void tearDown() throws Exception { + closeable.close(); + } + + @Test + public void doWork_postsNotificationWhenBeyondThreshold() { + long threshold = TimeUnit.DAYS.toMillis(3); + long lastUsed = System.currentTimeMillis() - threshold - TimeUnit.HOURS.toMillis(1); + when(sharedPreferences.getLong(eq("lastUsed"), anyLong())).thenReturn(lastUsed); + + AppUsageNotificationWorker worker = createWorker(); + + worker.doWork(); + + verify(sharedPreferences).getLong("lastUsed", 0); + verify(notificationManager).createNotificationChannel(any(NotificationChannel.class)); + verify(notificationManager).notify(eq(0), any(Notification.class)); + + ArgumentCaptor timestampCaptor = ArgumentCaptor.forClass(Long.class); + verify(editor).putLong(eq("lastUsed"), timestampCaptor.capture()); + verify(editor).apply(); + + long updatedTimestamp = timestampCaptor.getValue(); + assertTrue(updatedTimestamp > lastUsed); + } + + @Test + public void doWork_skipsNotificationWhenWithinThreshold() { + long threshold = TimeUnit.DAYS.toMillis(3); + long lastUsed = System.currentTimeMillis() - threshold + TimeUnit.HOURS.toMillis(2); + when(sharedPreferences.getLong(eq("lastUsed"), anyLong())).thenReturn(lastUsed); + + AppUsageNotificationWorker worker = createWorker(); + + worker.doWork(); + + verify(sharedPreferences).getLong("lastUsed", 0); + verify(notificationManager, never()).createNotificationChannel(any(NotificationChannel.class)); + verify(notificationManager, never()).notify(anyInt(), any(Notification.class)); + + ArgumentCaptor timestampCaptor = ArgumentCaptor.forClass(Long.class); + verify(editor).putLong(eq("lastUsed"), timestampCaptor.capture()); + verify(editor).apply(); + + long updatedTimestamp = timestampCaptor.getValue(); + assertTrue(updatedTimestamp > lastUsed); + } + + @Test + public void doWork_skipsNotificationWhenTimestampMissing() { + when(sharedPreferences.getLong(eq("lastUsed"), anyLong())).thenReturn(0L); + + AppUsageNotificationWorker worker = createWorker(); + + worker.doWork(); + + verify(sharedPreferences).getLong("lastUsed", 0); + verify(notificationManager, never()).createNotificationChannel(any(NotificationChannel.class)); + verify(notificationManager, never()).notify(anyInt(), any(Notification.class)); + + ArgumentCaptor timestampCaptor = ArgumentCaptor.forClass(Long.class); + verify(editor).putLong(eq("lastUsed"), timestampCaptor.capture()); + verify(editor).apply(); + + long updatedTimestamp = timestampCaptor.getValue(); + assertTrue(updatedTimestamp > 0L); + } + + private AppUsageNotificationWorker createWorker() { + TestContext context = new TestContext(sharedPreferences, notificationManager, resources); + return new AppUsageNotificationWorker(context, workerParameters, sharedPreferences, notificationManager); + } + + private static class TestContext extends MockContext { + + private final SharedPreferences sharedPreferences; + private final NotificationManager notificationManager; + private final Resources resources; + private final ApplicationInfo applicationInfo; + + TestContext(SharedPreferences sharedPreferences, + NotificationManager notificationManager, + Resources resources) { + this.sharedPreferences = sharedPreferences; + this.notificationManager = notificationManager; + this.resources = resources; + this.applicationInfo = new ApplicationInfo(); + this.applicationInfo.packageName = PACKAGE_NAME; + this.applicationInfo.targetSdkVersion = Build.VERSION_CODES.O; + } + + @Override + public Context getApplicationContext() { + return this; + } + + @Override + public SharedPreferences getSharedPreferences(String name, int mode) { + return sharedPreferences; + } + + @Override + public Object getSystemService(String name) { + if (Context.NOTIFICATION_SERVICE.equals(name)) { + return notificationManager; + } + return super.getSystemService(name); + } + + @Override + public Resources getResources() { + return resources; + } + + @Override + public String getPackageName() { + return PACKAGE_NAME; + } + + @Override + public ApplicationInfo getApplicationInfo() { + return applicationInfo; + } + + @Override + public String getOpPackageName() { + return PACKAGE_NAME; + } + } +}