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