Notifications working with the app off

This commit is contained in:
koalasat 2024-07-12 21:43:23 +02:00
parent 26a2b7101c
commit a4e1ddb255
8 changed files with 134 additions and 100 deletions

View File

@ -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) {

View File

@ -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();
};
}

View File

@ -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={() => <Text></Text>}
onError={(syntheticEvent) => <Text>{syntheticEvent.type}</Text>}
onLoadEnd={() => setTimeout(onLoadEnd, 3000)}
/>
</SafeAreaView>
);

View File

@ -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

View File

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".MainApplication"
@ -15,6 +16,7 @@
android:theme="@style/AppTheme"
android:extractNativeLibs="true"
>
<service android:name=".NotificationsService" />
<activity
android:name=".MainActivity"
android:label="@string/app_name"

View File

@ -4,26 +4,17 @@ import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.robosats.workers.NotificationWorker;
import java.util.concurrent.TimeUnit;
public class MainActivity extends ReactActivity {
private static final int REQUEST_CODE_POST_NOTIFICATIONS = 1;
@ -31,22 +22,20 @@ public class MainActivity extends ReactActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle extras = getIntent().getExtras();
if (extras != null) {
String coordinator = extras.getString("coordinator");
String token = extras.getString("token");
int order_id = extras.getInt("order_id", 0);
if (order_id > 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);

View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB