mirror of
https://github.com/vernu/textbee.git
synced 2026-03-03 02:27:00 +00:00
chore: add fallback heartbeat check using fcm
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
package com.vernu.sms.helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.StatFs;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.BuildConfig;
|
||||
import com.vernu.sms.dtos.HeartbeatInputDTO;
|
||||
import com.vernu.sms.dtos.HeartbeatResponseDTO;
|
||||
import com.vernu.sms.dtos.SimInfoCollectionDTO;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class HeartbeatHelper {
|
||||
private static final String TAG = "HeartbeatHelper";
|
||||
|
||||
/**
|
||||
* Collects device information and sends a heartbeat request to the API.
|
||||
*
|
||||
* @param context Application context
|
||||
* @param deviceId Device ID
|
||||
* @param apiKey API key for authentication
|
||||
* @return true if heartbeat was sent successfully, false otherwise
|
||||
*/
|
||||
public static boolean sendHeartbeat(Context context, String deviceId, String apiKey) {
|
||||
if (deviceId == null || deviceId.isEmpty()) {
|
||||
Log.d(TAG, "Device not registered, skipping heartbeat");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
Log.e(TAG, "API key not available, skipping heartbeat");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect device information
|
||||
HeartbeatInputDTO heartbeatInput = new HeartbeatInputDTO();
|
||||
|
||||
try {
|
||||
// Get FCM token (blocking wait)
|
||||
try {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
final String[] fcmToken = new String[1];
|
||||
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
fcmToken[0] = task.getResult();
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
if (latch.await(5, TimeUnit.SECONDS) && fcmToken[0] != null) {
|
||||
heartbeatInput.setFcmToken(fcmToken[0]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get FCM token: " + e.getMessage());
|
||||
// Continue without FCM token
|
||||
}
|
||||
|
||||
// Get battery information
|
||||
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
|
||||
Intent batteryStatus = context.registerReceiver(null, ifilter);
|
||||
if (batteryStatus != null) {
|
||||
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
int batteryPct = (int) ((level / (float) scale) * 100);
|
||||
heartbeatInput.setBatteryPercentage(batteryPct);
|
||||
|
||||
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
heartbeatInput.setIsCharging(isCharging);
|
||||
}
|
||||
|
||||
// Get network type
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cm != null) {
|
||||
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||
if (activeNetwork != null && activeNetwork.isConnected()) {
|
||||
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
heartbeatInput.setNetworkType("wifi");
|
||||
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
|
||||
heartbeatInput.setNetworkType("cellular");
|
||||
} else {
|
||||
heartbeatInput.setNetworkType("none");
|
||||
}
|
||||
} else {
|
||||
heartbeatInput.setNetworkType("none");
|
||||
}
|
||||
}
|
||||
|
||||
// Get app version
|
||||
heartbeatInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
heartbeatInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
|
||||
// Get device uptime
|
||||
heartbeatInput.setDeviceUptimeMillis(SystemClock.uptimeMillis());
|
||||
|
||||
// Get memory information
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
heartbeatInput.setMemoryFreeBytes(runtime.freeMemory());
|
||||
heartbeatInput.setMemoryTotalBytes(runtime.totalMemory());
|
||||
heartbeatInput.setMemoryMaxBytes(runtime.maxMemory());
|
||||
|
||||
// Get storage information
|
||||
File internalStorage = context.getFilesDir();
|
||||
StatFs statFs = new StatFs(internalStorage.getPath());
|
||||
long availableBytes = statFs.getAvailableBytes();
|
||||
long totalBytes = statFs.getTotalBytes();
|
||||
heartbeatInput.setStorageAvailableBytes(availableBytes);
|
||||
heartbeatInput.setStorageTotalBytes(totalBytes);
|
||||
|
||||
// Get system information
|
||||
heartbeatInput.setTimezone(TimeZone.getDefault().getID());
|
||||
heartbeatInput.setLocale(Locale.getDefault().toString());
|
||||
|
||||
// Get receive SMS enabled status
|
||||
boolean receiveSMSEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
heartbeatInput.setReceiveSMSEnabled(receiveSMSEnabled);
|
||||
|
||||
// Collect SIM information
|
||||
SimInfoCollectionDTO simInfoCollection = new SimInfoCollectionDTO();
|
||||
simInfoCollection.setLastUpdated(System.currentTimeMillis());
|
||||
simInfoCollection.setSims(TextBeeUtils.collectSimInfo(context));
|
||||
heartbeatInput.setSimInfo(simInfoCollection);
|
||||
|
||||
// Send heartbeat request
|
||||
Call<HeartbeatResponseDTO> call = ApiManager.getApiService().heartbeat(deviceId, apiKey, heartbeatInput);
|
||||
Response<HeartbeatResponseDTO> response = call.execute();
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
HeartbeatResponseDTO responseBody = response.body();
|
||||
if (responseBody.fcmTokenUpdated) {
|
||||
Log.d(TAG, "FCM token was updated during heartbeat");
|
||||
}
|
||||
Log.d(TAG, "Heartbeat sent successfully");
|
||||
return true;
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send heartbeat. Response code: " + (response.code()));
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Heartbeat API call failed: " + e.getMessage());
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error collecting device information: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if device is eligible to send heartbeat (registered, enabled, heartbeat enabled).
|
||||
*
|
||||
* @param context Application context
|
||||
* @return true if device is eligible, false otherwise
|
||||
*/
|
||||
public static boolean isDeviceEligibleForHeartbeat(Context context) {
|
||||
// Check if device is registered
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
if (deviceId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if device is enabled
|
||||
boolean deviceEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
|
||||
if (!deviceEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if heartbeat feature is enabled
|
||||
boolean heartbeatEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_HEARTBEAT_ENABLED_KEY,
|
||||
true // Default to true
|
||||
);
|
||||
|
||||
return heartbeatEnabled;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import com.vernu.sms.R;
|
||||
import com.vernu.sms.activities.MainActivity;
|
||||
import com.vernu.sms.helpers.SMSHelper;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.helpers.HeartbeatHelper;
|
||||
import com.vernu.sms.helpers.HeartbeatManager;
|
||||
import com.vernu.sms.models.SMSPayload;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
@@ -37,7 +39,16 @@ public class FCMService extends FirebaseMessagingService {
|
||||
Log.d(TAG, remoteMessage.getData().toString());
|
||||
|
||||
try {
|
||||
// Parse SMS payload data
|
||||
// Check message type first
|
||||
String messageType = remoteMessage.getData().get("type");
|
||||
|
||||
if ("heartbeat_check".equals(messageType)) {
|
||||
// Handle heartbeat check request from backend
|
||||
handleHeartbeatCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse SMS payload data (legacy handling)
|
||||
Gson gson = new Gson();
|
||||
SMSPayload smsPayload = gson.fromJson(remoteMessage.getData().get("smsData"), SMSPayload.class);
|
||||
|
||||
@@ -55,6 +66,45 @@ public class FCMService extends FirebaseMessagingService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle heartbeat check request from backend
|
||||
*/
|
||||
private void handleHeartbeatCheck() {
|
||||
Log.d(TAG, "Received heartbeat check request from backend");
|
||||
|
||||
// Check if device is eligible for heartbeat
|
||||
if (!HeartbeatHelper.isDeviceEligibleForHeartbeat(this)) {
|
||||
Log.d(TAG, "Device not eligible for heartbeat, skipping heartbeat check");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get device ID and API key
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
this,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
this,
|
||||
AppConstants.SHARED_PREFS_API_KEY_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
// Send heartbeat using shared helper
|
||||
boolean success = HeartbeatHelper.sendHeartbeat(this, deviceId, apiKey);
|
||||
|
||||
if (success) {
|
||||
Log.d(TAG, "Heartbeat sent successfully in response to backend check");
|
||||
// Ensure scheduled work is added if missing
|
||||
HeartbeatManager.scheduleHeartbeat(this);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send heartbeat in response to backend check");
|
||||
// Still try to ensure scheduled work is added
|
||||
HeartbeatManager.scheduleHeartbeat(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMS to recipients using the provided payload
|
||||
*/
|
||||
|
||||
@@ -1,39 +1,15 @@
|
||||
package com.vernu.sms.workers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.os.StatFs;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.BuildConfig;
|
||||
import com.vernu.sms.dtos.HeartbeatInputDTO;
|
||||
import com.vernu.sms.dtos.HeartbeatResponseDTO;
|
||||
import com.vernu.sms.dtos.SimInfoCollectionDTO;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
import com.vernu.sms.helpers.HeartbeatHelper;
|
||||
|
||||
public class HeartbeatWorker extends Worker {
|
||||
private static final String TAG = "HeartbeatWorker";
|
||||
@@ -47,166 +23,32 @@ public class HeartbeatWorker extends Worker {
|
||||
public Result doWork() {
|
||||
Context context = getApplicationContext();
|
||||
|
||||
// Check if device is registered
|
||||
// Check if device is eligible for heartbeat
|
||||
if (!HeartbeatHelper.isDeviceEligibleForHeartbeat(context)) {
|
||||
Log.d(TAG, "Device not eligible for heartbeat, skipping");
|
||||
return Result.success(); // Not a failure, just skip
|
||||
}
|
||||
|
||||
// Get device ID and API key
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_DEVICE_ID_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
if (deviceId.isEmpty()) {
|
||||
Log.d(TAG, "Device not registered, skipping heartbeat");
|
||||
return Result.success(); // Not a failure, just skip
|
||||
}
|
||||
|
||||
// Check if device is enabled
|
||||
boolean deviceEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
|
||||
if (!deviceEnabled) {
|
||||
Log.d(TAG, "Device not enabled, skipping heartbeat");
|
||||
return Result.success(); // Not a failure, just skip
|
||||
}
|
||||
|
||||
// Check if heartbeat feature is enabled
|
||||
boolean heartbeatEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_HEARTBEAT_ENABLED_KEY,
|
||||
true // Default to true
|
||||
);
|
||||
|
||||
if (!heartbeatEnabled) {
|
||||
Log.d(TAG, "Heartbeat feature disabled, skipping heartbeat");
|
||||
return Result.success(); // Not a failure, just skip
|
||||
}
|
||||
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_API_KEY_KEY,
|
||||
""
|
||||
);
|
||||
|
||||
if (apiKey.isEmpty()) {
|
||||
Log.e(TAG, "API key not available, skipping heartbeat");
|
||||
return Result.success(); // Not a failure, just skip
|
||||
}
|
||||
// Send heartbeat using shared helper
|
||||
boolean success = HeartbeatHelper.sendHeartbeat(context, deviceId, apiKey);
|
||||
|
||||
// Collect device information
|
||||
HeartbeatInputDTO heartbeatInput = new HeartbeatInputDTO();
|
||||
|
||||
try {
|
||||
// Get FCM token (blocking wait)
|
||||
try {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
final String[] fcmToken = new String[1];
|
||||
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
fcmToken[0] = task.getResult();
|
||||
}
|
||||
latch.countDown();
|
||||
});
|
||||
if (latch.await(5, TimeUnit.SECONDS) && fcmToken[0] != null) {
|
||||
heartbeatInput.setFcmToken(fcmToken[0]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get FCM token: " + e.getMessage());
|
||||
// Continue without FCM token
|
||||
}
|
||||
|
||||
// Get battery information
|
||||
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
|
||||
Intent batteryStatus = context.registerReceiver(null, ifilter);
|
||||
if (batteryStatus != null) {
|
||||
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
int batteryPct = (int) ((level / (float) scale) * 100);
|
||||
heartbeatInput.setBatteryPercentage(batteryPct);
|
||||
|
||||
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
|
||||
status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
heartbeatInput.setIsCharging(isCharging);
|
||||
}
|
||||
|
||||
// Get network type
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
if (cm != null) {
|
||||
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
|
||||
if (activeNetwork != null && activeNetwork.isConnected()) {
|
||||
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
|
||||
heartbeatInput.setNetworkType("wifi");
|
||||
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
|
||||
heartbeatInput.setNetworkType("cellular");
|
||||
} else {
|
||||
heartbeatInput.setNetworkType("none");
|
||||
}
|
||||
} else {
|
||||
heartbeatInput.setNetworkType("none");
|
||||
}
|
||||
}
|
||||
|
||||
// Get app version
|
||||
heartbeatInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
heartbeatInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
|
||||
// Get device uptime
|
||||
heartbeatInput.setDeviceUptimeMillis(SystemClock.uptimeMillis());
|
||||
|
||||
// Get memory information
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
heartbeatInput.setMemoryFreeBytes(runtime.freeMemory());
|
||||
heartbeatInput.setMemoryTotalBytes(runtime.totalMemory());
|
||||
heartbeatInput.setMemoryMaxBytes(runtime.maxMemory());
|
||||
|
||||
// Get storage information
|
||||
File internalStorage = context.getFilesDir();
|
||||
StatFs statFs = new StatFs(internalStorage.getPath());
|
||||
long availableBytes = statFs.getAvailableBytes();
|
||||
long totalBytes = statFs.getTotalBytes();
|
||||
heartbeatInput.setStorageAvailableBytes(availableBytes);
|
||||
heartbeatInput.setStorageTotalBytes(totalBytes);
|
||||
|
||||
// Get system information
|
||||
heartbeatInput.setTimezone(TimeZone.getDefault().getID());
|
||||
heartbeatInput.setLocale(Locale.getDefault().toString());
|
||||
|
||||
// Get receive SMS enabled status
|
||||
boolean receiveSMSEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
|
||||
context,
|
||||
AppConstants.SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY,
|
||||
false
|
||||
);
|
||||
heartbeatInput.setReceiveSMSEnabled(receiveSMSEnabled);
|
||||
|
||||
// Collect SIM information
|
||||
SimInfoCollectionDTO simInfoCollection = new SimInfoCollectionDTO();
|
||||
simInfoCollection.setLastUpdated(System.currentTimeMillis());
|
||||
simInfoCollection.setSims(TextBeeUtils.collectSimInfo(context));
|
||||
heartbeatInput.setSimInfo(simInfoCollection);
|
||||
|
||||
// Send heartbeat request
|
||||
Call<HeartbeatResponseDTO> call = ApiManager.getApiService().heartbeat(deviceId, apiKey, heartbeatInput);
|
||||
Response<HeartbeatResponseDTO> response = call.execute();
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
HeartbeatResponseDTO responseBody = response.body();
|
||||
if (responseBody.fcmTokenUpdated) {
|
||||
Log.d(TAG, "FCM token was updated during heartbeat");
|
||||
}
|
||||
Log.d(TAG, "Heartbeat sent successfully");
|
||||
return Result.success();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send heartbeat. Response code: " + (response.code()));
|
||||
return Result.retry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Heartbeat API call failed: " + e.getMessage());
|
||||
return Result.retry();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error collecting device information: " + e.getMessage());
|
||||
if (success) {
|
||||
return Result.success();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send heartbeat, will retry");
|
||||
return Result.retry();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ConfigModule } from '@nestjs/config'
|
||||
import { SmsQueueService } from './queue/sms-queue.service'
|
||||
import { SmsQueueProcessor } from './queue/sms-queue.processor'
|
||||
import { SmsStatusUpdateTask } from './tasks/sms-status-update.task'
|
||||
import { HeartbeatCheckTask } from './tasks/heartbeat-check.task'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -50,7 +51,7 @@ import { SmsStatusUpdateTask } from './tasks/sms-status-update.task'
|
||||
ConfigModule,
|
||||
],
|
||||
controllers: [GatewayController],
|
||||
providers: [GatewayService, SmsQueueService, SmsQueueProcessor, SmsStatusUpdateTask],
|
||||
providers: [GatewayService, SmsQueueService, SmsQueueProcessor, SmsStatusUpdateTask, HeartbeatCheckTask],
|
||||
exports: [MongooseModule, GatewayService, SmsQueueService],
|
||||
})
|
||||
export class GatewayModule {}
|
||||
|
||||
97
api/src/gateway/tasks/heartbeat-check.task.ts
Normal file
97
api/src/gateway/tasks/heartbeat-check.task.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { Cron, CronExpression } from '@nestjs/schedule'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model } from 'mongoose'
|
||||
import { Device, DeviceDocument } from '../schemas/device.schema'
|
||||
import * as firebaseAdmin from 'firebase-admin'
|
||||
import { Message } from 'firebase-admin/messaging'
|
||||
|
||||
@Injectable()
|
||||
export class HeartbeatCheckTask {
|
||||
private readonly logger = new Logger(HeartbeatCheckTask.name)
|
||||
|
||||
constructor(
|
||||
@InjectModel(Device.name) private deviceModel: Model<DeviceDocument>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Cron job that runs every 5 minutes to check for devices with stale heartbeats
|
||||
* (>30 minutes) and send FCM push notifications to trigger heartbeat requests.
|
||||
*/
|
||||
@Cron(CronExpression.EVERY_5_MINUTES)
|
||||
async checkAndTriggerStaleHeartbeats() {
|
||||
this.logger.log('Running cron job to check for stale heartbeats')
|
||||
|
||||
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000)
|
||||
|
||||
try {
|
||||
// Find devices with stale heartbeats
|
||||
const devices = await this.deviceModel.find({
|
||||
heartbeatEnabled: true,
|
||||
enabled: true,
|
||||
$or: [
|
||||
{ lastHeartbeat: null },
|
||||
{ lastHeartbeat: { $lt: thirtyMinutesAgo } },
|
||||
],
|
||||
fcmToken: { $exists: true, $ne: null },
|
||||
})
|
||||
|
||||
if (devices.length === 0) {
|
||||
this.logger.log('No devices with stale heartbeats found')
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${devices.length} device(s) with stale heartbeats, sending FCM notifications`,
|
||||
)
|
||||
|
||||
// Send FCM messages to trigger heartbeats
|
||||
const fcmMessages: Message[] = []
|
||||
const deviceIds: string[] = []
|
||||
|
||||
for (const device of devices) {
|
||||
if (!device.fcmToken) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fcmMessage: Message = {
|
||||
data: {
|
||||
type: 'heartbeat_check',
|
||||
},
|
||||
token: device.fcmToken,
|
||||
android: {
|
||||
priority: 'high',
|
||||
},
|
||||
}
|
||||
|
||||
fcmMessages.push(fcmMessage)
|
||||
deviceIds.push(device._id.toString())
|
||||
}
|
||||
|
||||
if (fcmMessages.length === 0) {
|
||||
this.logger.warn('No valid FCM tokens found for devices with stale heartbeats')
|
||||
return
|
||||
}
|
||||
|
||||
// Send FCM messages
|
||||
const response = await firebaseAdmin.messaging().sendEach(fcmMessages)
|
||||
|
||||
this.logger.log(
|
||||
`Sent ${response.successCount} heartbeat check FCM notification(s), ${response.failureCount} failed`,
|
||||
)
|
||||
|
||||
// Log failures for debugging
|
||||
if (response.failureCount > 0) {
|
||||
response.responses.forEach((resp, index) => {
|
||||
if (!resp.success) {
|
||||
this.logger.error(
|
||||
`Failed to send heartbeat check to device ${deviceIds[index]}: ${resp.error?.message || 'Unknown error'}`,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error checking and triggering stale heartbeats', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user