Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +24,8 @@
public class AppUsageNotificationWorker extends Worker {

private final SharedPreferences sharedPreferences;
@Nullable
private final NotificationManager notificationManager;

/**
* Constructor for {@link AppUsageNotificationWorker}.
Expand All @@ -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;
}

/**
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
Comment on lines +88 to +100

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Avoid invoking Android framework stubs in local unit tests

The new AppUsageNotificationWorkerTest lives under app/src/test, but the test methods construct the worker and exercise NotificationManager.createNotificationChannel and other framework APIs. Local JVM tests in this module run with the stubbed android.jar, so any call into NotificationManager or NotificationChannel will throw RuntimeException("Method … not mocked") unless the test runs under Robolectric or instrumentation. As-is, ./gradlew test will consistently fail once the Android SDK path issue is fixed. Consider converting this test to Robolectric or moving it under androidTest so that Android APIs are available.

Useful? React with 👍 / 👎.


ArgumentCaptor<Long> 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<Long> 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<Long> 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;
}
}
}
Loading