mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 12:11:35 +00:00
Notifications working with the app off
This commit is contained in:
parent
26a2b7101c
commit
a4e1ddb255
@ -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) {
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
BIN
mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png
Normal file
BIN
mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Loading…
Reference in New Issue
Block a user