From a4e1ddb2551ac0bb3b0df81356dee5ca6941c7b7 Mon Sep 17 00:00:00 2001 From: koalasat Date: Fri, 12 Jul 2024 21:43:23 +0200 Subject: [PATCH] Notifications working with the app off --- frontend/src/basic/Routes.tsx | 1 + frontend/src/models/Garage.model.ts | 3 + mobile/App.tsx | 13 ++ mobile/android/app/build.gradle | 2 - .../android/app/src/main/AndroidManifest.xml | 2 + .../main/java/com/robosats/MainActivity.java | 68 ++------ ...nWorker.java => NotificationsService.java} | 145 ++++++++++++------ .../app/src/main/res/mipmap-hdpi/ic_icon.png | Bin 0 -> 18940 bytes 8 files changed, 134 insertions(+), 100 deletions(-) rename mobile/android/app/src/main/java/com/robosats/{workers/NotificationWorker.java => NotificationsService.java} (63%) create mode 100644 mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png 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 0000000000000000000000000000000000000000..ab5e6f084e85225c45cba9295f7bb7a1600cfc61 GIT binary patch literal 18940 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 Qhhj**l@~91W9a|C0D?qL-~a#s literal 0 HcmV?d00001