diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af56f61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea/ diff --git a/README.md b/README.md index 9433dfd..ae25391 100644 --- a/README.md +++ b/README.md @@ -44,25 +44,54 @@ The port can be changed in the ``server/.env`` file ## Create a Notification > ### Example using [curl](https://curl.se/) > ```` -> curl '127.0.0.1:5000' \ +> curl '127.0.0.1:3000' \ > --header 'Content-Type: application/json' \ > --data '{ > "title": "Foo Bar Baz!", -> "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." +> "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", +> "url": "http://example.com", > }' > ```` +>|Property|Type|Description|Required| +>|---|---|---|---| +>|title|String|The title of the notification|**Yes**| +>|message|String|The longer text that will be included in the notification|No| +>|url|String|Open the URL on notifcation press|No| +> +> #### Response +> `Created 201` + +## Get All Notifications +> +> ```` +> curl '127.0.0.1:3000' +> ```` +> #### Response +> ```` +>[ +> { +> "title": "Foo Bar Baz!", +> "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", +> "url": "http://example.com", +> }, +> ... +>] +> ```` + +## Get Latest Notifications +> +> ```` +> curl '127.0.0.1:3000/latest' +> ```` > #### Response > ```` ->{ -> "status": 201, -> "message": "Successfully created notification", -> "data": { -> "title": "Foo Bar Baz!", -> "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit." -> } ->} +> { +> "title": "Foo Bar Baz!", +> "message": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", +> "url": "http://example.com", +> } > ```` -> + ## Connect to the server stream In the configuration tab type in the `/events` endpoint on your server diff --git a/android/app/build.gradle b/android/app/build.gradle index 9063d8e..ccda886 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "com.viktorholk.apipushnotifications" minSdk 26 targetSdk 35 - versionCode 7 - versionName "1.2.0" + versionCode 8 + versionName "1.2.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/app/src/main/java/com/viktorholk/apipushnotifications/ConfigurationFragment.java b/android/app/src/main/java/com/viktorholk/apipushnotifications/ConfigurationFragment.java index a5b9db5..57c9f19 100644 --- a/android/app/src/main/java/com/viktorholk/apipushnotifications/ConfigurationFragment.java +++ b/android/app/src/main/java/com/viktorholk/apipushnotifications/ConfigurationFragment.java @@ -13,8 +13,6 @@ import java.util.regex.Pattern; public class ConfigurationFragment extends Fragment { - - private static final Pattern URL_PATTERN = Pattern.compile("https?:\\/\\/(.*)"); private SharedPreferences sharedPreferences; private SharedPreferences.Editor editor; @@ -48,9 +46,7 @@ private void applyConfiguration(String urlText) { if (urlText.length() == 0) return; - if (!URL_PATTERN.matcher(urlText).matches()) { - urlText = "http://" + urlText; - } + urlText = Utils.parseURL(urlText); String urlShared = sharedPreferences.getString("url", ""); if (!urlText.equals(urlShared)) { diff --git a/android/app/src/main/java/com/viktorholk/apipushnotifications/NotificationsService.java b/android/app/src/main/java/com/viktorholk/apipushnotifications/NotificationsService.java index 1a414f0..4314d0c 100644 --- a/android/app/src/main/java/com/viktorholk/apipushnotifications/NotificationsService.java +++ b/android/app/src/main/java/com/viktorholk/apipushnotifications/NotificationsService.java @@ -3,10 +3,12 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ServiceInfo; +import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.util.Log; @@ -41,6 +43,11 @@ public class NotificationsService extends Service { private OkHttpClient client; private Call currentCall; private boolean isStoppedByUser = false; + + private static final int MAX_RETRIES = 5; + private static final int RETRY_TIME = 2000; + private int retryCount = 0; + private final Intent serviceFragmentBroadcast = new Intent("serviceFragmentBroadcast"); @Override @@ -64,6 +71,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { } private void listenForNotifications() { + broadcast("Connecting...", false); + String url = MainActivity.sharedPreferences.getString("url", ""); Request request = new Request.Builder() .addHeader("Accept", "text/event-stream") @@ -74,18 +83,19 @@ private void listenForNotifications() { currentCall.enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { - handleFailure(e); + handleFailure(e, true); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + retryCount = 0; if (!response.isSuccessful()) { - handleFailure(new IOException("Response failed with status code: " + response.code())); + handleFailure(new IOException("Response failed with status code: " + response.code()), false); return; } if (!"text/event-stream".equals(response.header("Content-Type"))) { - handleFailure(new IOException("Expected response content type to be an event stream")); + handleFailure(new IOException("Expected response content type to be an event stream"), false); return; } @@ -94,10 +104,27 @@ public void onResponse(@NonNull Call call, @NonNull Response response) throws IO }); } - private void handleFailure(IOException e) { - Log.w(LOG_TAG, "Failure: " + e); - broadcast(e.toString(), true); - stopSelf(); + + private void handleFailure(IOException e, boolean withRetry) { + if (isStoppedByUser) { + broadcast("Stopped", false); + return; + } + + // Try to reconnect + if (withRetry && (retryCount < MAX_RETRIES)) { + retryCount++; + broadcast(String.format("Retrying Connection (%s) \n%s", retryCount, e), false); + try { + Thread.sleep(RETRY_TIME); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + } + listenForNotifications(); + } else { + broadcast(e.toString(), true); + stopSelf(); + } } private void handleSuccess(@NonNull Response response) throws IOException { @@ -117,11 +144,10 @@ private void handleSuccess(@NonNull Response response) throws IOException { } } } catch (Exception e) { - if (isStoppedByUser) + if (isStoppedByUser) { broadcast("Stopped", false); - else - handleFailure(new IOException("Lost connection")); - + } else + handleFailure(new IOException("Lost connection"), true); break; } } @@ -155,7 +181,6 @@ private void createNotificationChannels() { private void startForegroundService() { Notification notification = new NotificationCompat.Builder(this, FOREGROUND_CHANNEL_ID) - .setOngoing(true) .setCategory(Notification.CATEGORY_SERVICE) .build(); @@ -176,6 +201,23 @@ private void showNotification(PushNotification notification) { .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true); + String notificationUrl = notification.getUrl(); + if (notificationUrl != null && !notificationUrl.isEmpty()) { + + notificationUrl = Utils.parseURL(notificationUrl); + + // Create the intent and pending intent + Intent notificationIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(notificationUrl)); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, + 0, + notificationIntent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE + ); + + builder.setContentIntent(pendingIntent); + } + NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); if (notificationManager != null) { Log.i(LOG_TAG, "Notifying: " + notification.getTitle()); @@ -187,7 +229,7 @@ private void broadcast(String message, boolean isError) { if (!Objects.isNull(message)) { serviceFragmentBroadcast.putExtra("message", message); serviceFragmentBroadcast.putExtra("isError", isError); - Log.i("Notification Service Broadcast", message); + Log.i(LOG_TAG, message); } sendBroadcast(serviceFragmentBroadcast); } diff --git a/android/app/src/main/java/com/viktorholk/apipushnotifications/PushNotification.java b/android/app/src/main/java/com/viktorholk/apipushnotifications/PushNotification.java index 2fe7818..3cc3b09 100644 --- a/android/app/src/main/java/com/viktorholk/apipushnotifications/PushNotification.java +++ b/android/app/src/main/java/com/viktorholk/apipushnotifications/PushNotification.java @@ -10,10 +10,12 @@ public class PushNotification { private String title; private String message; + private String url; - public PushNotification(String title, String message) { + public PushNotification(String title, String message, String url) { this.title = title; this.message = message; + this.url = url; } public String getTitle() { @@ -24,4 +26,8 @@ public String getMessage() { return message; } + public String getUrl() { + return url; + } + } diff --git a/android/app/src/main/java/com/viktorholk/apipushnotifications/ServiceFragment.java b/android/app/src/main/java/com/viktorholk/apipushnotifications/ServiceFragment.java index 7597d0d..37d9aa7 100644 --- a/android/app/src/main/java/com/viktorholk/apipushnotifications/ServiceFragment.java +++ b/android/app/src/main/java/com/viktorholk/apipushnotifications/ServiceFragment.java @@ -75,7 +75,6 @@ private void toggleService() { } else { // Clear the previous error message serviceMessageTextView.setText(""); - Log.e("test", Boolean.toString(NotificationsService.running)); // Start the service getActivity().startService(new Intent(getActivity(), NotificationsService.class)); diff --git a/android/app/src/main/java/com/viktorholk/apipushnotifications/Utils.java b/android/app/src/main/java/com/viktorholk/apipushnotifications/Utils.java new file mode 100644 index 0000000..60ced24 --- /dev/null +++ b/android/app/src/main/java/com/viktorholk/apipushnotifications/Utils.java @@ -0,0 +1,18 @@ +package com.viktorholk.apipushnotifications; + +import java.util.regex.Pattern; + +public class Utils { + public static String parseURL(String value) { + final Pattern pattern = Pattern.compile("https?:\\/\\/(.*)"); + + // Add http to the URL if no protocol is defined + if (!pattern.matcher(value).matches()) { + value = String.format("http://%s", value); + } + + return value; + } + + +} diff --git a/android/build.gradle b/android/build.gradle index 3a9ee32..da74099 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.4' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/server/package.json b/server/package.json index dd14e67..d6b74fc 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "Push Notifications API Server", - "version": "1.2.0", + "version": "1.2.1", "description": "Server for handling push notifications", "scripts": { "start": "ts-node --files -r tsconfig-paths/register src/index.ts" diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts index c2abd02..9349da5 100644 --- a/server/src/routes/index.ts +++ b/server/src/routes/index.ts @@ -6,6 +6,7 @@ const router = Router(); interface PushNotification { title: string; message?: string; + url?: string } type ClientContext = { req: Request; res: Response }; @@ -23,13 +24,14 @@ function sendNotifications(notification: PushNotification): void { // Create new notification router.post("/", (req: Request, res: Response) => { - const { title, message } = req.body; + + const { title, message, url } = req.body; if (!title || title.trim() === "") { return res.status(400).send("'title' field is required"); } - const notification: PushNotification = { title, message }; + const notification: PushNotification = { title, message, url }; notifications.push(notification); sendNotifications(notification);