Rename de.gotify to com.github.gotify
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package com.github.gotify;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class GotifyPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(new LogManager(reactContext));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
44
android/app/src/main/java/com/github/gotify/Log.java
Normal file
44
android/app/src/main/java/com/github/gotify/Log.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.github.gotify;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class Log {
|
||||
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.ENGLISH);
|
||||
private static final List<String> LOG = Collections.synchronizedList(new ArrayList<String>());
|
||||
private static final String TAG = "gotify";
|
||||
|
||||
public static void i(String message) {
|
||||
i(message, null);
|
||||
}
|
||||
|
||||
public static List<String> get() {
|
||||
return LOG;
|
||||
}
|
||||
|
||||
public static void i(String message, Throwable throwable) {
|
||||
log("INFO", message, throwable);
|
||||
android.util.Log.i(TAG, message, throwable);
|
||||
}
|
||||
|
||||
public static void e(String message) {
|
||||
e(message, null);
|
||||
}
|
||||
|
||||
public static void e(String message, Throwable throwable) {
|
||||
log("ERROR", message, throwable);
|
||||
android.util.Log.e("gotify", message, throwable);
|
||||
}
|
||||
|
||||
private static void log(String type, String message, Throwable exception) {
|
||||
if (exception == null) {
|
||||
LOG.add(String.format("%s: %s - %s", type, FORMAT.format(new Date()), message));
|
||||
} else {
|
||||
LOG.add(String.format("%s: %s - %s%s%s", type, FORMAT.format(new Date()), message, "\n", android.util.Log.getStackTraceString(exception)));
|
||||
}
|
||||
}
|
||||
}
|
||||
32
android/app/src/main/java/com/github/gotify/LogManager.java
Normal file
32
android/app/src/main/java/com/github/gotify/LogManager.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.github.gotify;
|
||||
|
||||
import com.facebook.react.bridge.Callback;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
public class LogManager extends ReactContextBaseJavaModule {
|
||||
LogManager(final ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "LogManager";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void clear(Callback callback) {
|
||||
Log.get().clear();
|
||||
callback.invoke();
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getLog(Callback callback) {
|
||||
StringBuilder log = new StringBuilder();
|
||||
for (String line : Log.get()) {
|
||||
log.append(line).append("\n");
|
||||
}
|
||||
callback.invoke(log.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.github.gotify;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
startService(new Intent(this, PushService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "gotify";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.github.gotify;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.facebook.react.ReactApplication;
|
||||
|
||||
import in.sriraman.sharedpreferences.RNSharedPreferencesReactPackage;
|
||||
import com.learnium.RNDeviceInfo.RNDeviceInfo;
|
||||
import com.oblador.vectoricons.VectorIconsPackage;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
return Arrays.<ReactPackage>asList(
|
||||
new MainReactPackage(),
|
||||
new RNSharedPreferencesReactPackage(),
|
||||
new RNDeviceInfo(),
|
||||
new GotifyPackage(),
|
||||
new VectorIconsPackage()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return "index";
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.github.gotify;
|
||||
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* Creates a Notification channel for android oreo.
|
||||
*/
|
||||
public class OreoNotificationSupport {
|
||||
public static final String CHANNEL_ID = "gotify";
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
public static void createChannel(NotificationManager notificationManager) {
|
||||
try {
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Gotify", NotificationManager.IMPORTANCE_DEFAULT);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
} catch (Exception e) {
|
||||
Log.e("Could not create channel", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
321
android/app/src/main/java/com/github/gotify/PushService.java
Normal file
321
android/app/src/main/java/com/github/gotify/PushService.java
Normal file
@@ -0,0 +1,321 @@
|
||||
package com.github.gotify;
|
||||
|
||||
import android.app.Notification;
|
||||
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.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.github.gotify.model.Message;
|
||||
import com.github.gotify.model.PagedMessages;
|
||||
import com.github.gotify.model.Paging;
|
||||
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
|
||||
public class PushService extends Service {
|
||||
private static final String TOKEN = "@global:token";
|
||||
private static final String URL = "@global:url";
|
||||
private static final List<String> UPDATE_ON_KEYS = Arrays.asList(TOKEN, URL);
|
||||
private static final int NO_MESSAGE = -1;
|
||||
|
||||
private final Object socketLock = new Object();
|
||||
private final OkHttpClient client = new OkHttpClient.Builder().readTimeout(0, TimeUnit.MILLISECONDS).pingInterval(1, TimeUnit.MINUTES).connectTimeout(10, TimeUnit.SECONDS).build();
|
||||
private final AtomicLong lastError = new AtomicLong(0);
|
||||
private final AtomicInteger lastReceivedMessage = new AtomicInteger(NO_MESSAGE);
|
||||
private Handler handler = null;
|
||||
private WebSocket socket = null;
|
||||
private Gson gson = null;
|
||||
|
||||
private SharedPreferences.OnSharedPreferenceChangeListener listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (!UPDATE_ON_KEYS.contains(key)) {
|
||||
return;
|
||||
}
|
||||
synchronized (socketLock) {
|
||||
if (socket != null) {
|
||||
Log.i("Closing WebSocket (preference change)");
|
||||
socket.close(1000, "client logout");
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
new Thread(pushService).start();
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable pushService = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
start(true);
|
||||
} catch (Exception e) {
|
||||
Log.e("Could not start service", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable pushServiceAfterError = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
start(false);
|
||||
}
|
||||
};
|
||||
|
||||
private final Runnable pushServiceAfterErrorInNewThread = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
new Thread(pushServiceAfterError).start();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
super.onStartCommand(intent, flags, startId);
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.i("Creating WebSocket-Service");
|
||||
|
||||
gson = new Gson();
|
||||
handler = new Handler();
|
||||
new Thread(pushService).start();
|
||||
appPreferences().registerOnSharedPreferenceChangeListener(listener);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
OreoNotificationSupport.createChannel((NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE));
|
||||
}
|
||||
}
|
||||
|
||||
private void foregroundNotification(String message) {
|
||||
Intent notificationIntent = new Intent(this, MainActivity.class);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(this, "GOTIFY_CHANNEL")
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle("Gotify")
|
||||
.setChannelId(OreoNotificationSupport.CHANNEL_ID)
|
||||
.setOngoing(true)
|
||||
.setPriority(Notification.PRIORITY_MIN)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setContentText(message)
|
||||
.setContentIntent(pendingIntent).build();
|
||||
|
||||
startForeground(1337, notification);
|
||||
}
|
||||
|
||||
private void ensureAllMessagesArePublished(boolean firstStart, String url, String token) {
|
||||
PagedMessages message = getMessages(url, token, 1, null);
|
||||
List<Message> messages = message.getMessages();
|
||||
|
||||
if (firstStart) {
|
||||
if (messages.isEmpty()) {
|
||||
lastReceivedMessage.set(NO_MESSAGE);
|
||||
Log.i("Last available message id: no stored messages on server");
|
||||
} else {
|
||||
lastReceivedMessage.set(messages.get(0).getId());
|
||||
Log.i("Last available message id: " + lastReceivedMessage.get());
|
||||
}
|
||||
} else {
|
||||
if (!messages.isEmpty() && message.getMessages().get(0).getId() > lastReceivedMessage.get()) {
|
||||
Log.i("Missed messages while being disconnected from the WebSocket, publishing them now.");
|
||||
if (lastReceivedMessage.get() == NO_MESSAGE) {
|
||||
notifyTill(url, token, 0);
|
||||
} else {
|
||||
notifyTill(url, token, lastReceivedMessage.get());
|
||||
}
|
||||
} else {
|
||||
Log.i("Missed no messages while being disconnected from the WebSocket.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyTill(String url, String token, int till) {
|
||||
Integer since = null;
|
||||
while (true) {
|
||||
PagedMessages messages = getMessages(url, token, 10, since);
|
||||
for (Message message : messages.getMessages()) {
|
||||
if (message.getId() > till) {
|
||||
notify(message);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
since = messages.getPaging().getSince();
|
||||
if (since <= 0) {
|
||||
// no messages left
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PagedMessages getMessages(String url, String token, int limit, @Nullable Integer since) {
|
||||
HttpUrl.Builder builder = HttpUrl.parse(url).newBuilder()
|
||||
.addPathSegment("message")
|
||||
.addQueryParameter("token", token)
|
||||
.addQueryParameter("limit", String.valueOf(limit));
|
||||
if (since != null) {
|
||||
builder.addQueryParameter("since", String.valueOf(since));
|
||||
}
|
||||
HttpUrl httpUrl = builder.build();
|
||||
final Request request = new Request.Builder().url(httpUrl).get().build();
|
||||
try {
|
||||
Response execute = client.newCall(request).execute();
|
||||
if (execute.isSuccessful()) {
|
||||
return gson.fromJson(execute.body().string(), PagedMessages.class);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("Could not request messages", e);
|
||||
}
|
||||
PagedMessages pagedMessages = new PagedMessages();
|
||||
pagedMessages.setMessages(new ArrayList<Message>());
|
||||
Paging paging = new Paging();
|
||||
paging.setSince(0);
|
||||
pagedMessages.setPaging(paging);
|
||||
return pagedMessages;
|
||||
}
|
||||
|
||||
private void start(boolean firstStart) {
|
||||
String url = appPreferences().getString(URL, null);
|
||||
String token = appPreferences().getString(TOKEN, null);
|
||||
|
||||
if (url == null || token == null) {
|
||||
Log.i("url or token not configured; login required");
|
||||
foregroundNotification("login required");
|
||||
return;
|
||||
}
|
||||
|
||||
ensureAllMessagesArePublished(firstStart, url, token);
|
||||
|
||||
HttpUrl httpUrl = HttpUrl.parse(url).newBuilder().addPathSegment("stream").addQueryParameter("token", token).build();
|
||||
|
||||
final Request request = new Request.Builder().url(httpUrl).get().build();
|
||||
|
||||
foregroundNotification("Initializing WebSocket");
|
||||
Log.i("Initializing WebSocket");
|
||||
|
||||
final WebSocket newSocket = client.newWebSocket(request, new WebSocketListener() {
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
Log.i("Initialized WebSocket");
|
||||
foregroundNotification("Listening to " + request.url().host());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
Message message = gson.fromJson(text, Message.class);
|
||||
PushService.this.notify(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
Log.e("WebSocket closed " + reason);
|
||||
foregroundNotification("WebSocket closed, re-login required");
|
||||
showNotification(-4, "WebSocket closed", "The WebSocket connection closed, this normally means the token(login) was invalidated. A re-login is required");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) {
|
||||
foregroundNotification("Error: " + t.getMessage());
|
||||
Log.e("WebSocket failure", t);
|
||||
if (response != null && response.code() >= 400 && response.code() <= 499) {
|
||||
showNotification(-2, "WebSocket Bad-Request", "Could not connect: " + response.message());
|
||||
appPreferences().edit().remove(TOKEN).apply();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean recentErrored = recentErrored();
|
||||
lastError.set(System.currentTimeMillis());
|
||||
|
||||
if (recentErrored) {
|
||||
Log.i("Waiting one minute to reconnect to the WebSocket (because WebSocket failed recently)");
|
||||
foregroundNotification("WebSocket connected failed, trying to reconnect in one minute.");
|
||||
handler.postDelayed(pushServiceAfterErrorInNewThread, TimeUnit.MINUTES.toMillis(1));
|
||||
} else {
|
||||
Log.i("Trying to reconnect to WebSocket");
|
||||
start(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
synchronized (socketLock) {
|
||||
socket = newSocket;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean recentErrored() {
|
||||
return System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(1) < lastError.get();
|
||||
}
|
||||
|
||||
private void showNotification(int id, String title, String message) {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
NotificationCompat.Builder b = new NotificationCompat.Builder(this, "GOTIFY_CHANNEL");
|
||||
|
||||
b.setAutoCancel(true)
|
||||
.setDefaults(Notification.DEFAULT_ALL)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setSmallIcon(android.R.mipmap.sym_def_app_icon)
|
||||
.setTicker("Gotify - " + title)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_SOUND)
|
||||
.setChannelId(OreoNotificationSupport.CHANNEL_ID)
|
||||
.setContentIntent(contentIntent);
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.notify(id, b.build());
|
||||
}
|
||||
|
||||
private SharedPreferences appPreferences() {
|
||||
// https://github.com/sriraman/react-native-shared-preferences/issues/12 for why wit_player_shared_preferences
|
||||
return this.getSharedPreferences("wit_player_shared_preferences", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private void notify(Message message) {
|
||||
if (lastReceivedMessage.get() < message.getId()) {
|
||||
lastReceivedMessage.set(message.getId());
|
||||
}
|
||||
|
||||
showNotification(message.getId(), message.getTitle(), message.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.i("Destroying WebSocket-Service");
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.github.gotify.model;
|
||||
|
||||
public class Message {
|
||||
private Integer id;
|
||||
private String title;
|
||||
private String message;
|
||||
private int priority;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(int priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.github.gotify.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PagedMessages {
|
||||
private Paging paging;
|
||||
private List<Message> messages;
|
||||
|
||||
public List<Message> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(List<Message> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public Paging getPaging() {
|
||||
return paging;
|
||||
}
|
||||
|
||||
public void setPaging(Paging paging) {
|
||||
this.paging = paging;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.github.gotify.model;
|
||||
|
||||
public class Paging {
|
||||
private int since;
|
||||
|
||||
public int getSince() {
|
||||
return since;
|
||||
}
|
||||
|
||||
public void setSince(int since) {
|
||||
this.since = since;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user