support for local notification service on Android
- add option in profile on Android to enable local notifications - use foreground service to keep the app alive when running in the background - implement enable and disbable notification function in status module When enabling notifications, a foreground service is started that displays a sticky notification to make the user aware that the app is running in the background. Notifications are updated whenever a new.message signal is handled on java side. Currently only one to one chats are generating notifications but that can be easily extended to other types of messages, including mentions and keywords. The ens name of the user as well as keywords to follow should then be passed to the native side when calling the enable function. Signed-off-by: yenda <eric@status.im>
This commit is contained in:
parent
a6c2518de7
commit
36ad6fb762
|
@ -17,3 +17,4 @@ SNOOPY=0
|
|||
RPC_NETWORKS_ONLY=1
|
||||
PARTITIONED_TOPIC=0
|
||||
MOBILE_UI_FOR_DESKTOP=1
|
||||
LOCAL_NOTIFICATIONs=0
|
|
@ -4,6 +4,7 @@
|
|||
package="im.status.ethereum">
|
||||
|
||||
<!-- non-dangerous permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.NFC"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
@ -68,6 +69,7 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||
<service android:name="im.status.ethereum.module.ForegroundService"></service>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
|
@ -110,6 +110,7 @@ var TopLevel = {
|
|||
"DeviceEventEmitter" : function () {},
|
||||
"Dimensions" : function () {},
|
||||
"dispatch" : function () {},
|
||||
"disableNotifications" : function () {},
|
||||
"displayNotification" : function () {},
|
||||
"dividedBy" : function () {},
|
||||
"DocumentDirectoryPath" : function () {},
|
||||
|
@ -117,6 +118,7 @@ var TopLevel = {
|
|||
"dy" : function () {},
|
||||
"ease" : function () {},
|
||||
"Easing" : function () {},
|
||||
"enableNotifications" : function () {},
|
||||
"enableVibration" : function () {},
|
||||
"encode" : function () {},
|
||||
"encodeURIComponent" : function () {},
|
||||
|
|
|
@ -24,6 +24,6 @@ android {
|
|||
dependencies {
|
||||
implementation 'com.facebook.react:react-native:+' // from node_modules
|
||||
compile 'com.github.status-im:function:0.0.1'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.0.0'
|
||||
implementation(group: 'status-im', name: 'status-go', version: getStatusGoSHA1(), ext: 'aar')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package im.status.ethereum.module;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.app.Service;
|
||||
import android.os.IBinder;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
public class ForegroundService extends Service {
|
||||
private static final String CHANNEL_ID = "status-service";
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
// NOTE: recent versions of Android require the service to display
|
||||
// a sticky notification to inform the user that the service is running
|
||||
Context context = getApplicationContext();
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager =
|
||||
context.getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID,
|
||||
"Status Service",
|
||||
NotificationManager.IMPORTANCE_HIGH));
|
||||
}
|
||||
String content = "Keep Status running to receive notifications";
|
||||
Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setContentTitle("Background notification service opened")
|
||||
.setContentText(content)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.build();
|
||||
// the id of the foreground notification MUST NOT be 0
|
||||
startForeground(1, notification);
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
package im.status.ethereum.module;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
||||
import androidx.core.app.Person;
|
||||
import androidx.core.app.Person.Builder;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import android.os.Build;
|
||||
import android.net.Uri;
|
||||
import android.media.AudioAttributes;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
class NewMessageSignalHandler {
|
||||
private static final String GROUP_STATUS_MESSAGES = "im.status.notifications.message";
|
||||
private static final String CHANNEL_NAME = "Status";
|
||||
private static final String CHANNEL_DESCRIPTION = "Get notifications on new messages and mentions";
|
||||
private static final String CHANNEL_ID = "status-chat-notifications";
|
||||
private static final String TAG = "StatusModule";
|
||||
private NotificationManager notificationManager;
|
||||
private HashMap<String, Person> persons;
|
||||
private HashMap<String, StatusChat> chats;
|
||||
private Context context;
|
||||
private Intent serviceIntent;
|
||||
private Boolean shouldRefreshNotifications;
|
||||
|
||||
public NewMessageSignalHandler(Context context) {
|
||||
// NOTE: when instanciated the NewMessageSignalHandler class starts a foreground service
|
||||
// to keep the app running in the background in order to receive notifications
|
||||
// call the stop() method in order to stop the service
|
||||
this.context = context;
|
||||
this.persons = new HashMap<String, Person>();
|
||||
this.chats = new HashMap<String, StatusChat>();
|
||||
this.notificationManager = context.getSystemService(NotificationManager.class);
|
||||
this.createNotificationChannel();
|
||||
this.shouldRefreshNotifications = false;
|
||||
Log.e(TAG, "Starting Foreground Service");
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
context.startService(serviceIntent);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
Log.e(TAG, "Stopping Foreground Service");
|
||||
Intent serviceIntent = new Intent(context, ForegroundService.class);
|
||||
context.stopService(serviceIntent);
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Uri soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() + "/" + R.raw.notification_sound);
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||
channel.setDescription(CHANNEL_DESCRIPTION);
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build();
|
||||
channel.setSound(soundUri, audioAttributes);
|
||||
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void notify(int notificationId, StatusChat chat) {
|
||||
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me");
|
||||
ArrayList<StatusMessage> messages = chat.getMessages();
|
||||
for (int i = 0; i < messages.size(); i++) {
|
||||
StatusMessage message = messages.get(i);
|
||||
messagingStyle.addMessage(message.getText(),
|
||||
message.getTimestamp(),
|
||||
message.getAuthor());
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setStyle(messagingStyle)
|
||||
.setGroup(GROUP_STATUS_MESSAGES);
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
builder.setVibrate(new long[0]);
|
||||
}
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
}
|
||||
|
||||
public void refreshNotifications() {
|
||||
NotificationCompat.InboxStyle summaryStyle = new NotificationCompat.InboxStyle();
|
||||
String summary = "";
|
||||
int notificationId = 2; // we start at 2 because the service is using 1 and can't use 0
|
||||
int messageCounter = 0;
|
||||
Iterator<StatusChat> chatIterator = chats.values().iterator();
|
||||
while(chatIterator.hasNext()) {
|
||||
StatusChat chat = (StatusChat)chatIterator.next();
|
||||
notify(notificationId, chat);
|
||||
notificationId++;
|
||||
messageCounter += chat.getMessages().size();
|
||||
summaryStyle.addLine(chat.getSummary());
|
||||
summary += chat.getSummary() + "\n";
|
||||
}
|
||||
// NOTE: this is necessary for old versions of Android, newer versions are
|
||||
// building this group themselves and I was not able to make any change to
|
||||
// what this group displays
|
||||
NotificationCompat.Builder groupBuilder =
|
||||
new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentText(summary)
|
||||
.setSmallIcon(R.drawable.ic_stat_notify_status)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentTitle("You got " + messageCounter + " messages in " + chats.size() + " chats")
|
||||
.setStyle(summaryStyle
|
||||
.setBigContentTitle("You got " + messageCounter + " messages in " + chats.size() + " chats"))
|
||||
.setGroup(GROUP_STATUS_MESSAGES)
|
||||
.setGroupSummary(true);
|
||||
notificationManager.notify(notificationId, groupBuilder.build());
|
||||
}
|
||||
|
||||
void handleNewMessageSignal(JSONObject newMessageSignal) {
|
||||
try {
|
||||
JSONArray chatsNewMessagesData = newMessageSignal.getJSONObject("event").getJSONArray("messages");
|
||||
for (int i = 0; i < chatsNewMessagesData.length(); i++) {
|
||||
try {
|
||||
upsertChat(chatsNewMessagesData.getJSONObject(i));
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON conversion failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
if(shouldRefreshNotifications) {
|
||||
refreshNotifications();
|
||||
shouldRefreshNotifications = false;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON conversion failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Person getPerson(String publicKey, String icon, String name) {
|
||||
// TODO: invalidate cache if icon and name are not the same as
|
||||
// the Person returned (in case the user set a different icon or username for instance)
|
||||
// not critical it's just for notifications at the moment
|
||||
// using a HashMap to cache Person because it's immutable
|
||||
Person person = persons.get(publicKey);
|
||||
if (person == null) {
|
||||
String base64Image = icon.split(",")[1];
|
||||
byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
|
||||
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
|
||||
person = new Person.Builder().setIcon(IconCompat.createWithBitmap(decodedByte)).setName(name).build();
|
||||
persons.put(publicKey, person);
|
||||
}
|
||||
return person;
|
||||
}
|
||||
|
||||
private void upsertChat(JSONObject chatNewMessagesData) {
|
||||
try {
|
||||
JSONObject chatData = chatNewMessagesData.getJSONObject("chat");
|
||||
// NOTE: this is an exemple of chatData
|
||||
// {"chatId":"contact-discovery-3622","filterId":"c0239d63f830e8b25f4bf7183c8d207f355a925b89514a17068cae4898e7f193",
|
||||
// "symKeyId":"","oneToOne":true,"identity":"046599511623d7385b926ce709ac57d518dac10d451a81f75cd32c7fb4b1c...",
|
||||
// "topic":"0xc446561b","discovery":false,"negotiated":false,"listen":true}
|
||||
boolean oneToOne = chatData.getBoolean("oneToOne");
|
||||
// NOTE: for now we only notify one to one chats
|
||||
// TODO: also notifiy on mentions, keywords and group chats
|
||||
// TODO: one would have to pass the ens name and keywords to notify on when instanciating the class as well
|
||||
// as have a method to add new ones after the handler is instanciated
|
||||
if (oneToOne) {
|
||||
JSONArray messagesData = chatNewMessagesData.getJSONArray("messages");
|
||||
|
||||
// there is no proper id for oneToOne chat in chatData so we peek into first message sig
|
||||
// TODO: won't work for sync becaus it could be our own message
|
||||
String id = messagesData.getJSONObject(0).getJSONObject("message").getString("sig");
|
||||
StatusChat chat = chats.get(id);
|
||||
|
||||
|
||||
// if the chat was not already there, we create one
|
||||
if (chat == null) {
|
||||
chat = new StatusChat(id, oneToOne);
|
||||
}
|
||||
|
||||
ArrayList<StatusMessage> messages = chat.getMessages();
|
||||
// parse the new messages
|
||||
for (int j = 0; j < messagesData.length(); j++) {
|
||||
StatusMessage message = createMessage(messagesData.getJSONObject(j));
|
||||
if (message != null) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!messages.isEmpty()) {
|
||||
chat.setMessages(messages);
|
||||
chats.put(id, chat);
|
||||
shouldRefreshNotifications = true;
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON conversion failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private StatusMessage createMessage(JSONObject messageData) {
|
||||
try {
|
||||
JSONObject metadata = messageData.getJSONObject("metadata");
|
||||
JSONObject authorMetadata = metadata.getJSONObject("author");
|
||||
JSONArray payload = new JSONArray(messageData.getString("payload"));
|
||||
// NOTE: this is an exemple of payload we are currently working with
|
||||
// it is in the transit format, which is basically JSON
|
||||
// refer to `transport.message.transit.cljs` on react side for details
|
||||
// ["~#c4",["7","text/plain","~:public-group-user-message",157201130275201,1572011302752,["^ ","~:chat-id","test","~:text","7"]]]
|
||||
if (payload.getString(0).equals("~#c4")) {
|
||||
Person author = getPerson(authorMetadata.getString("publicKey"), authorMetadata.getString("identicon"), authorMetadata.getString("alias"));
|
||||
JSONArray payloadContent = payload.getJSONArray(1);
|
||||
String text = payloadContent.getString(0);
|
||||
Double timestamp = payloadContent.getDouble(4);
|
||||
return new StatusMessage(author, timestamp.longValue(), text);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON conversion failed: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class StatusChat {
|
||||
private ArrayList<StatusMessage> messages;
|
||||
private String id;
|
||||
private String name;
|
||||
private Boolean oneToOne;
|
||||
|
||||
StatusChat(String id, Boolean oneToOne) {
|
||||
this.id = id;
|
||||
this.oneToOne = oneToOne;
|
||||
this.messages = new ArrayList<StatusMessage>();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
//TODO this should be improved as it would rename the chat
|
||||
// after our own user if we were posting from another device
|
||||
// in 1-1 chats it should be the name of the user whose
|
||||
// key is different than ours
|
||||
return getLastMessage().getAuthor().getName().toString();
|
||||
}
|
||||
|
||||
private StatusMessage getLastMessage() {
|
||||
return messages.get(messages.size()-1);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return getLastMessage().getTimestamp();
|
||||
}
|
||||
|
||||
public ArrayList<StatusMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(ArrayList<StatusMessage> messages) {
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return "<b>" + getLastMessage().getAuthor().getName() + "</b>: " + getLastMessage().getText();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StatusMessage {
|
||||
public Person getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
private Person author;
|
||||
private long timestamp;
|
||||
private String text;
|
||||
|
||||
StatusMessage(Person author, long timestamp, String text) {
|
||||
this.author = author;
|
||||
this.timestamp = timestamp;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,9 @@ import android.net.Uri;
|
|||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
@ -24,6 +26,8 @@ import com.facebook.react.bridge.LifecycleEventListener;
|
|||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
|
@ -32,6 +36,8 @@ import statusgo.Statusgo;
|
|||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
|
@ -51,16 +57,18 @@ import java.util.zip.ZipOutputStream;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import android.app.Service;
|
||||
|
||||
class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventListener, SignalHandler {
|
||||
|
||||
private static final String TAG = "StatusModule";
|
||||
private static final String logsZipFileName = "Status-debug-logs.zip";
|
||||
private static final String gethLogFileName = "geth.log";
|
||||
private static final String statusLogFileName = "Status.log";
|
||||
|
||||
private static StatusModule module;
|
||||
private ReactApplicationContext reactContext;
|
||||
private boolean rootedDevice;
|
||||
private NewMessageSignalHandler newMessageSignalHandler;
|
||||
|
||||
StatusModule(ReactApplicationContext reactContext, boolean rootedDevice) {
|
||||
super(reactContext);
|
||||
|
@ -82,12 +90,25 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
|
||||
@Override
|
||||
public void onHostPause() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHostDestroy() {
|
||||
Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
|
||||
getReactApplicationContext().stopService(intent);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void enableNotifications() {
|
||||
this.newMessageSignalHandler = new NewMessageSignalHandler(reactContext);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void disableNotifications() {
|
||||
if (newMessageSignalHandler != null) {
|
||||
newMessageSignalHandler.stop();
|
||||
newMessageSignalHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkAvailability() {
|
||||
|
@ -117,11 +138,24 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
return this.getReactApplicationContext().getNoBackupFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
public void handleSignal(String jsonEvent) {
|
||||
Log.d(TAG, "Signal event: " + jsonEvent);
|
||||
public void handleSignal(final String jsonEventString) {
|
||||
try {
|
||||
final JSONObject jsonEvent = new JSONObject(jsonEventString);
|
||||
String eventType = jsonEvent.getString("type");
|
||||
Log.d(TAG, "Signal event: " + jsonEventString);
|
||||
// NOTE: the newMessageSignalHandler is only instanciated if the user
|
||||
// enabled notifications in the app
|
||||
if (newMessageSignalHandler != null) {
|
||||
if (eventType.equals("messages.new")) {
|
||||
newMessageSignalHandler.handleNewMessageSignal(jsonEvent);
|
||||
}
|
||||
}
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putString("jsonEvent", jsonEvent);
|
||||
params.putString("jsonEvent", jsonEventString);
|
||||
this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params);
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "JSON conversion failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private File getLogsFile() {
|
||||
|
@ -278,29 +312,27 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
}
|
||||
|
||||
@ReactMethod
|
||||
public void saveAccountAndLogin(final String accountData, final String password , final String config, final String subAccountsData) {
|
||||
public void saveAccountAndLogin(final String accountData, final String password, final String config, final String subAccountsData) {
|
||||
Log.d(TAG, "saveAccountAndLogin");
|
||||
String finalConfig = prepareDirAndUpdateConfig(config);
|
||||
String result = Statusgo.saveAccountAndLogin(accountData, password, finalConfig, subAccountsData);
|
||||
if (result.startsWith("{\"error\":\"\"")) {
|
||||
Log.d(TAG, "StartNode result: " + result);
|
||||
Log.d(TAG, "Geth node started");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Log.e(TAG, "StartNode failed: " + result);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void saveAccountAndLoginWithKeycard(final String accountData, final String password , final String config, final String chatKey) {
|
||||
public void saveAccountAndLoginWithKeycard(final String accountData, final String password, final String config, final String chatKey) {
|
||||
Log.d(TAG, "saveAccountAndLoginWithKeycard");
|
||||
String finalConfig = prepareDirAndUpdateConfig(config);
|
||||
String result = Statusgo.saveAccountAndLoginWithKeycard(accountData, password, finalConfig, chatKey);
|
||||
if (result.startsWith("{\"error\":\"\"")) {
|
||||
Log.d(TAG, "StartNode result: " + result);
|
||||
Log.d(TAG, "Geth node started");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Log.e(TAG, "StartNode failed: " + result);
|
||||
}
|
||||
}
|
||||
|
@ -311,8 +343,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
String result = Statusgo.login(accountData, password);
|
||||
if (result.startsWith("{\"error\":\"\"")) {
|
||||
Log.d(TAG, "Login result: " + result);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Log.e(TAG, "Login failed: " + result);
|
||||
}
|
||||
}
|
||||
|
@ -320,11 +351,11 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
@ReactMethod
|
||||
public void logout() {
|
||||
Log.d(TAG, "logout");
|
||||
disableNotifications();
|
||||
String result = Statusgo.logout();
|
||||
if (result.startsWith("{\"error\":\"\"")) {
|
||||
Log.d(TAG, "Logout result: " + result);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Log.e(TAG, "Logout failed: " + result);
|
||||
}
|
||||
}
|
||||
|
@ -411,7 +442,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String result =Statusgo.openAccounts(rootDir);
|
||||
String result = Statusgo.openAccounts(rootDir);
|
||||
callback.invoke(result);
|
||||
}
|
||||
};
|
||||
|
@ -450,8 +481,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
String result = Statusgo.loginWithKeycard(accountData, password, chatKey);
|
||||
if (result.startsWith("{\"error\":\"\"")) {
|
||||
Log.d(TAG, "LoginWithKeycard result: " + result);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Log.e(TAG, "LoginWithKeycard failed: " + result);
|
||||
}
|
||||
}
|
||||
|
@ -543,8 +573,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(dbFile));
|
||||
outputStreamWriter.write(dbJson);
|
||||
outputStreamWriter.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "File write failed: " + e.toString());
|
||||
showErrorMessage(e.getLocalizedMessage());
|
||||
}
|
||||
|
@ -567,7 +596,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
dumpAdbLogsTo(new FileOutputStream(statusLogFile));
|
||||
|
||||
final Stack<String> errorList = new Stack<String>();
|
||||
final Boolean zipped = zip(new File[] {dbFile, gethLogFile, statusLogFile}, zipFile, errorList);
|
||||
final Boolean zipped = zip(new File[]{dbFile, gethLogFile, statusLogFile}, zipFile, errorList);
|
||||
if (zipped && zipFile.exists()) {
|
||||
zipFile.setReadable(true, false);
|
||||
callback.invoke(zipFile.getAbsolutePath());
|
||||
|
@ -579,8 +608,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
|
|||
showErrorMessage(e.getLocalizedMessage());
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
dbFile.delete();
|
||||
statusLogFile.delete();
|
||||
zipFile.deleteOnExit();
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 640 B |
Binary file not shown.
After Width: | Height: | Size: 429 B |
Binary file not shown.
After Width: | Height: | Size: 809 B |
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 648 B |
Binary file not shown.
|
@ -61,6 +61,7 @@ https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.3.
|
|||
https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.4.1/baseLibrary-3.4.1
|
||||
https://dl.google.com/dl/android/maven2/com/android/databinding/compilerCommon/3.0.1/compilerCommon-3.0.1
|
||||
https://dl.google.com/dl/android/maven2/com/android/support/appcompat-v7/28.0.0/appcompat-v7-28.0.0
|
||||
https://dl.google.com/dl/android/maven2/com/android/support/support-compat/28.0.0/support-compat-28.0.0
|
||||
https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.2.1/crash-26.2.1
|
||||
https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.3.1/crash-26.3.1
|
||||
https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.4.1/crash-26.4.1
|
||||
|
|
|
@ -965,6 +965,21 @@ in {
|
|||
sha256 = "0lhp66q8rxf8cxylr8g6qjqy6s26prgrnmq133cnwx2r0ciyba53";
|
||||
};
|
||||
};
|
||||
"https://dl.google.com/dl/android/maven2/com/android/support/support-compat/28.0.0/support-compat-28.0.0" =
|
||||
{
|
||||
host = repositories.google;
|
||||
path =
|
||||
"com/android/support/support-compat/28.0.0/support-compat-28.0.0";
|
||||
type = "aar";
|
||||
pom = {
|
||||
sha1 = "ededbbdbfc461c09f992371624bf7fa564748c36";
|
||||
sha256 = "06ln7psm2gm6nskdj48cgd2mrzs1mlk6m0px3jb0zz4249na0ybb";
|
||||
};
|
||||
jar = {
|
||||
sha1 = "d252b640ed832cf8addc35ef0a9f9186dc7738a5";
|
||||
sha256 = "12hi2xc9qshbdr2jw96664i3va9wj0pjjhv9r2hrwgzavc0knzp1";
|
||||
};
|
||||
};
|
||||
"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.2.1/crash-26.2.1" =
|
||||
{
|
||||
host = repositories.google;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
[status-im.i18n :as i18n]
|
||||
[status-im.multiaccounts.update.core :as multiaccounts.update]
|
||||
[status-im.native-module.core :as native-module]
|
||||
[status-im.notifications.core :as notifications]
|
||||
[status-im.utils.build :as build]
|
||||
[status-im.utils.config :as config]
|
||||
[status-im.utils.fx :as fx]
|
||||
|
@ -65,50 +66,71 @@
|
|||
{:dev-mode? dev-mode?}
|
||||
{}))
|
||||
|
||||
(fx/defn switch-notifications
|
||||
{:events [:multiaccounts.ui/notifications-switched]}
|
||||
[cofx notifications-enabled?]
|
||||
(fx/merge cofx
|
||||
{(if notifications-enabled?
|
||||
::notifications/enable
|
||||
::notifications/disable) nil}
|
||||
(multiaccounts.update/multiaccount-update
|
||||
{:notifications-enabled? notifications-enabled?}
|
||||
{})))
|
||||
|
||||
(fx/defn switch-chaos-mode
|
||||
[{:keys [db] :as cofx} chaos-mode?]
|
||||
(when (:multiaccount db)
|
||||
(fx/merge cofx
|
||||
{::chaos-mode-changed chaos-mode?}
|
||||
(multiaccounts.update/multiaccount-update {:chaos-mode? chaos-mode?}
|
||||
(multiaccounts.update/multiaccount-update
|
||||
{:chaos-mode? chaos-mode?}
|
||||
{}))))
|
||||
|
||||
(fx/defn enable-notifications [cofx desktop-notifications?]
|
||||
(multiaccounts.update/multiaccount-update cofx
|
||||
(multiaccounts.update/multiaccount-update
|
||||
cofx
|
||||
{:desktop-notifications? desktop-notifications?}
|
||||
{}))
|
||||
|
||||
(fx/defn toggle-datasync
|
||||
[{:keys [db] :as cofx} enabled?]
|
||||
(let [settings (get-in db [:multiaccount :settings])
|
||||
warning {:utils/show-popup {:title (i18n/label :t/datasync-warning-title)
|
||||
warning {:utils/show-popup
|
||||
{:title (i18n/label :t/datasync-warning-title)
|
||||
:content (i18n/label :t/datasync-warning-content)}}]
|
||||
|
||||
(fx/merge cofx
|
||||
(when enabled? warning)
|
||||
(multiaccounts.update/update-settings (assoc settings :datasync? enabled?)
|
||||
(multiaccounts.update/update-settings
|
||||
(assoc settings :datasync? enabled?)
|
||||
{}))))
|
||||
|
||||
(fx/defn toggle-v1-messages
|
||||
[{:keys [db] :as cofx} enabled?]
|
||||
(let [settings (get-in db [:multiaccount :settings])
|
||||
warning {:utils/show-popup {:title (i18n/label :t/v1-messages-warning-title)
|
||||
warning {:utils/show-popup
|
||||
{:title (i18n/label :t/v1-messages-warning-title)
|
||||
:content (i18n/label :t/v1-messages-warning-content)}}]
|
||||
|
||||
(fx/merge cofx
|
||||
(when enabled? warning)
|
||||
(multiaccounts.update/update-settings (assoc settings :v1-messages? enabled?)
|
||||
(multiaccounts.update/update-settings
|
||||
(assoc settings :v1-messages? enabled?)
|
||||
{}))))
|
||||
|
||||
(fx/defn toggle-disable-discovery-topic
|
||||
[{:keys [db] :as cofx} enabled?]
|
||||
(let [settings (get-in db [:multiaccount :settings])
|
||||
warning {:utils/show-popup {:title (i18n/label :t/disable-discovery-topic-warning-title)
|
||||
:content (i18n/label :t/disable-discovery-topic-warning-content)}}]
|
||||
warning {:utils/show-popup
|
||||
{:title
|
||||
(i18n/label :t/disable-discovery-topic-warning-title)
|
||||
:content
|
||||
(i18n/label :t/disable-discovery-topic-warning-content)}}]
|
||||
|
||||
(fx/merge cofx
|
||||
(when enabled? warning)
|
||||
(multiaccounts.update/update-settings (assoc settings :disable-discovery-topic? enabled?)
|
||||
(multiaccounts.update/update-settings
|
||||
(assoc settings :disable-discovery-topic? enabled?)
|
||||
{}))))
|
||||
|
||||
(fx/defn switch-web3-opt-in-mode
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
[status-im.i18n :as i18n]
|
||||
[status-im.native-module.core :as status]
|
||||
[status-im.node.core :as node]
|
||||
[status-im.notifications.core :as notifications]
|
||||
[status-im.protocol.core :as protocol]
|
||||
[status-im.stickers.core :as stickers]
|
||||
[status-im.ui.screens.mobile-network-settings.events :as mobile-network]
|
||||
|
@ -122,7 +123,8 @@
|
|||
(when (not= network-id fetched-network-id)
|
||||
;;TODO: this shouldn't happen but in case it does
|
||||
;;we probably want a better error message
|
||||
(utils/show-popup (i18n/label :t/ethereum-node-started-incorrectly-title)
|
||||
(utils/show-popup
|
||||
(i18n/label :t/ethereum-node-started-incorrectly-title)
|
||||
(i18n/label :t/ethereum-node-started-incorrectly-description
|
||||
{:network-id network-id
|
||||
:fetched-network-id fetched-network-id})
|
||||
|
@ -144,14 +146,18 @@
|
|||
(fx/defn get-config-callback
|
||||
{:events [::get-config-callback]}
|
||||
[{:keys [db] :as cofx} config]
|
||||
(let [[{:keys [address] :as multiaccount} current-network networks] (deserialize-config config)
|
||||
(let [[{:keys [address notifications-enabled?] :as multiaccount}
|
||||
current-network networks] (deserialize-config config)
|
||||
network-id (str (get-in networks [current-network :config :NetworkId]))]
|
||||
(fx/merge cofx
|
||||
{:db (assoc db
|
||||
(cond-> {:db (assoc db
|
||||
:networks/current-network current-network
|
||||
:networks/networks networks
|
||||
:multiaccount (convert-multiaccount-addresses
|
||||
multiaccount))}
|
||||
(and platform/android?
|
||||
notifications-enabled?)
|
||||
(assoc ::notifications/enable nil))
|
||||
;; NOTE: initializing mailserver depends on user mailserver
|
||||
;; preference which is why we wait for config callback
|
||||
(protocol/initialize-protocol {:default-mailserver true})
|
||||
|
@ -169,9 +175,15 @@
|
|||
[{:keys [db] :as cofx} address password save-password?]
|
||||
(let [auth-method (:auth-method db)
|
||||
new-auth-method (if save-password?
|
||||
(when-not (or (= "biometric" auth-method) (= "password" auth-method))
|
||||
(if (= auth-method "biometric-prepare") "biometric" "password"))
|
||||
(when (and auth-method (not= auth-method "none")) "none"))]
|
||||
(when-not (or (= "biometric" auth-method)
|
||||
(= "password" auth-method))
|
||||
(if (= auth-method "biometric-prepare")
|
||||
"biometric"
|
||||
"password"))
|
||||
(when (and auth-method
|
||||
(not= auth-method
|
||||
"none"))
|
||||
"none"))]
|
||||
(fx/merge cofx
|
||||
{:db (assoc db :chats/loading? true)
|
||||
::json-rpc/call
|
||||
|
|
|
@ -30,6 +30,12 @@
|
|||
config
|
||||
#(callback (types/json->clj %))))
|
||||
|
||||
(defn enable-notifications []
|
||||
(.enableNotifications (status)))
|
||||
|
||||
(defn disable-notifications []
|
||||
(.disableNotifications (status)))
|
||||
|
||||
(defn save-account-and-login
|
||||
"NOTE: beware, the password has to be sha3 hashed"
|
||||
[multiaccount-data hashed-password config accounts-data]
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
(ns status-im.notifications.core
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[status-im.native-module.core :as status]))
|
||||
|
||||
(re-frame/reg-fx
|
||||
::enable
|
||||
(fn [_]
|
||||
(status/enable-notifications)))
|
||||
|
||||
(re-frame/reg-fx
|
||||
::disable
|
||||
(fn [_]
|
||||
(status/disable-notifications)))
|
|
@ -6,6 +6,7 @@
|
|||
[status-im.utils.handlers :as handlers]
|
||||
[status-im.ethereum.json-rpc :as json-rpc]
|
||||
[status-im.ethereum.core :as ethereum]
|
||||
[status-im.native-module.core :as status]
|
||||
[status-im.transport.message.contact :as contact]
|
||||
[status-im.transport.message.protocol :as protocol]
|
||||
[status-im.transport.message.transit :as transit]
|
||||
|
@ -197,4 +198,3 @@
|
|||
:params [confirmations]
|
||||
:on-success #(log/debug "successfully confirmed messages")
|
||||
:on-failure #(log/error "failed to confirm messages" %)}))))
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
(defn- flat-list-content
|
||||
[preferred-name registrar tribute-to-talk
|
||||
active-contacts-count show-backup-seed?
|
||||
keycard-account?]
|
||||
keycard-account? notifications-enabled?]
|
||||
[(cond-> {:title (or (when registrar preferred-name)
|
||||
:t/ens-usernames)
|
||||
:subtitle (if registrar
|
||||
|
@ -151,15 +151,23 @@
|
|||
[(when show-backup-seed?
|
||||
[components.common/counter {:size 22} 1]) :chevron]
|
||||
:on-press #(re-frame/dispatch [:navigate-to :privacy-and-security])}
|
||||
;; TODO commented out for now because it will be enabled for android notifications
|
||||
#_{:icon :main-icons/notification
|
||||
(when (and platform/android?
|
||||
config/local-notifications?)
|
||||
{:icon :main-icons/notification
|
||||
:title :t/notifications
|
||||
:accessibility-label :notifications-button
|
||||
;; TODO commented out for now, uncomment when notifications-settings view
|
||||
;; is populated. Then remove :on-press below
|
||||
;; :on-press #(re-frame/dispatch [:navigate-to :notifications-settings])
|
||||
:on-press #(.openURL react/linking "app-settings://notification/status-im")
|
||||
:accessories [:chevron]}
|
||||
:on-press
|
||||
#(re-frame/dispatch
|
||||
[:multiaccounts.ui/notifications-switched (not notifications-enabled?)])
|
||||
:accessories
|
||||
[[react/switch
|
||||
{:track-color #js {:true colors/blue :false nil}
|
||||
:value notifications-enabled?
|
||||
:on-value-change
|
||||
#(re-frame/dispatch
|
||||
[:multiaccounts.ui/notifications-switched
|
||||
(not notifications-enabled?)])
|
||||
:disabled false}]]})
|
||||
{:icon :main-icons/mobile
|
||||
:title :t/sync-settings
|
||||
:accessibility-label :sync-settings-button
|
||||
|
@ -212,7 +220,8 @@
|
|||
seed-backed-up?
|
||||
mnemonic
|
||||
keycard-key-uid
|
||||
address]
|
||||
address
|
||||
notifications-enabled?]
|
||||
:as multiaccount} @(re-frame/subscribe [:multiaccount])
|
||||
active-contacts-count @(re-frame/subscribe [:contacts/active-count])
|
||||
tribute-to-talk @(re-frame/subscribe [:tribute-to-talk/profile])
|
||||
|
@ -225,7 +234,7 @@
|
|||
(flat-list-content
|
||||
preferred-name registrar tribute-to-talk
|
||||
active-contacts-count show-backup-seed?
|
||||
keycard-key-uid)
|
||||
keycard-key-uid notifications-enabled?)
|
||||
list-ref
|
||||
scroll-y]))
|
||||
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
(def max-message-delivery-attempts (js/parseInt (get-config :MAX_MESSAGE_DELIVERY_ATTEMPTS "6")))
|
||||
(def contract-nodes-enabled? (enabled? (get-config :CONTRACT_NODES "0")))
|
||||
(def mobile-ui-for-desktop? (enabled? (get-config :MOBILE_UI_FOR_DESKTOP "0")))
|
||||
;; NOTE: only disabled in releases
|
||||
(def local-notifications? (enabled? (get-config :LOCAL_NOTIFICATIONS "1")))
|
||||
|
||||
;; CONFIG VALUES
|
||||
(def log-level
|
||||
|
|
Loading…
Reference in New Issue