Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android: Graceful shutdown button in notification for persistent mode #2736

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions android/app/src/main/java/com/zeus/LndMobile.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
Expand All @@ -69,6 +70,7 @@
// TODO break this class up
class LndMobile extends ReactContextBaseJavaModule {
private final String TAG = "LndMobile";
public static Map<String, String> translationCache = new HashMap<>();
Messenger messenger;
private boolean lndMobileServiceBound = false;
private Messenger lndMobileServiceMessenger; // The service
Expand Down Expand Up @@ -245,6 +247,21 @@ public String getName() {
return "LndMobile";
}

@ReactMethod
public void updateTranslationCache(String locale, ReadableMap translations) {
translationCache.clear();
ReadableMapKeySetIterator iterator = translations.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
translationCache.put(key, translations.getString(key));
}

// Get service instance and rebuild notification
Intent intent = new Intent(getReactApplicationContext(), LndMobileService.class);
intent.setAction("app.zeusln.zeus.android.intent.action.UPDATE_NOTIFICATION");
getReactApplicationContext().startService(intent);
}

@ReactMethod
public void checkLndMobileServiceConnected(Promise p) {
if (lndMobileServiceBound) {
Expand Down
150 changes: 111 additions & 39 deletions android/app/src/main/java/com/zeus/LndMobileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.database.sqlite.SQLiteDatabase;
import android.os.IBinder;
import android.os.Build;
Expand All @@ -34,6 +35,14 @@
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import org.json.JSONException;
import org.json.JSONObject;

import com.reactnativecommunity.asyncstorage.AsyncLocalStorageUtil;
import com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier;

Expand Down Expand Up @@ -486,59 +495,122 @@ private boolean getPersistentServicesEnabled(Context context) {
return false;
}

private String getLocalizedString(String key) {
String translation = LndMobile.translationCache.get(key);
return translation != null ? translation : "MISSING STRING";
}

private String fetchLocalizedStringFromBundle(String locale, String key) throws IOException, JSONException {
String assetPath = "stored-locales/" + locale + ".json";
Log.d(TAG, "Attempting to load: " + assetPath);
AssetManager assetManager = getApplicationContext().getAssets();
String[] mainAssets = assetManager.list("");
for (String file : mainAssets) {
Log.d(TAG, "Root asset: " + file);
if (file.equals("assets")) {
String[] assetFiles = assetManager.list("assets");
for (String assetFile : assetFiles) {
Log.d(TAG, "Asset file: " + assetFile);
}
}
}
try (InputStream is = assetManager.open(assetPath);
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
StringBuilder result = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
result.append(line);
}
JSONObject json = new JSONObject(result.toString());

String[] keyParts = key.split("\\.");
JSONObject current = json;
for (int i = 0; i < keyParts.length - 1; i++) {
current = current.getJSONObject(keyParts[i]);
}
return current.getString(keyParts[keyParts.length - 1]);
}
}

@Override
public int onStartCommand(Intent intent, int flags, int startid) {
// Hyperlog.v(TAG, "onStartCommand()");
if (intent != null && intent.getAction() != null && intent.getAction().equals("app.zeusln.zeus.android.intent.action.STOP")) {
Log.i(TAG, "Received stopForeground Intent");
stopForeground(true);
stopSelf();
return START_NOT_STICKY;
} else {
boolean persistentServicesEnabled = getPersistentServicesEnabled(this);
// persistent services on, start service as foreground-svc
if (persistentServicesEnabled) {
Notification.Builder notificationBuilder = null;
Intent notificationIntent = new Intent (this, MainActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel chan = new NotificationChannel(BuildConfig.APPLICATION_ID, "ZEUS", NotificationManager.IMPORTANCE_NONE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
if (intent != null && intent.getAction() != null) {
if (intent.getAction().equals("app.zeusln.zeus.android.intent.action.STOP")) {
Log.i(TAG, "Received stopForeground Intent");
stopForeground(true);
stopSelf();
return START_NOT_STICKY;
} else if (intent.getAction().equals("app.zeusln.zeus.android.intent.action.GRACEFUL_STOP")) {
Handler handler = new Handler(msg -> {
if (msg.what == MSG_STOP_LND_RESULT) {
stopForeground(true);
stopSelf();
Process.killProcess(Process.myPid());
return true;
}
return false;
});
Messenger messenger = new Messenger(handler);
mClients.add(messenger);
stopLnd(messenger, -1);
return START_NOT_STICKY;
} else if (intent.getAction().equals("app.zeusln.zeus.android.intent.action.UPDATE_NOTIFICATION")) {
if (notificationManager == null) {
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
assert notificationManager != null;
notificationManager.createNotificationChannel(chan);

notificationBuilder = new Notification.Builder(this, BuildConfig.APPLICATION_ID);
} else {
notificationBuilder = new Notification.Builder(this);
}

notificationBuilder
.setContentTitle("ZEUS")
.setContentText("LND is running in the background")
.setSmallIcon(R.drawable.ic_stat_ic_notification_lnd)
.setContentIntent(pendingIntent)
.setOngoing(true);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
notificationBuilder.setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE);
}
notificationManager.notify(ONGOING_NOTIFICATION_ID, buildNotification());
return START_NOT_STICKY;
}
}
boolean persistentServicesEnabled = getPersistentServicesEnabled(this);
// persistent services on, start service as foreground-svc
if (persistentServicesEnabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel chan = new NotificationChannel(BuildConfig.APPLICATION_ID, "ZEUS", NotificationManager.IMPORTANCE_NONE);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
assert notificationManager != null;
notificationManager.createNotificationChannel(chan);
}

Notification notification = notificationBuilder.build();
Notification notification = buildNotification();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(ONGOING_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
startForeground(ONGOING_NOTIFICATION_ID, notification);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(ONGOING_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
startForeground(ONGOING_NOTIFICATION_ID, notification);
}
}

// else noop, instead of calling startService, start will be handled by binding
return startid;
}

private Notification buildNotification() {
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
Intent stopIntent = new Intent(this, LndMobileService.class);
stopIntent.setAction("app.zeusln.zeus.android.intent.action.GRACEFUL_STOP");
PendingIntent stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE);
Notification.Builder notificationBuilder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationBuilder = new Notification.Builder(this, BuildConfig.APPLICATION_ID);
} else {
notificationBuilder = new Notification.Builder(this);
}
notificationBuilder
.setContentText(getLocalizedString("androidNotification.lndRunningBackground"))
.setSmallIcon(R.drawable.ic_stat_ic_notification_lnd)
.setContentIntent(pendingIntent)
.setOngoing(true)
.addAction(0, getLocalizedString("androidNotification.shutdown"), stopPendingIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
notificationBuilder.setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE);
}
return notificationBuilder.build();
}

