Skip to content
Merged
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 @@ -19,12 +19,27 @@

import app.notesr.core.security.crypto.CryptoManager;
import app.notesr.core.security.crypto.CryptoManagerProvider;
import app.notesr.data.DatabaseProvider;
import app.notesr.service.AndroidService;
import app.notesr.service.AndroidServiceEntry;
import app.notesr.service.AndroidServiceRegistry;

/**
* A foreground {@link app.notesr.service.AndroidService} responsible for handling the application's
* lifecycle cleanup when the task is removed
* (e.g., when the app is swiped away from the recent apps list).
*
* <p>This service ensures that sensitive data is cleared and database connections are
* properly closed to maintain data integrity and security. If no other application services
* are running, it performs a full cleanup and terminates the process.</p>
*
* <p>The service operates as a foreground service to ensure the system grants it sufficient
* time to execute cleanup logic during the task removal phase.</p>
*/
public final class AppCloseAndroidService extends AndroidService {

private static final int NOTIFICATION_ID = 1005;

private static final String CHANNEL_ID = "app_close_service_channel";
private static final String CHANNEL_NAME = "App Close Service Channel";

Expand All @@ -51,7 +66,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
}

startForeground(1005, notification, type);
startForeground(NOTIFICATION_ID, notification, type);
register(null, null);

return START_NOT_STICKY;
Expand All @@ -65,27 +80,51 @@ protected AndroidServiceEntry getEntry(String payload, String state) {

@Override
public void onTaskRemoved(Intent rootIntent) {
if (getCurrentRunningServicesCount() == 0) {
CryptoManager cryptoManager = CryptoManagerProvider.getInstance(getApplicationContext());
cryptoManager.destroySecrets();
if (getOtherRunningServicesCount() == 0) {
closeDatabase();
destroySecrets();

stopForeground(true);
stopSelf();
stopForegroundService();
stopService();

super.onTaskRemoved(rootIntent);
System.exit(0);
callSuperOnTaskRemoved(rootIntent);
exitProcess();
} else {
stopForeground(true);
stopSelf();
stopForegroundService();
stopService();
}
}

private long getCurrentRunningServicesCount() {
void callSuperOnTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
}

void stopForegroundService() {
stopForeground(true);
}

void stopService() {
stopSelf();
}

long getOtherRunningServicesCount() {
return AndroidServiceRegistry.getInstance(getApplicationContext())
.getSet()
.stream()
.filter(serviceEntry ->
serviceEntry.getServiceClass() != getClass())
.count();
}

void closeDatabase() {
DatabaseProvider.close();
}

void destroySecrets() {
CryptoManagerProvider.getInstance(getApplicationContext()).destroySecrets();
}

void exitProcess() {
System.exit(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (c) 2026 zHd4
* SPDX-License-Identifier: MIT
*/

package app.notesr.service.lifecycle;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.description;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

import android.content.Intent;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class AppCloseAndroidServiceTest {

private AppCloseAndroidService service;

@BeforeEach
void setUp() {
service = spy(new AppCloseAndroidService());
}

@Test
void testOnTaskRemovedWhenNoOtherServicesRunningClosesEverythingAndExits() {
doReturn(0L).when(service).getOtherRunningServicesCount();
doNothing().when(service).closeDatabase();
doNothing().when(service).destroySecrets();
doNothing().when(service).stopForegroundService();
doNothing().when(service).stopService();
doNothing().when(service).callSuperOnTaskRemoved(any(Intent.class));
doNothing().when(service).exitProcess();

Intent intent = new Intent();

service.onTaskRemoved(intent);

verify(service, description("Database should be closed" +
" when no other services are running"))
.closeDatabase();
verify(service, description("Secrets should be destroyed" +
" when no other services are running"))
.destroySecrets();
verify(service, description("Foreground notification should be stopped" +
" when no other services are running"))
.stopForegroundService();
verify(service, description("Service should stop itself" +
" when no other services are running"))
.stopService();
verify(service, description("Super.onTaskRemoved should be called" +
" when no other services are running"))
.callSuperOnTaskRemoved(intent);
verify(service, description("Process should exit" +
" when no other services are running"))
.exitProcess();
}

@Test
void testOnTaskRemovedWhenOtherServicesRunningOnlyStopsSelf() {
doReturn(1L).when(service).getOtherRunningServicesCount();
doNothing().when(service).stopForegroundService();
doNothing().when(service).stopService();

Intent intent = new Intent();

service.onTaskRemoved(intent);

verify(service, never().description("Database should NOT be closed" +
" when other services are running"))
.closeDatabase();
verify(service, never().description("Secrets should NOT be destroyed" +
" when other services are running"))
.destroySecrets();
verify(service, never().description("Super.onTaskRemoved should NOT be called" +
" when other services are running"))
.callSuperOnTaskRemoved(any(Intent.class));
verify(service, never().description("Process should NOT exit" +
" when other services are running"))
.exitProcess();
verify(service, description("Foreground notification should be stopped" +
" even if other services are running"))
.stopForegroundService();
verify(service, description("Service should stop itself" +
" even if other services are running"))
.stopService();
}
}
Loading