diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 2f0e761..5a08d7a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
+
call, Response response) {
if (response.isSuccessful()) {
Log.d(TAG, "Device info updated successfully after boot");
+
+ // Sync heartbeatIntervalMinutes from server response
+ if (response.body() != null && response.body().data != null) {
+ if (response.body().data.get("heartbeatIntervalMinutes") != null) {
+ Object intervalObj = response.body().data.get("heartbeatIntervalMinutes");
+ if (intervalObj instanceof Number) {
+ int intervalMinutes = ((Number) intervalObj).intValue();
+ SharedPreferenceHelper.setSharedPreferenceInt(context, AppConstants.SHARED_PREFS_HEARTBEAT_INTERVAL_MINUTES_KEY, intervalMinutes);
+ Log.d(TAG, "Synced heartbeat interval from server: " + intervalMinutes + " minutes");
+ }
+ }
+ }
} else {
Log.e(TAG, "Failed to update device info after boot. Response code: " + response.code());
}
diff --git a/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java b/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java
index e561f29..6874a17 100644
--- a/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java
+++ b/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java
@@ -4,6 +4,8 @@ import com.vernu.sms.dtos.SMSDTO;
import com.vernu.sms.dtos.SMSForwardResponseDTO;
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
+import com.vernu.sms.dtos.HeartbeatInputDTO;
+import com.vernu.sms.dtos.HeartbeatResponseDTO;
import retrofit2.Call;
import retrofit2.http.Body;
@@ -24,4 +26,7 @@ public interface GatewayApiService {
@PATCH("gateway/devices/{deviceId}/sms-status")
Call updateSMSStatus(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() SMSDTO body);
+
+ @POST("gateway/devices/{deviceId}/heartbeat")
+ Call heartbeat(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() HeartbeatInputDTO body);
}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/vernu/sms/workers/HeartbeatWorker.java b/android/app/src/main/java/com/vernu/sms/workers/HeartbeatWorker.java
new file mode 100644
index 0000000..0558b2a
--- /dev/null
+++ b/android/app/src/main/java/com/vernu/sms/workers/HeartbeatWorker.java
@@ -0,0 +1,205 @@
+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.helpers.SharedPreferenceHelper;
+
+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 HeartbeatWorker extends Worker {
+ private static final String TAG = "HeartbeatWorker";
+
+ public HeartbeatWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ Context context = getApplicationContext();
+
+ // Check if device is registered
+ 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
+ }
+
+ // 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);
+
+ // Send heartbeat request
+ Call call = ApiManager.getApiService().heartbeat(deviceId, apiKey, heartbeatInput);
+ Response 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());
+ return Result.retry();
+ }
+ }
+}