@Override
public IBinder onBind(Intent intent) {
return messenger.getBinder();
Expand Down
4 changes: 4 additions & 0 deletions lndmobile/LndMobile.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export interface ILndMobile {
unbindLndMobileService(): Promise<void>; // TODO(hsjoberg): function looks broken
sendPongToLndMobileservice(): Promise<{ data: string }>;
checkLndMobileServiceConnected(): Promise<boolean>;
updateTranslationCache(
locale: string,
translations: { [key: string]: string }
): void;
}

export interface ILndMobileTools {
Expand Down
4 changes: 3 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1311,5 +1311,7 @@
"views.OnChainAddresses.sortBy.balanceAscending": "Balance (ascending)",
"views.OnChainAddresses.sortBy.balanceDescending": "Balance (descending)",
"views.OnChainAddresses.createAddress": "Create address",
"views.OnChainAddresses.changeAddresses": "Change addresses"
"views.OnChainAddresses.changeAddresses": "Change addresses",
"androidNotification.lndRunningBackground": "LND is running in the background",
"androidNotification.shutdown": "Shutdown"
}
16 changes: 16 additions & 0 deletions utils/LocaleUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NativeModules } from 'react-native';

import stores from '../stores/Stores';
import * as EN from '../locales/en.json';
import * as CS from '../locales/cs.json';
Expand Down Expand Up @@ -64,6 +66,12 @@ const Hebrew: any = HE;
const Croatian: any = HR;
const Korean: any = KO;

// strings that are needed on the java layer
const JAVA_LAYER_STRINGS = [
'androidNotification.lndRunningBackground',
'androidNotification.shutdown'
];

export function localeString(localeString: string): any {
const { settings } = stores.settingsStore;
const { locale } = settings;
Expand Down Expand Up @@ -142,3 +150,11 @@ export const formatInlineNoun = (text: string): string => {
}
return text;
};

export const bridgeJavaStrings = async (locale: string) => {
const neededTranslations: { [key: string]: string } = {};
JAVA_LAYER_STRINGS.forEach((key) => {
neededTranslations[key] = localeString(key);
});
NativeModules.LndMobile.updateTranslationCache(locale, neededTranslations);
};
3 changes: 2 additions & 1 deletion views/Settings/Language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Screen from '../../components/Screen';

import SettingsStore, { LOCALE_KEYS } from '../../stores/SettingsStore';

import { localeString } from '../../utils/LocaleUtils';
import { localeString, bridgeJavaStrings } from '../../utils/LocaleUtils';
import { themeColor } from '../../utils/ThemeUtils';

interface LanguageProps {
Expand Down Expand Up @@ -116,6 +116,7 @@ export default class Language extends React.Component<
await updateSettings({
locale: item.key
}).then(() => {
bridgeJavaStrings(item.key);
navigation.goBack();
});
}}
Expand Down
5 changes: 4 additions & 1 deletion views/Wallet/Wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
startLnd,
expressGraphSync
} from '../../utils/LndMobileUtils';
import { localeString } from '../../utils/LocaleUtils';
import { localeString, bridgeJavaStrings } from '../../utils/LocaleUtils';
import { IS_BACKED_UP_KEY } from '../../utils/MigrationUtils';
import { protectedNavigation } from '../../utils/NavigationUtils';
import { isLightTheme, themeColor } from '../../utils/ThemeUtils';
Expand Down Expand Up @@ -250,6 +250,9 @@ export default class Wallet extends React.Component<WalletProps, WalletState> {

// This awaits on settings, so should await on Tor being bootstrapped before making requests
await SettingsStore.getSettings().then(async (settings: Settings) => {
const locale = settings.locale || 'en';
bridgeJavaStrings(locale);

SystemNavigationBar.setNavigationColor(
themeColor('background'),
isLightTheme() ? 'dark' : 'light'
Expand Down
Loading