diff --git a/android/app/src/main/java/com/zeus/LndMobile.java b/android/app/src/main/java/com/zeus/LndMobile.java index b19c005ed..41238a8df 100644 --- a/android/app/src/main/java/com/zeus/LndMobile.java +++ b/android/app/src/main/java/com/zeus/LndMobile.java @@ -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; @@ -69,6 +70,7 @@ // TODO break this class up class LndMobile extends ReactContextBaseJavaModule { private final String TAG = "LndMobile"; + public static Map translationCache = new HashMap<>(); Messenger messenger; private boolean lndMobileServiceBound = false; private Messenger lndMobileServiceMessenger; // The service @@ -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) { diff --git a/android/app/src/main/java/com/zeus/LndMobileService.java b/android/app/src/main/java/com/zeus/LndMobileService.java index 307fedf09..536978267 100644 --- a/android/app/src/main/java/com/zeus/LndMobileService.java +++ b/android/app/src/main/java/com/zeus/LndMobileService.java @@ -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; @@ -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; @@ -486,52 +495,91 @@ 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); } } @@ -539,6 +587,30 @@ public int onStartCommand(Intent intent, int flags, int startid) { 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(); diff --git a/lndmobile/LndMobile.d.ts b/lndmobile/LndMobile.d.ts index baaef8f0a..c36627d39 100644 --- a/lndmobile/LndMobile.d.ts +++ b/lndmobile/LndMobile.d.ts @@ -47,6 +47,10 @@ export interface ILndMobile { unbindLndMobileService(): Promise; // TODO(hsjoberg): function looks broken sendPongToLndMobileservice(): Promise<{ data: string }>; checkLndMobileServiceConnected(): Promise; + updateTranslationCache( + locale: string, + translations: { [key: string]: string } + ): void; } export interface ILndMobileTools { diff --git a/locales/en.json b/locales/en.json index 4adc44b82..2daa7e5e4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" } \ No newline at end of file diff --git a/utils/LocaleUtils.ts b/utils/LocaleUtils.ts index 262bbe26c..ca480cc77 100644 --- a/utils/LocaleUtils.ts +++ b/utils/LocaleUtils.ts @@ -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'; @@ -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; @@ -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); +}; diff --git a/views/Settings/Language.tsx b/views/Settings/Language.tsx index 7fee8ff27..feb72209f 100644 --- a/views/Settings/Language.tsx +++ b/views/Settings/Language.tsx @@ -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 { @@ -116,6 +116,7 @@ export default class Language extends React.Component< await updateSettings({ locale: item.key }).then(() => { + bridgeJavaStrings(item.key); navigation.goBack(); }); }} diff --git a/views/Wallet/Wallet.tsx b/views/Wallet/Wallet.tsx index 901618020..9f7f3a653 100644 --- a/views/Wallet/Wallet.tsx +++ b/views/Wallet/Wallet.tsx @@ -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'; @@ -250,6 +250,9 @@ export default class Wallet extends React.Component { // 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'