diff --git a/frontend/src/basic/Routes.tsx b/frontend/src/basic/Routes.tsx
index f0ab7d20..93797b81 100644
--- a/frontend/src/basic/Routes.tsx
+++ b/frontend/src/basic/Routes.tsx
@@ -13,6 +13,7 @@ const Routes: React.FC = () => {
useEffect(() => {
window.addEventListener('navigateToPage', (event) => {
+ console.log('navigateToPage', JSON.stringify(event));
const orderId = event?.detail?.order_id;
const coordinator = event?.detail?.coordinator;
if (orderId && coordinator) {
diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts
index 48a28a30..523e1f92 100644
--- a/frontend/src/models/Garage.model.ts
+++ b/frontend/src/models/Garage.model.ts
@@ -99,6 +99,7 @@ class Garage {
const slot = this.getSlot(token);
if (attributes) {
if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken);
+ this.save();
this.triggerHook('onRobotUpdate');
}
return slot;
@@ -106,6 +107,7 @@ class Garage {
setCurrentSlot: (currentSlot: string) => void = (currentSlot) => {
this.currentSlot = currentSlot;
+ this.save();
this.triggerHook('onRobotUpdate');
};
@@ -181,6 +183,7 @@ class Garage {
Object.values(this.slots).forEach((slot) => {
slot.syncCoordinator(coordinator, this);
});
+ this.save();
};
}
diff --git a/mobile/App.tsx b/mobile/App.tsx
index 59dca69d..383a7734 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -26,6 +26,7 @@ const App = () => {
useEffect(() => {
TorModule.start();
DeviceEventEmitter.addListener('navigateToPage', (payload) => {
+ window.navigateToPage = payload;
injectMessage({
category: 'system',
type: 'navigateToPage',
@@ -63,6 +64,17 @@ const App = () => {
);
};
+ const onLoadEnd = () => {
+ if (window.navigateToPage) {
+ injectMessage({
+ category: 'system',
+ type: 'navigateToPage',
+ detail: window.navigateToPage,
+ });
+ window.navigateToPage = undefined;
+ }
+ };
+
const init = (responseId: string) => {
const loadCookie = async (key: string) => {
return await EncryptedStorage.getItem(key).then((value) => {
@@ -207,6 +219,7 @@ const App = () => {
allowsLinkPreview={false}
renderLoading={() => }
onError={(syntheticEvent) => {syntheticEvent.type}}
+ onLoadEnd={() => setTimeout(onLoadEnd, 3000)}
/>
);
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index f4c949cf..42b7c578 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -300,8 +300,6 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
- implementation "androidx.work:work-runtime:2.7.1"
-
if (enableHermes) {
//noinspection GradleDynamicVersion
implementation("com.facebook.react:hermes-engine:+") { // From node_modules
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index 0a9777a3..8ef828f5 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
+
0) {
- navigateToPage(coordinator, order_id);
- }
- }
-
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_CODE_POST_NOTIFICATIONS);
} else {
- // Permission already granted, schedule your work
- scheduleFirstNotificationTask();
- schedulePeriodicNotificationTask();
+ Intent serviceIntent = new Intent(getApplicationContext(), NotificationsService.class);
+ getApplicationContext().startService(serviceIntent);
+ }
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ String coordinator = intent.getStringExtra("coordinator");
+ int order_id = intent.getIntExtra("order_id", 0);
+ if (order_id > 0) {
+ navigateToPage(coordinator, order_id);
+ }
}
}
@@ -86,8 +75,8 @@ public class MainActivity extends ReactActivity {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_POST_NOTIFICATIONS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- scheduleFirstNotificationTask();
- schedulePeriodicNotificationTask();
+ Intent serviceIntent = new Intent(getApplicationContext(), NotificationsService.class);
+ getApplicationContext().startService(serviceIntent);
} else {
// Permission denied, handle accordingly
// Maybe show a message to the user explaining why the permission is necessary
@@ -106,37 +95,6 @@ public class MainActivity extends ReactActivity {
}
}
- private void scheduleFirstNotificationTask() {
- OneTimeWorkRequest workRequest =
- new OneTimeWorkRequest.Builder(NotificationWorker.class)
- .setInitialDelay(20, TimeUnit.SECONDS)
- .build();
-
- WorkManager.getInstance(getApplicationContext())
- .enqueue(workRequest);
- }
-
- private void schedulePeriodicNotificationTask() {
- // Trigger the WorkManager setup and enqueueing here
- PeriodicWorkRequest periodicWorkRequest =
- new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES)
- .build();
-
- WorkManager.getInstance(getApplicationContext())
- .enqueueUniquePeriodicWork("RobosatsNotificationsWork",
- ExistingPeriodicWorkPolicy.KEEP, periodicWorkRequest);
- }
-
- private void scheduleInitialTask() {
- OneTimeWorkRequest workRequest =
- new OneTimeWorkRequest.Builder(NotificationWorker.class)
- .setInitialDelay(25, TimeUnit.SECONDS)
- .build();
-
- WorkManager.getInstance(getApplicationContext())
- .enqueue(workRequest);
- }
-
public static class MainActivityDelegate extends ReactActivityDelegate {
public MainActivityDelegate(ReactActivity activity, String mainComponentName) {
super(activity, mainComponentName);
diff --git a/mobile/android/app/src/main/java/com/robosats/workers/NotificationWorker.java b/mobile/android/app/src/main/java/com/robosats/NotificationsService.java
similarity index 63%
rename from mobile/android/app/src/main/java/com/robosats/workers/NotificationWorker.java
rename to mobile/android/app/src/main/java/com/robosats/NotificationsService.java
index db0dc3db..537e7d9e 100644
--- a/mobile/android/app/src/main/java/com/robosats/workers/NotificationWorker.java
+++ b/mobile/android/app/src/main/java/com/robosats/NotificationsService.java
@@ -1,23 +1,23 @@
-package com.robosats.workers;
+package com.robosats;
import android.app.Application;
+import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.Service;
import android.content.Context;
-
import android.content.Intent;
import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
-import androidx.work.Worker;
-import androidx.work.WorkerParameters;
import com.facebook.react.bridge.ReactApplicationContext;
-import com.robosats.MainActivity;
-import com.robosats.R;
import com.robosats.tor.TorKmp;
import com.robosats.tor.TorKmpManager;
@@ -26,10 +26,16 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
-import java.net.Proxy;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.Objects;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import kotlin.UninitializedPropertyAccessException;
@@ -39,21 +45,82 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
-public class NotificationWorker extends Worker {
+public class NotificationsService extends Service {
+ private Handler handler;
+ private Runnable periodicTask;
private static final String CHANNEL_ID = "robosats_notifications";
+ private static final int NOTIFICATION_ID = 76453;
+ private static final long INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
private static final String PREFS_NAME_NOTIFICATION = "Notifications";
private static final String PREFS_NAME_SYSTEM = "System";
private static final String KEY_DATA_SLOTS = "Slots";
private static final String KEY_DATA_PROXY = "UsePoxy";
private static final String KEY_DATA_FEDERATION = "Federation";
- public NotificationWorker(Context context, WorkerParameters params) {
- super(context, params);
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
}
@Override
- public Result doWork() {
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ createNotificationChannel();
+ startForeground(NOTIFICATION_ID, buildServiceNotification());
+ handler = new Handler();
+ periodicTask = new Runnable() {
+ @Override
+ public void run() {
+ Log.d("NotificationsService", "Running periodic task");
+ executeBackgroundTask();
+ handler.postDelayed(periodicTask, INTERVAL_MS);
+ }
+ };
+
+ Log.d("NotificationsService", "Squeduling periodic task");
+ handler.postDelayed(periodicTask, 5000);
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (handler != null && periodicTask != null) {
+ handler.removeCallbacks(periodicTask);
+ }
+
+ stopForeground(true);
+ }
+
+ private void createNotificationChannel() {
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ "Robosats",
+ NotificationManager.IMPORTANCE_DEFAULT
+ );
+ manager.createNotificationChannel(channel);
+ }
+
+ private void executeBackgroundTask() {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.submit(this::checkNotifications);
+ executor.shutdown();
+ }
+
+ private Notification buildServiceNotification() {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Tor Notifications")
+ .setContentText("The app will run on the background to send you notifications about your orders.")
+ .setSmallIcon(R.mipmap.ic_icon)
+ .setTicker("Robosats");
+
+ return builder.build();
+ }
+
+ public void checkNotifications() {
+ Log.d("NotificationsService", "checkNotifications");
SharedPreferences sharedPreferences =
getApplicationContext()
.getSharedPreferences(PREFS_NAME_NOTIFICATION, ReactApplicationContext.MODE_PRIVATE);
@@ -69,19 +136,13 @@ public class NotificationWorker extends Worker {
JSONObject slot = (JSONObject) slots.get(robotToken);
JSONObject robots = slot.getJSONObject("robots");
JSONObject coordinatorRobot;
- String activeShortAlias;
- try {
- activeShortAlias = slot.getString("activeShortAlias");
- coordinatorRobot = robots.getJSONObject(activeShortAlias);
- fetchNotifications(coordinatorRobot, activeShortAlias);
- } catch (JSONException | InterruptedException e) {
- Log.d("JSON error", String.valueOf(e));
- }
+ String shortAlias = slot.getString("activeShortAlias");
+ coordinatorRobot = robots.getJSONObject(shortAlias);
+ fetchNotifications(coordinatorRobot, shortAlias);
}
- } catch (JSONException e) {
- Log.d("JSON error", String.valueOf(e));
+ } catch (JSONException | InterruptedException e) {
+ Log.d("NotificationsService", "Error reading garage: " + e);
}
- return Result.success();
}
private void fetchNotifications(JSONObject robot, String coordinator) throws JSONException, InterruptedException {
@@ -93,11 +154,11 @@ public class NotificationWorker extends Worker {
JSONObject federation = new JSONObject(sharedPreferences.getString(KEY_DATA_FEDERATION, "{}"));
long unix_time_millis = sharedPreferences.getLong(token, 0);
String url = federation.getString(coordinator) + "/api/notifications";
-// if (unix_time_millis > 0) {
-// String last_created_at = String
-// .valueOf(LocalDateTime.ofInstant(Instant.ofEpochMilli(unix_time_millis), ZoneId.of("UTC")));
-// url += "?created_at=" + last_created_at;
-// }
+ if (unix_time_millis > 0) {
+ String last_created_at = String
+ .valueOf(LocalDateTime.ofInstant(Instant.ofEpochMilli(unix_time_millis), ZoneId.of("UTC")));
+ url += "?created_at=" + last_created_at;
+ }
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
@@ -120,7 +181,7 @@ public class NotificationWorker extends Worker {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
displayErrorNotification();
- Log.d("RobosatsError", e.toString());
+ Log.d("NotificationsService", "Error fetching coordinator: " + e.toString());
}
@Override
@@ -142,8 +203,17 @@ public class NotificationWorker extends Worker {
displayOrderNotification(order_id, notification.getString("title"), coordinator);
+ long milliseconds;
+ try {
+ String created_at = notification.getString("created_at");
+ LocalDateTime datetime = LocalDateTime.parse(created_at, DateTimeFormatter.ISO_DATE_TIME);
+ milliseconds = datetime.toInstant(ZoneOffset.UTC).toEpochMilli() + 1000;
+ } catch (JSONException e) {
+ milliseconds = System.currentTimeMillis();
+ }
+
SharedPreferences.Editor editor = sharedPreferences.edit();
- editor.putLong(token, System.currentTimeMillis());
+ editor.putLong(token, milliseconds);
editor.apply();
}
} catch (JSONException e) {
@@ -158,11 +228,6 @@ public class NotificationWorker extends Worker {
NotificationManager notificationManager = (NotificationManager)
getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
- order_id.toString(),
- NotificationManager.IMPORTANCE_HIGH);
- notificationManager.createNotificationChannel(channel);
-
Intent intent = new Intent(this.getApplicationContext(), MainActivity.class);
intent.putExtra("coordinator", coordinator);
intent.putExtra("order_id", order_id);
@@ -173,7 +238,7 @@ public class NotificationWorker extends Worker {
new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setContentTitle("Order #" + order_id)
.setContentText(message)
- .setSmallIcon(R.mipmap.ic_launcher_round)
+ .setSmallIcon(R.mipmap.ic_icon)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
@@ -185,16 +250,11 @@ public class NotificationWorker extends Worker {
NotificationManager notificationManager = (NotificationManager)
getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
- "robosats_error",
- NotificationManager.IMPORTANCE_HIGH);
- notificationManager.createNotificationChannel(channel);
-
NotificationCompat.Builder builder =
new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
.setContentTitle("Connection Error")
.setContentText("There was an error while connecting to the Tor network.")
- .setSmallIcon(R.mipmap.ic_launcher_round)
+ .setSmallIcon(R.mipmap.ic_icon)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true);
@@ -225,4 +285,3 @@ public class NotificationWorker extends Worker {
return torKmp;
}
}
-
diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png
new file mode 100644
index 00000000..ab5e6f08
Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png differ