From e26c5bc8061d0de94fc83439ed21e1dbf9ba6994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20Sj=C3=B6berg?= Date: Tue, 25 Jul 2023 17:16:40 +0200 Subject: [PATCH] New Tor implementation and persistent lnd/tor services Co-authored-by: djkazic --- .gitmodules | 3 - android/app/build.gradle | 16 +- android/app/proguard-rules.pro | 2 + android/app/src/main/AndroidManifest.xml | 1 + .../main/java/com/blixtwallet/BlixtTor.java | 165 ++++++++++ .../java/com/blixtwallet/BlixtTorPackage.java | 22 ++ .../java/com/blixtwallet/BlixtTorUtils.java | 16 + .../main/java/com/blixtwallet/LndMobile.java | 40 ++- .../blixtwallet/LndMobileScheduledSync.java | 15 - .../LndMobileScheduledSyncWorker.java | 287 ++++++++++-------- .../com/blixtwallet/LndMobileService.java | 66 +++- .../java/com/blixtwallet/LndMobileTools.java | 21 +- .../java/com/blixtwallet/MainActivity.java | 14 + .../java/com/blixtwallet/MainApplication.java | 2 +- .../installer/AndroidTorInstaller.java | 89 ++++++ .../drawable-hdpi/ic_stat_ic_notification.png | Bin 0 -> 10930 bytes .../drawable-mdpi/ic_stat_ic_notification.png | Bin 0 -> 8936 bytes .../ic_stat_ic_notification.png | Bin 0 -> 10017 bytes .../ic_stat_ic_notification.png | Bin 0 -> 11484 bytes .../ic_stat_ic_notification.png | Bin 0 -> 12071 bytes locales/en.json | 3 + src/migration/app-migration.ts | 14 +- src/state/NotificationManager.ts | 2 +- src/state/Settings.ts | 30 ++ src/state/index.ts | 11 +- src/storage/app.ts | 6 + src/windows/InitProcess/DEV_Commands.tsx | 4 + src/windows/Settings/Settings.tsx | 24 +- tor | 1 - 29 files changed, 681 insertions(+), 173 deletions(-) create mode 100644 android/app/src/main/java/com/blixtwallet/BlixtTor.java create mode 100644 android/app/src/main/java/com/blixtwallet/BlixtTorPackage.java create mode 100644 android/app/src/main/java/com/blixtwallet/BlixtTorUtils.java create mode 100644 android/app/src/main/java/com/msopentech/thali/android/installer/AndroidTorInstaller.java create mode 100644 android/app/src/main/res/drawable-hdpi/ic_stat_ic_notification.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_stat_ic_notification.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_stat_ic_notification.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_stat_ic_notification.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_stat_ic_notification.png delete mode 160000 tor diff --git a/.gitmodules b/.gitmodules index 1d764850a..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "tor"] - path = tor - url = https://github.com/thaliproject/Tor_Onion_Proxy_Library diff --git a/android/app/build.gradle b/android/app/build.gradle index 29243356c..189167980 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -154,15 +154,17 @@ dependencies { implementation "com.github.mmin18:realtimeblurview:1.2.1" - // Tor - // implementation "com.msopentech.thali:universal:0.0.3" - // implementation "org.torproject:tor-android-binary:0.4.2.7a" - // implementation "com.msopentech.thali.toronionproxy.android:android:0.0.3@aar" - // implementation "org.slf4j:slf4j-android:1.7.30" - // implementation "net.freehaven.tor.control:jtorctl:0.2" // controlling tor instance via its control port - implementation "com.jakewharton:process-phoenix:2.0.0" implementation 'org.brotli:dec:0.1.2' + + implementation "com.jakewharton:process-phoenix:2.0.0" + + implementation 'info.guardianproject:tor-android:0.4.7.8' + implementation 'info.guardianproject:jtorctl:0.4.5.7' + + implementation('dev.doubledot.doki:library:0.0.1@aar') { + transitive = true + } } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 3ff1cebad..6457feed7 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -32,3 +32,5 @@ -keep class lnrpc.** { *; } -keep class com.facebook.react.turbomodule.** { *; } + +-keep class org.torproject.jni.** { *; } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 45ab123e9..eeca26e73 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ + diff --git a/android/app/src/main/java/com/blixtwallet/BlixtTor.java b/android/app/src/main/java/com/blixtwallet/BlixtTor.java new file mode 100644 index 000000000..d8a644501 --- /dev/null +++ b/android/app/src/main/java/com/blixtwallet/BlixtTor.java @@ -0,0 +1,165 @@ +package com.blixtwallet; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; +import android.widget.Toast; + +import java.util.Stack; + +import androidx.core.app.NotificationManagerCompat; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.reactnativecommunity.asyncstorage.AsyncLocalStorageUtil; +import com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier; + +import org.torproject.jni.TorService; + + +public class BlixtTor extends ReactContextBaseJavaModule { + static private final String TAG = "BlixtTor"; + static TorService torService; + static String currentTorStatus = TorService.STATUS_OFF; + static Stack calleeResolvers = new Stack<>(); + static NotificationManagerCompat notificationManager; + + static private boolean getPersistentServicesEnabled(Context context) { + ReactDatabaseSupplier dbSupplier = ReactDatabaseSupplier.getInstance(context); + SQLiteDatabase db = dbSupplier.get(); + String persistentServicesEnabled = AsyncLocalStorageUtil.getItemImpl(db, "persistentServicesEnabled"); + if (persistentServicesEnabled != null) { + return persistentServicesEnabled.equals("true"); + } + Log.w(TAG, "Could not find persistentServicesEnabled in asyncStorage"); + return false; + } + + static private final ServiceConnection torServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + // We've bound to LocalService, cast the IBinder and get LocalService instance + TorService.LocalBinder binder = (TorService.LocalBinder) service; + torService = binder.getService(); + Log.i(TAG, "torService.getService()"); + boolean persistentServicesEnabled = getPersistentServicesEnabled(torService); + if (persistentServicesEnabled) { + torService.startForeground(0xc0feefee, getNotification(torService)); + } + Log.i(TAG, "onServiceConnected"); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + Log.i(TAG, "onServiceDisconnected"); + } + + public Notification getNotification(Context context) { + Intent notificationIntent = new Intent (context, MainActivity.class); + PendingIntent pendingIntent = + PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel(BuildConfig.APPLICATION_ID, "blixt", NotificationManager.IMPORTANCE_NONE); + channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + notificationManager = NotificationManagerCompat.from(context); + notificationManager.createNotificationChannel(channel); + notificationManager.createNotificationChannel(channel); + } + return new Notification.Builder(context, BuildConfig.APPLICATION_ID) + .setContentTitle("Tor") + .setContentText("Tor is running in the background") + .setSmallIcon(R.drawable.ic_stat_ic_notification) + .setContentIntent(pendingIntent) + .setTicker("Blixt Wallet") + .setOngoing(true) + .build(); + } + }; + + private final BroadcastReceiver torBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { +// Toast.makeText(context, intent.getStringExtra(TorService.EXTRA_STATUS), Toast.LENGTH_SHORT).show(); + String status = intent.getStringExtra(TorService.EXTRA_STATUS); + if (intent != null && intent.getAction() != null && intent.getAction().equals("org.torproject.android.intent.action.STOP")) { + torService.stopSelf(); + } + currentTorStatus = status; + Log.i(TAG, "onReceive " + status); + if (status.equals(TorService.STATUS_ON)) { + while (calleeResolvers.size() > 0) { + calleeResolvers.pop().resolve(TorService.socksPort); + } + } else if (status.equals(TorService.STATUS_OFF)) { + getReactApplicationContext().unregisterReceiver(torBroadcastReceiver); + } + } + }; + + public BlixtTor(ReactApplicationContext reactContext) { + super(reactContext); + } + + public String getName() { + return "BlixtTor"; + } + + @ReactMethod + public void startTor(Promise promise) { + Log.i(TAG, "startTor()"); +// if (calleeResolver != null) { +// Log.i(TAG, "calleeResolver != null"); +// promise.reject(TAG, "Tor already in progress starting"); +// return; +// } + if (currentTorStatus.equals(TorService.STATUS_ON)) { + Log.i(TAG, "socksPort = " + TorService.socksPort + ", currentTorStatus.equals(TorService.STATUS_ON) " + currentTorStatus.equals(TorService.STATUS_ON)); + promise.resolve(TorService.socksPort); + return; + } + calleeResolvers.add(promise); + + boolean persistentServicesEnabled = getPersistentServicesEnabled(getReactApplicationContext()); + getReactApplicationContext().registerReceiver(torBroadcastReceiver, new IntentFilter(TorService.ACTION_STATUS)); + Intent intent = new Intent(getReactApplicationContext(), TorService.class); + + if (persistentServicesEnabled) { + getReactApplicationContext().startForegroundService(intent); + } + getReactApplicationContext().bindService(intent, torServiceConnection, Context.BIND_AUTO_CREATE); + } + + @ReactMethod + public void stopTor(Promise promise) { + if (notificationManager != null) { + notificationManager.cancelAll(); + } + Log.i(TAG,"Unbinding TorService"); + try { + getReactApplicationContext().unbindService(torServiceConnection); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Tried to unbindService on unbound service"); + } + promise.resolve(true); + }; + + @ReactMethod + public void showMsg(Promise promise) { + MainActivity.getActivity().showMsg(); + promise.resolve(true); + } +} diff --git a/android/app/src/main/java/com/blixtwallet/BlixtTorPackage.java b/android/app/src/main/java/com/blixtwallet/BlixtTorPackage.java new file mode 100644 index 000000000..3c766d7c2 --- /dev/null +++ b/android/app/src/main/java/com/blixtwallet/BlixtTorPackage.java @@ -0,0 +1,22 @@ +package com.blixtwallet; + +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 BlixtTorPackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new BlixtTor(reactContext)); + } +} diff --git a/android/app/src/main/java/com/blixtwallet/BlixtTorUtils.java b/android/app/src/main/java/com/blixtwallet/BlixtTorUtils.java new file mode 100644 index 000000000..177658ddc --- /dev/null +++ b/android/app/src/main/java/com/blixtwallet/BlixtTorUtils.java @@ -0,0 +1,16 @@ +package com.blixtwallet; + +import com.blixtwallet.BuildConfig; + +public class BlixtTorUtils { + public static int getListenPort() { + int listenPort = 9760; + if (BuildConfig.CHAIN.equals("testnet")) { + listenPort += 10; + } + if (BuildConfig.DEBUG) { + listenPort += 100; + } + return listenPort; + } +} diff --git a/android/app/src/main/java/com/blixtwallet/LndMobile.java b/android/app/src/main/java/com/blixtwallet/LndMobile.java index 4b069d94a..148e58012 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobile.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobile.java @@ -1,7 +1,5 @@ package com.blixtwallet; -// import com.blixtwallet.tor.BlixtTorUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -63,14 +61,13 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableType; import com.facebook.react.modules.core.DeviceEventManagerModule; -import com.facebook.react.modules.permissions.PermissionsModule; - import com.reactnativecommunity.asyncstorage.AsyncLocalStorageUtil; -import com.jakewharton.processphoenix.ProcessPhoenix; -import com.oblador.keychain.KeychainModule; +import com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier; import com.hypertrack.hyperlog.HyperLog; +import org.torproject.jni.TorService; + // TODO break this class up class LndMobile extends ReactContextBaseJavaModule { private final String TAG = "LndMobile"; @@ -242,6 +239,17 @@ public void onServiceDisconnected(ComponentName className) { private LndMobileServiceConnection lndMobileServiceConnection; + private boolean getPersistentServicesEnabled(Context context) { + ReactDatabaseSupplier dbSupplier = ReactDatabaseSupplier.getInstance(context); + SQLiteDatabase db = dbSupplier.get(); + String persistentServicesEnabled = AsyncLocalStorageUtil.getItemImpl(db, "persistentServicesEnabled"); + if (persistentServicesEnabled != null) { + return persistentServicesEnabled.equals("true"); + } + HyperLog.w(TAG, "Could not find persistentServicesEnabled in asyncStorage"); + return false; + } + public LndMobile(ReactApplicationContext reactContext) { super(reactContext); } @@ -284,9 +292,13 @@ public void initialize(Promise promise) { lndMobileServiceConnection = new LndMobileServiceConnection(req); messenger = new Messenger(new IncomingHandler()); // me - + Intent intent = new Intent(getReactApplicationContext(), LndMobileService.class); + if (getPersistentServicesEnabled(getReactApplicationContext())) { + getReactApplicationContext().startForegroundService(intent); + } + // else rely on bindService to start LND getReactApplicationContext().bindService( - new Intent(getReactApplicationContext(), LndMobileService.class), + intent, lndMobileServiceConnection, Context.BIND_AUTO_CREATE ); @@ -353,13 +365,11 @@ public void startLnd(boolean torEnabled, String args, Promise promise) { String params = "--lnddir=" + getReactApplicationContext().getFilesDir().getPath(); if (torEnabled) { - // int listenPort = BlixtTorUtils.getListenPort(); - // int socksPort = BlixtTorUtils.getSocksPort(); - // int controlPort = BlixtTorUtils.getControlPort(); - // params += " --tor.active --tor.socks=127.0.0.1:" + socksPort + " --tor.control=127.0.0.1:" + controlPort; - // params += " --tor.v3 --listen=localhost:" + listenPort; - } - else { + int listenPort = BlixtTorUtils.getListenPort(); + String controlSocket = "unix://" + getReactApplicationContext().getDir(TorService.class.getSimpleName(), Context.MODE_PRIVATE).getAbsolutePath() + "/data/ControlSocket"; + params += " --tor.active --tor.control=" + controlSocket; + params += " --tor.v3 --listen=localhost:" + listenPort; + } else { // If Tor isn't active, make sure we aren't // listening at all params += " --nolisten"; diff --git a/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSync.java b/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSync.java index f9989a0e9..75479ed75 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSync.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSync.java @@ -1,34 +1,19 @@ package com.blixtwallet; -import android.util.Log; -import android.content.ComponentName; -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; - import androidx.work.Constraints; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.PeriodicWorkRequest; import androidx.work.NetworkType; import androidx.work.WorkInfo; import androidx.work.WorkManager; -import androidx.lifecycle.Observer; -import androidx.annotation.Nullable; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.Promise; -import com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier; -import com.reactnativecommunity.asyncstorage.AsyncLocalStorageUtil; import com.google.common.util.concurrent.ListenableFuture; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import com.hypertrack.hyperlog.HyperLog; diff --git a/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSyncWorker.java b/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSyncWorker.java index f220063da..d2047a9ba 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSyncWorker.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobileScheduledSyncWorker.java @@ -1,8 +1,5 @@ package com.blixtwallet; -// import com.blixtwallet.tor.BlixtTor; -// import com.blixtwallet.tor.BlixtTorUtils; - import android.annotation.SuppressLint; import android.app.ActivityManager; import android.content.ComponentName; @@ -18,19 +15,16 @@ import android.os.Process; import android.os.RemoteException; import android.util.Log; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.concurrent.futures.ResolvableFuture; +import androidx.concurrent.futures.CallbackToFutureAdapter; +import com.google.common.util.concurrent.ListenableFuture; import androidx.work.ListenableWorker; import androidx.work.WorkerParameters; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.protobuf.ByteString; - -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; - import com.facebook.react.bridge.ReactApplicationContext; import com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier; import com.reactnativecommunity.asyncstorage.AsyncLocalStorageUtil; @@ -39,13 +33,14 @@ import com.facebook.react.bridge.ReadableMap; import com.oblador.keychain.KeychainModule; - +import com.google.protobuf.ByteString; import com.hypertrack.hyperlog.HyperLog; +import org.torproject.jni.TorService; + public class LndMobileScheduledSyncWorker extends ListenableWorker { private final String TAG = "LndScheduledSyncWorker"; private final String HANDLERTHREAD_NAME = "blixt_lndmobile_sync"; - private ResolvableFuture future = ResolvableFuture.create(); private Handler incomingHandler; private boolean lndMobileServiceBound = false; private Messenger messengerService; // The service @@ -53,13 +48,15 @@ public class LndMobileScheduledSyncWorker extends ListenableWorker { private ReactDatabaseSupplier dbSupplier; private boolean lndStarted = false; private boolean torEnabled = false; + private int torSocksPort = -1; private boolean torStarted = false; + private boolean persistentServicesEnabled = false; // Keeps track of how many times we've tried to get info // If this keeps going without `syncedToChain` flipping to `true` // we'll close down lnd and the worker private int numGetInfoCalls = 0; - // BlixtTor blixtTor; + BlixtTor blixtTor; // private enum WorkState { // NOT_STARTED, BOUND, WALLET_UNLOCKED, WAITING_FOR_SYNC, DONE; @@ -75,70 +72,99 @@ public class LndMobileScheduledSyncWorker extends ListenableWorker { public LndMobileScheduledSyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); dbSupplier = ReactDatabaseSupplier.getInstance(getApplicationContext()); - // blixtTor = new BlixtTor(new ReactApplicationContext(getApplicationContext())); + blixtTor = new BlixtTor(new ReactApplicationContext(getApplicationContext())); } @Override public ListenableFuture startWork() { - HyperLog.i(TAG, "------------------------------------"); - HyperLog.i(TAG, "Starting scheduled sync work"); - HyperLog.i(TAG, "I am " + getApplicationContext().getPackageName()); - writeLastScheduledSyncAttemptToDb(); - - HyperLog.i(TAG, "MainActivity.started = " + MainActivity.started); - if (MainActivity.started) { - HyperLog.i(TAG, "MainActivity is started, quitting job"); - future.set(Result.success()); - return future; - } - torEnabled = getTorEnabled(); + persistentServicesEnabled = getPersistentServicesEnabled(); + + return CallbackToFutureAdapter.getFuture(completer -> { + HyperLog.i(TAG, "------------------------------------"); + HyperLog.i(TAG, "Starting scheduled sync work"); + HyperLog.i(TAG, "I am " + getApplicationContext().getPackageName()); + writeLastScheduledSyncAttemptToDb(); + + HyperLog.i(TAG, "MainActivity.started = " + MainActivity.started); + if (persistentServicesEnabled || MainActivity.started) { + HyperLog.i(TAG, "MainActivity is started or persistentServicesEnabled = " + persistentServicesEnabled + ", quitting job"); + completer.set(Result.success()); + return null; + } - KeychainModule keychain = new KeychainModule(new ReactApplicationContext(getApplicationContext())); - - WritableMap keychainOptions = Arguments.createMap(); - WritableMap keychainOptionsAuthenticationPrompt = Arguments.createMap(); - keychainOptionsAuthenticationPrompt.putString("title", "Authenticate to retrieve secret"); - keychainOptionsAuthenticationPrompt.putString("cancel", "Cancel"); - keychainOptions.putMap("authenticationPrompt", keychainOptionsAuthenticationPrompt); + KeychainModule keychain = new KeychainModule(new ReactApplicationContext(getApplicationContext())); + + WritableMap keychainOptions = Arguments.createMap(); + WritableMap keychainOptionsAuthenticationPrompt = Arguments.createMap(); + keychainOptionsAuthenticationPrompt.putString("title", "Authenticate to retrieve secret"); + keychainOptionsAuthenticationPrompt.putString("cancel", "Cancel"); + keychainOptions.putMap("authenticationPrompt", keychainOptionsAuthenticationPrompt); + + keychain.getInternetCredentialsForServer("password", keychainOptions, new PromiseWrapper() { + @Override + public void onSuccess(@Nullable Object value) { + HyperLog.d(TAG, "onSuccess"); + + if (value == null) { + HyperLog.e(TAG, "Failed to get wallet password, got null from keychain provider"); + completer.set(Result.failure()); + return; + } else { + HyperLog.d(TAG, "Password data retrieved from keychain"); + final String password = ((ReadableMap) value).getString("password"); + HyperLog.d(TAG, "Password retrieved"); + + if (torEnabled) { + // boolean startTorResult = startTor(); + // if (!startTorResult) { + // Log.e(TAG, "Could not start Tor"); + // future.set(Result.failure()); + // return; + // } + blixtTor.startTor(new PromiseWrapper() { + @Override + void onSuccess(@Nullable Object value) { + HyperLog.i(TAG, "Tor started"); + HyperLog.i(TAG, "torSocksPort: " + (int) value); + torStarted = true; + torSocksPort = (int) value; + + startLndWorkThread(completer, password); + } - keychain.getInternetCredentialsForServer("password", keychainOptions, new PromiseWrapper() { - @Override - public void onSuccess(@Nullable Object value) { - HyperLog.d(TAG, "onSuccess"); + @Override + void onFail(Throwable throwable) { + HyperLog.e(TAG, "Failed to start Tor", throwable); + blixtTor.stopTor(new PromiseWrapper() { + @Override + void onSuccess(@Nullable Object value) { + } - if (value == null) { - HyperLog.e(TAG, "Failed to get wallet password, got null from keychain provider"); - future.set(Result.failure()); - return; - } - else { - HyperLog.d(TAG, "Password data retrieved from keychain"); - final String password = ((ReadableMap) value).getString("password"); - HyperLog.d(TAG, "Password retrieved"); - - if (torEnabled) { - boolean startTorResult = startTor(); - if (!startTorResult) { - future.set(Result.failure()); - return; + @Override + void onFail(Throwable throwable) { + } + }); + completer.set(Result.failure()); + } + }); + } else { + startLndWorkThread(completer, password); } } - startLndWorkThread(future, password); } - } - @Override - public void onFail(Throwable throwable) { - HyperLog.d(TAG, "Failed to get wallet password " + throwable.getMessage(), throwable); - future.set(Result.failure()); - } + @Override + public void onFail(Throwable throwable) { + HyperLog.d(TAG, "Failed to get wallet password " + throwable.getMessage(), throwable); + completer.set(Result.failure()); + } + }); + return null; }); - - return future; } - private void startLndWorkThread(ResolvableFuture future, String password) { + private void startLndWorkThread(CallbackToFutureAdapter.Completer completer, String password) { // Make sure we don't attempt to start lnd twice. // A better fix in the future would be to actually // use MSG_CHECKSTATUS in the future @@ -164,7 +190,7 @@ public void handleMessage(Message msg) { // Just exit if we reach this scenario HyperLog.w(TAG, "WARNING, Got MSG_REGISTER_CLIENT_ACK when lnd should already be started, quitting work."); unbindLndMobileService(); - future.set(Result.success()); + completer.set(Result.success()); return; } } catch (Throwable t) { @@ -198,7 +224,10 @@ public void handleMessage(Message msg) { HyperLog.i(TAG, "Got WalletState.RPC_ACTIVE"); HyperLog.i(TAG, "LndMobileService reports RPC server ready. Sending GetInfo request"); getInfoRequest(); - } else { + } else if (currentState == lnrpc.Stateservice.WalletState.SERVER_ACTIVE) { + HyperLog.i(TAG, "Got WalletState.SERVER_ACTIVE"); + HyperLog.i(TAG, "We do not care about that."); + } else { HyperLog.w(TAG, "SubscribeState got unknown state " + currentState); } } catch (Throwable t) { @@ -232,14 +261,14 @@ public void handleMessage(Message msg) { Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { - stopWorker(true); + stopWorker(true, completer); } }, 10000); } else { if (++numGetInfoCalls == 20) { HyperLog.e(TAG, "GetInfo was called " + numGetInfoCalls + " times and still no syncedToChain = true. shutting down worker."); - stopWorker(false); + stopWorker(false, completer); } else{ HyperLog.i(TAG, "Sleeping 10s then checking again"); Handler handler = new Handler(); @@ -250,7 +279,7 @@ public void run() { } catch (Throwable t) { HyperLog.e(TAG, "Job handler got an exception, shutting down worker.", t); - stopWorker(false); + stopWorker(false, completer); } } }, 10000); @@ -271,7 +300,7 @@ public void run() { } } catch (Throwable t) { HyperLog.e(TAG, "Job handler got an exception, shutting down worker.", t); - stopWorker(false); + stopWorker(false, completer); } } }; @@ -288,70 +317,76 @@ public void run() { thread.run(); } - private void stopWorker(boolean success) { + private void stopWorker(boolean success, CallbackToFutureAdapter.Completer completer) { HyperLog.i(TAG, "Job is done. Quitting"); unbindLndMobileService(); if (torStarted) { - if (!MainActivity.started) { +// if (!MainActivity.started) { HyperLog.i(TAG, "Stopping Tor"); - // blixtTor.stopTor(new PromiseWrapper() { - // @Override - // void onSuccess(@Nullable Object value) { - // HyperLog.i(TAG, "Tor stopped"); - // } - - // @Override - // void onFail(Throwable throwable) { - // HyperLog.e(TAG, "Fail while stopping Tor", throwable); - // } - // }); - future.set(success ? Result.success() : Result.failure()); - killLndProcess(); - return; - } else { - HyperLog.w(TAG, "MainActivity was started when shutting down sync work. I will not stop Tor"); - } + blixtTor.stopTor(new PromiseWrapper() { + @Override + void onSuccess(@Nullable Object value) { + HyperLog.i(TAG,"Tor stopped"); + } + + @Override + void onFail(Throwable throwable) { + HyperLog.e(TAG, "Fail while stopping Tor", throwable); + } + }); +// } else { +// HyperLog.w(TAG, "MainActivity was started when shutting down sync work. I will not stop Tor"); +// } } new Handler().postDelayed(new Runnable() { @Override public void run() { HyperLog.i(TAG, "Calling future.set(Result.success());"); - future.set(success ? Result.success() : Result.failure()); + completer.set(success ? Result.success() : Result.failure()); } }, 1500); } - private boolean startTor() { + private boolean startTor(CallbackToFutureAdapter.Completer completer) { HyperLog.i(TAG, "Starting Tor"); - // blixtTor.startTor(new PromiseWrapper() { - // @Override - // void onSuccess(@Nullable Object value) { - // HyperLog.i(TAG, "Tor started"); - // torStarted = true; - // } - - // @Override - // void onFail(Throwable throwable) { - // HyperLog.e(TAG, "Failed to start Tor", throwable); - // future.set(Result.failure()); - // } - // }); - // int torTries = 0; - // while (!torStarted) { - // if (torTries++ > 30) { - // HyperLog.e(TAG, "Couldn't start Tor."); - // future.set(Result.failure()); - // return false; - // } - // HyperLog.i(TAG, "Waiting for Tor to start"); - // try { - // Thread.sleep(1500); - // } catch (InterruptedException e) { - // e.printStackTrace(); - // } - // } + blixtTor.startTor(new PromiseWrapper() { + @Override + void onSuccess(@Nullable Object value) { + HyperLog.i(TAG, "Tor started"); + HyperLog.i(TAG, "torSocksPort: " + (int) value); + torStarted = true; + torSocksPort = (int) value; + } + + @Override + void onFail(Throwable throwable) { + HyperLog.e(TAG, "Failed to start Tor", throwable); + blixtTor.stopTor(new PromiseWrapper() { + @Override + void onSuccess(@Nullable Object value) {} + + @Override + void onFail(Throwable throwable) {} + }); + completer.set(Result.failure()); + } + }); + int torTries = 0; + while (!torStarted) { + if (torTries++ > 40) { + HyperLog.e(TAG, "Couldn't start Tor."); + completer.set(Result.failure()); + return false; + } + HyperLog.i(TAG, "Waiting for Tor to start"); + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } return true; } @@ -361,12 +396,10 @@ private void startLnd() throws RemoteException { Bundle bundle = new Bundle(); String params = "--lnddir=" + getApplicationContext().getFilesDir().getPath(); if (torEnabled) { - // HyperLog.d(TAG, "Adding Tor params for starting lnd"); - // int socksPort = BlixtTorUtils.getSocksPort(); - // int controlPort = BlixtTorUtils.getControlPort(); - // params += " --tor.active --tor.socks=127.0.0.1:" + socksPort + " --tor.control=127.0.0.1:" + controlPort; - // // params += " --tor.v3 --listen=localhost"; - // params += " --nolisten"; + String controlSocket = "unix://" + getApplicationContext().getDir(TorService.class.getSimpleName(), Context.MODE_PRIVATE).getAbsolutePath() + "/data/ControlSocket"; + HyperLog.d(TAG, "Adding Tor params for starting lnd, torSocksPort: " + torSocksPort + ", controlSocket: " + controlSocket); + params += " --tor.active --tor.socks=127.0.0.1:" + torSocksPort + " --tor.control=" + controlSocket; + params += " --nolisten"; } else { // If Tor isn't active, make sure we aren't @@ -435,6 +468,16 @@ private void unbindLndMobileService() { } } + private boolean getPersistentServicesEnabled() { + SQLiteDatabase db = dbSupplier.get(); + String persistentServicesEnabled = AsyncLocalStorageUtil.getItemImpl(db, "persistentServicesEnabled"); + if (persistentServicesEnabled != null) { + return persistentServicesEnabled.equals("true"); + } + HyperLog.w(TAG, "Could not find persistentServicesEnabled in asyncStorage"); + return false; + } + private boolean getTorEnabled() { SQLiteDatabase db = dbSupplier.get(); String torEnabled = AsyncLocalStorageUtil.getItemImpl(db, "torEnabled"); diff --git a/android/app/src/main/java/com/blixtwallet/LndMobileService.java b/android/app/src/main/java/com/blixtwallet/LndMobileService.java index 3bdf9c9f3..75f1e9e0f 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobileService.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobileService.java @@ -1,10 +1,16 @@ package com.blixtwallet; import android.app.ActivityManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.database.sqlite.SQLiteDatabase; import android.os.IBinder; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -14,7 +20,6 @@ import android.util.Base64; import android.util.Log; - import lndmobile.Callback; import lndmobile.Lndmobile; import lndmobile.RecvStream; @@ -27,6 +32,8 @@ import java.util.Map; import java.util.Set; import java.util.HashSet; +import com.reactnativecommunity.asyncstorage.AsyncLocalStorageUtil; +import com.reactnativecommunity.asyncstorage.ReactDatabaseSupplier; import com.google.protobuf.ByteString; @@ -34,6 +41,7 @@ public class LndMobileService extends Service { private static final String TAG = "LndMobileService"; + private final int ONGOING_NOTIFICATION_ID = 1; boolean lndStarted = false; boolean subscribeInvoicesStreamActive = false; Set streamsStarted = new HashSet(); @@ -70,6 +78,10 @@ public class LndMobileService extends Service { private Map streamMethods = new HashMap<>(); private Map writeStreams = new HashMap<>(); + + + private NotificationManager notificationManager; + private static boolean isReceiveStream(Method m) { return m.toString().contains("RecvStream"); } @@ -521,6 +533,55 @@ void sendToClients(Message msg) { } } + private boolean getPersistentServicesEnabled(Context context) { + ReactDatabaseSupplier dbSupplier = ReactDatabaseSupplier.getInstance(context); + SQLiteDatabase db = dbSupplier.get(); + String persistentServicesEnabled = AsyncLocalStorageUtil.getItemImpl(db, "persistentServicesEnabled"); + if (persistentServicesEnabled != null) { + return persistentServicesEnabled.equals("true"); + } + HyperLog.w(TAG, "Could not find persistentServicesEnabled in asyncStorage"); + return false; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startid) { + HyperLog.v(TAG, "onStartCommand()"); + if (intent != null && intent.getAction() != null && intent.getAction().equals("com.blixtwallet.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) { + 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, "blixt", NotificationManager.IMPORTANCE_NONE); + chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + assert notificationManager != null; + notificationManager.createNotificationChannel(chan); + } + Notification notification = new Notification.Builder(this, BuildConfig.APPLICATION_ID) + .setContentTitle("LND") + .setContentText("LND is running in the background") + .setSmallIcon(R.drawable.ic_stat_ic_notification) + .setContentIntent(pendingIntent) + .setTicker("Blixt Wallet") + .setOngoing(true) + .build(); + startForeground(ONGOING_NOTIFICATION_ID, notification); + } + } + + // else noop, instead of calling startService, start will be handled by binding + return startid; + } + @Override public IBinder onBind(Intent intent) { HyperLog.v(TAG, "onBind()"); @@ -596,6 +657,9 @@ private boolean killLndProcess() { } private void stopLnd(Messenger recipient, int request) { + if (notificationManager != null) { + notificationManager.cancelAll(); + } Lndmobile.stopDaemon( lnrpc.LightningOuterClass.StopRequest.newBuilder().build().toByteArray(), new Callback() { diff --git a/android/app/src/main/java/com/blixtwallet/LndMobileTools.java b/android/app/src/main/java/com/blixtwallet/LndMobileTools.java index 93e9c7c51..1d212333d 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobileTools.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobileTools.java @@ -1,7 +1,5 @@ package com.blixtwallet; -// import com.blixtwallet.tor.BlixtTorUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -70,6 +68,8 @@ import com.hypertrack.hyperlog.HyperLog; +import org.torproject.jni.TorService; + // TODO break this class up class LndMobileTools extends ReactContextBaseJavaModule { final String TAG = "LndMobileTools"; @@ -78,6 +78,17 @@ public LndMobileTools(ReactApplicationContext reactContext) { super(reactContext); } + private boolean getPersistentServicesEnabled(Context context) { + ReactDatabaseSupplier dbSupplier = ReactDatabaseSupplier.getInstance(context); + SQLiteDatabase db = dbSupplier.get(); + String persistentServicesEnabled = AsyncLocalStorageUtil.getItemImpl(db, "persistentServicesEnabled"); + if (persistentServicesEnabled != null) { + return persistentServicesEnabled.equals("true"); + } + HyperLog.w(TAG, "Could not find persistentServicesEnabled in asyncStorage"); + return false; + } + @Override public String getName() { return "LndMobileTools"; @@ -631,6 +642,12 @@ public void deleteTLSCerts(Promise promise) { @ReactMethod public void restartApp() { + Intent stopTorIntent = new Intent(getReactApplicationContext(), TorService.class); + stopTorIntent.setAction("org.torproject.android.intent.action.STOP"); + getReactApplicationContext().stopService(stopTorIntent); + Intent stopLndIntent = new Intent(getReactApplicationContext(), LndMobileService.class); + stopLndIntent.setAction("com.blixtwallet.android.intent.action.STOP"); + getReactApplicationContext().startService(stopLndIntent); ProcessPhoenix.triggerRebirth(getReactApplicationContext()); } diff --git a/android/app/src/main/java/com/blixtwallet/MainActivity.java b/android/app/src/main/java/com/blixtwallet/MainActivity.java index f5d89e072..d94b6153d 100644 --- a/android/app/src/main/java/com/blixtwallet/MainActivity.java +++ b/android/app/src/main/java/com/blixtwallet/MainActivity.java @@ -6,6 +6,7 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate; import android.os.Bundle; +import androidx.core.content.ContextCompat; /* * Blixt imports here: @@ -20,6 +21,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.ref.WeakReference; + +import dev.doubledot.doki.ui.DokiActivity; public class MainActivity extends ReactActivity { @@ -59,11 +63,13 @@ protected ReactActivityDelegate createReactActivityDelegate() { static byte[] tmpChanBackup; + public static WeakReference currentActivity; // react-native-screens override @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(null); + currentActivity = new WeakReference<>(MainActivity.this); started = true; } @@ -132,4 +138,12 @@ else if (requestCode == INTENT_EXPORTCHANBACKUPFILE && resultCode == Activity.RE } } } + + public void showMsg() { + startActivity(new Intent(MainActivity.this, DokiActivity.class)); + } + + public static MainActivity getActivity() { + return currentActivity.get(); + } } diff --git a/android/app/src/main/java/com/blixtwallet/MainApplication.java b/android/app/src/main/java/com/blixtwallet/MainApplication.java index 47c25e6ba..d3fc769b2 100644 --- a/android/app/src/main/java/com/blixtwallet/MainApplication.java +++ b/android/app/src/main/java/com/blixtwallet/MainApplication.java @@ -39,7 +39,7 @@ protected List getPackages() { packages.add(new LndMobileToolsPackage()); packages.add(new GossipFileScheduledSyncPackage()); packages.add(new LndMobileScheduledSyncPackage()); - // packages.add(new BlixtTorPackage()); + packages.add(new BlixtTorPackage()); packages.add(new RealTimeBlurPackage()); return packages; diff --git a/android/app/src/main/java/com/msopentech/thali/android/installer/AndroidTorInstaller.java b/android/app/src/main/java/com/msopentech/thali/android/installer/AndroidTorInstaller.java new file mode 100644 index 000000000..8a13f7a99 --- /dev/null +++ b/android/app/src/main/java/com/msopentech/thali/android/installer/AndroidTorInstaller.java @@ -0,0 +1,89 @@ +package com.msopentech.thali.android.installer;/* +Copyright (c) Microsoft Open Technologies, Inc. +All Rights Reserved +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, +INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. +See the Apache 2 License for the specific language governing permissions and limitations under the License. +*/ +//package com.msopentech.thali.android.installer; + + +import android.content.Context; +import android.util.Log; +//import com.msopentech.thali.toronionproxy.TorInstaller; +//import org.torproject.android.binary.TorResourceInstaller; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +/** + * Installs Tor for an Android app. This is a wrapper around the TorResourceInstaller. + * + * Since this class only deals with installing Tor, it is up to the developer to implement + * the openBridgesStream which will give the bridges for pluggable transports. A + * typical implementation looks like: + * + * + * public InputStream openBridgesStream() throws IOException { + * return context.getResources().openRawResource(R.raw.bridges); + * } + * + */ +public class AndroidTorInstaller { + +// private final TorResourceInstaller resourceInstaller; + + private static final String TAG = "AndroidTorInstaller"; + + protected final Context context; + + protected File torrcFile; + + /** + * The configDir will be the location of tor configuration files. It contains the files, geoip, geoip6, + * bridges.txt and the default torrc file. + * + * The location of tor executable will be in the Android native library directory for the app. + */ + public AndroidTorInstaller(Context context, File configDir) { +// this.resourceInstaller = new TorResourceInstaller(context, configDir); + this.context = context; + } + + public void updateTorConfigCustom(String content) throws IOException, TimeoutException { + if(torrcFile == null) { + throw new FileNotFoundException("Unable to find torrc file. Have you installed Tor resources?"); + } +// resourceInstaller.updateTorConfigCustom(torrcFile, content); + } + +// @Override + public void setup() throws IOException { +// try { +// File torFile = resourceInstaller.installResources(); +// if(torFile != null) { +// Log.d("AndroidTorInstaller", "tor executable = " + torFile.getAbsolutePath()); +// } else { +// Log.w(TAG, "Failed to setup tor. No tor executable installed"); +// throw new IOException("Failed to Failed to setup tor. No tor executable installed"); +// } + +// this.torrcFile = resourceInstaller.getTorrcFile(); +// if(torrcFile != null) { +// Log.d("AndroidTorInstaller", "torrc = " + torrcFile.getAbsolutePath()); +// } else { +// Log.w(TAG, "Failed to setup tor. No torrc file installed"); +// throw new IOException("Failed to Failed to setup tor. No torrc file installed"); +// } + +// } catch (TimeoutException e) { +// Log.w(TAG, "Failed to setup tor: " + e.getMessage()); +// throw new IOException(e); +// } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_stat_ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..f735490621dced88d1893a6f7d28b2c065dc3363 GIT binary patch literal 10930 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_stat_ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..7681d8777b2c1f03874913b49df6bff840dd36d1 GIT binary patch literal 8936 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 OV4!2F{aDi_{C@!Qm2TDm literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..3f341f60d4e35555a041cb08e2df317c43bc4843 GIT binary patch literal 10017 zcmeHrXIN8P({?BVQlv^RktPTslu!}~y-SI7P=o*hLJK7HBGQY1Qbl_2MX7?)L8OTk zQE67BNR!@qgXcWYbI$d>f1d05-hU^_-r0MtnR{mLnZ1%VF$g^kDsonG002OxrKxIw z{}#PC$Vl*KQ^Lb20D!jA-^k3<0O1nJ{~}*)iF@q^`O&M6TV^`c(;R7Ax&HH9%Jcf?VGrol8j*gacT3zz^nJ} z%?9OGvwtcr+6cWPpBwByd+B&Re#bBSU^f*P&=LEIvjh6lVaIZ3H(+WQ~L5^vbbW6C%9J`@V`<651t=x?_K7|!b#ZOPSk~gmUM;HB!WcOzN61go``!wAq zp}%kN0kf&jXjhukst5eZ@X;0NsmTewc%7Q-YtX*Ymhi>8Iq?3SYL5jD8A%cn!>!=M znbQ^0bz>o#&wDIFKdu&rjzChNa4aj!<$VsL0K$`Br>Bp%GG6Bi4mu_3SuZ+AqrXtl z6 z5NL9@X|STQy8Tsxtm~%jV@sczpojEViaocU8S25;b|gfKy^~)YPJ^1~y^lF^%>znz zqc*HwTlhMF`FMZSMSB&{{Uc*K|Y2!GTLJW8l>a zgR5X1i_AyNN-A;KfYd;*+_FKS^VxIqef{3WMWQFK(X61Sr!o^RQ{(2w#Gm5~2CWKu z>nJ2?rxJP&EfrsL?h|k?kvqFKspiQ*kFKku*2NyzzR%6{^Ep-#LUi>z+@9!hUv7o| zfKxOYQm6%6RHK;(2Bc}hu?ot`PL>}=wwK*ot377grRXgesle zw1eE&*iMKEN-+J7d4!M;#SfkIs-k$in-x7Lmd0)oHftr3+_E2>)s<-w`HrWCvc}LcCpJ12ifEdNZ^ceGZfZ>5X-FMUi1ocv`7qYH9Qlsb z_{pSbtec$W=34!6rS!es!=E9KP^s8jP&t}ksO%mYokiTtbiDH?DhSbw;^B!r0Zncr zSb=Bwl=AQxr5vYjFo7mc4D(^cskTI@x9pzlND%SqV~#sZJMRX4vaou2^j zWPY(8=fidp2mjkFeiMwGmv=wt73E{f@>KGFg{T(tT34ANTzDzcfC-A%U&AmFp+*!e zlx&kXFkDKJpS&zV!e$nf6H?#bkKX$-)*v&uB1LW7a%D5mu(S<9e>+;=TmmA2%Jj>~ z&W)Hom4F`m1IxHT>w*I046}QhJ1XRn<9H;Kp%w3By3;*~_>lsklax z46Hs0+CdOE_6fHRar)&?E^YpW&lH}f#n#)l)kRBsHNsGW5(b(}WFoJB&8MhK42gAK zk^>wq%h^|FCA7o5LS#Rp-zEt4z#qOT3EBTr^=>id6tFjg%YQz`scE9`)2)-iIbi&w zA}{Qb5Qs56S8+`$1yNQI@$^2=+vf=uOi3TYm4q(IczC&}+ttv~pO~bnhze>5U_Sej z?R6K41jPNs5b-Ij2j`|+pT@96QR{pde;YEoyQ`+EofJ${~^; z*+jW^m)$MwADx8PyA+Jd=f279Y|nYTg?Ircnkx!n-VlZFbVi1o#J}vK^Db_+`KHSX z+=#n(qki+2pAE}G^p*l3TY)U@S_x<@$EZ>#v5RcALcAf7R&3Bp)yO);?(1s|KSPy4 zZV8o79M#FM*S2+t_PCwQ(I4xSl>XdRmjf4tcwRWbZSGVc=-AUC16Ml~zuhKVab3up z8A#OSccmAbPSY5ra1uXhGD@Jc<%qmLpSW|Fza}rhWS{UzdX2WM2BsWBHvPl4ON*Hj z3bbb6|77dqpV(dT*_@YC(g1QiCkApRiak;h3iJQDTEl;7{uU3r<$W?aOk0Kqf1xH z)m&zdob@?@`d16P7|1}C?Unn?$5niWlc$dmW44eS3VctxD0x5|+zHs~=n>76G99VG1O3(jSU(YWV~2wapAhbDUr@)fv_ zAx@z8Wp3cZ#@BRyVr%wgpq)h4{GoDkD-mUtU!k%@;{ecSsF8=I#4BYj?hTVmzqUzY z14CU22{X)e(!#vvmqNzZbJhthGbmd_^9(PmS!17&d-ds%@aH*K#>6yik{~?Hg&wnW zycECGUZ<*RRyoH6S=Dap0_R>=B$?!>m_u|%G{z`P^a?GDB;TTaitdMEZ$7A^^5i!* zT#_D)bbFldPqQ3SQ;#(KK`_~r^jsQ&MN5NI1pG`udFfd(nhR(nWT6#Ubvco zFybuz(q>sf50^_mi7x?37)UdRhw6dU^K}}p_OW&5HDf93YK7LoXepteVNqo^tz-^o zk7IjJcj9?#$tOiDkW~IPLjYdx%klYyH?nwA<9!IjC}B>ozdau;T+j%c>(BmrUp>>t zgG^|MNGW$W?L`{N&8I!~kd_2ddhxJ8=%nry_6|G3NosJp_craWT@6!M#&tA8Tnzf9NVJi$0&*QZS2pxx>ySe$N_Y)`i=P#tmh=<;eV5Ak(R z{X4!9F#og)P?i zAOdK!LrGKZPQ+U~ZgN)hWo>j zky)CTzf&$J=&IALW<-Jj&3#E-X33lJpARy3gcbD%mnD6x3NC5d!7pcG6$hd+2wTk4 zO%)$ovkMEH8b*H5xTadDUK`@jhk7uRxQFpr?D(6obPuX;MQs+qai#1dh<-^N2xjtT9-1kCMrW3% zgir51`QE)SoS=ica~xR3#UJcQ0l1dlp6W``Nevyy9`ny?&dIjUUX$)Jn{0E!XXJKg8WLHJZYqm}+Yl3o~^G z>e1@c<8X>1Z!lCYxl${R*I=_pxt7I$s+)+75y0;9D zGws`QthWj(Ts>WBDmm_>e*H}8PM%yR#kn&3QWC7r(1o|m>(iR*4Ill7ePKd{(Jx2G z=|Vmz1}pD=>23@bf5_*zP;AMT(Obdh4!6HsK^0kR8Yf0j_<$<7OHm2YR5L=n!?&ju zV5D{^uu1h)346@xmJ_0b2~g!7^7a1K;QHiJU{2lp^&2vw_ak4Hl~nvi^gZCdAQgh*e-t3-G5;Z{i zQf`w*@*y)|Oc~DonU1-=w-KmJJ*rEWCmWngwm-Uh5X?fxK^B|!n0D$-qRTtGQ{(T2 zlm&Lr4v3i;^{U+wR4&uGy>sT*PtRMM$&WBRH0i}8@hf|*6@)Uq_Fo9) zrR`RoWaCQI2n+j66pKSnCBlzdCvrBq>SNBY_)<95L_bJ&&hODZgT)3nmQtI@u4z`d ziQWXB5vmSo7@!^!71KRQT%7j-_~(qSpY ztqa`C=HYw29(NhoStI4ybbWX;ZtI={i?S74Z?zYiGOI6G{y?oAcpFU@$v12ytp6;@ zR)1?G5}~GWo}JI;HQiopI>hDX^r9nxJLNM-IZoK?#(0Xe0_W~{C|4o$`1!5-W=pj2 z^v%Gd+?YNdFBTL#skl456I-nFaEZFhHXRX2<9gqcdioj7&4efq5)zl%-S))#r^?I= z(P)LEPbp_79VsXLb*s;Ih(O#U$_H@dTQ5I&LHbdK*KqS8kel4q5P66A= znLsffQ-j7+a@RYM0;RRWxa=t!IG;1@KZ) z$>JCV0_GOy=hkq?&85%|F2)LMsF+fs}wc}Qh zCztbu;Q?Gxo=BjttBV^B<_qWkjSItHUkrn}fxksOo#EVOx(J{O28#khL?9xfAT?jK zw>Y;vIZzI3XAd(_RsRzLe+TDw^z?Lxfx$jLJ|aF6A{eX#SPTkA0M)+v>ojCrHu^)Dk&->4no@6%77r! z;$k2fs00#(lo7R&kdPD;mA04s3zU`{&J*coi@JcqgNvZ?IFk0F_9%N%DG<^QDh7gx zNkKt2Qua`gsH7xR2Hys)H z1_^@NKy5%W($Y|IQ3+dHd!)^8C_7u2ItJ^C#HSPOigZAM-Q661Ph1cVQ$lFLxy42P zu=;xh;ezzE$1A|ObhfU5K(F9e}qg>SRB3* zFEGVKMgENDA}ug{FnD5-7j=pU_^pAD1*U>UAw4lzBMim`&VAtk@Iv!-KaAjz-YC1@hVXiSOxZdj-5gN(=ljos z`qw!6e@PY!C5e=hk`x0;iHYKqB`G0|&$kTzghHaEQ8JPe_7JJRqvJ64o<2w{O348q zDLxu}0sW2!DDay~!M{uUIHE3i5*3#K;qk?wMq=Wyiw{1fk}y$GZt$N0gD;-ye?=?@ z{(m@;`z`R7WdN`D#~6Nj!LL@}e=k>ma&|%E|KaP;T>Kwez(fCI%U^) zzY_kZy8fZ-zhdCO68@*U{=d;h{+|sG$_@W7$OpfjIfYZq;kQC0Ha9g?0X-M*SJ)&6 z{K#c@O>-OoKtX?T5CAf=nDK+8o?5zUq>BXfw3IXq9j`p`ib`6lN=AhUcb6R34sW_Fk`VV(iHuE@9$?@I-byfwo zwB$mdcQAg^hI=#)M!m5!m!~ABY+LP}^|wlNGZucHwvOh?ym=@lBZ1NC`nfnX)N|@# zxqm9h)G73`JBwiK8^S&*thlbir%6;4}6g285nJr>b8iBqTVg-8!f=t#!g_Kb6m|wej?9xaaTRI<~Pt zGi+Zk;y+PH)ceJK^KR|?FW8nl1p-OdqUrjj6Sxwh+0M@ULb;>Oi`$1j;5*Ns;`jP( zuN<{x6$MdmvSm#trl$57N4M8wVq$js9x#;>C=vjK_lcK6h)9Roq}}dtlXcfA=PTyO z`E7mM{rMgT$YTrAP6d2?=~s^)&4CAd!m^zKojIDkuZThvbr{r&U-na7BL-0Jug@LV z3lE(u_TFr{vz~nOu?UC&5O#MK{EnYL`n9)@kL+U+%Ok0wI!;xiZ@&(b+uPf70g6)( zj`?2P%$5(DmWRXJsAxOXdodId-B0kI2sh8Hskc?0*Rke}jEr=b=w_x_2&csZHu<@K z7?e-?P;<$jC^4v&XI1K3SXkWPl=IzWGB#FF=@sQxzp8d9&P*vqbpKhEmk=3Y@WNoK zG_MvcGWuM(mzO(%KY4(9e{E)w1mGsMv>l)}?CIt85zaX}!`@#_bieg$iShQ~OK!5z zUvKkOQjLv`>H0k~c(u}X_#2b2>pS%Y-7hYhI={{5zxv_XN?X8w&9jd#!9)ORfDseX z1EE;DVLS0p2)qpX`EbKqiTh>1nFWldwP1DWZ3!LnrDnmO&#O|4@5iscbyDX zR8)#Ty3|0JDTRw6Pi`QGRs~oVg#@Ptgivg&K+j?Y4BSmv0urbHuNKU zkj7xz^Xc;s)6?34@?4fLeZ?@17V&bUrU-4_6a!l^O`dB-cm(O8x)FRS5CU$GUDYD$ zMDQmsuWxL0G~v?~UV?7r+st&3l$4}>*ssEIg*1^j2FV;rqa0mPkzz1dFMNEI2D&|O z+2AFmCgLAi3kf;7*{9eAZA+{eGDk(~r(%k~m4bmbY9g39HJG%Gt zJbNGC`{y~1@BO!D=AO8&wa&HHxz@Vw`@W8e($-YL$Dzgn008)^j}&!~pO1bWSQyB6 z{ev160Dy$aSI@{@$J&e0#myN8cYrdw`?x?Eq26#90N}k)mtpT|%0&`(ClF1451kCX zXRXO#>~+MdCEfht2NC0Qan;2r*2gMX#pYMRw4={%1A00xINJ)er%ev(l-t#KrSK1H zFMd*wX+>>bUiznS|-d2=JI{fvgaXpZ!|aHltrYj$OSk#+c_Il%A6p5}t#x?g1Pww3VRYTWYT z;MGQ|W;*TVtCK~v>bo%}!b2<>*7J??NyI7x$NUNgSRt@hi@`GczIpD`%QdA{{mu29 z>x*y4O(v{u=E*XSZa0lLYwL19rbsW>118z7Kbp?8`$@japNo|~Xus?5YPc$Yr5~#^ zzx%Vk_`E>!ywT}9#qBJRuZ}wIZjk8A->aRb*h1F2pPX&=aWl)HN5RK*Cc{xg3k1}g ztof)fJJn!t>^abM7cP~?eG3#ZKJ?jAmYUN&OP=aC+wfVWH}qbq_%>?VD2V-B46Xf| zuKj0-UmLli&DAHauW8B7!c)hCms&h;ncY@}^>gt9?{2R*Jx27j175T#iPK0}DyXO3 zf#d>lvYWJ<0#09l3Zw}FeyK*p_IshsQsudf55nbI3YY; z3e!94RplV{Cxcn z*F9JBGIRJu{ZXAo!dJ>^+l_+JQg$dE3%BD zdX4N|^d7`q=FbC-@8Jh*0v$U+0aL8tEaJ>`CbzN@i-(h%xVG~@Ow{fUMsB-t-BDv& zjnpSnAcl-J!j)Ca?+sow?#3A@@hTh^U3~xfr8oRXO#v6d)cnEzs|9x)&YJEraCLE2 z6=hOs1)c5DVeHt%S}Q4H^c*hxr?|C^=bD3*m>&{lrHs%1IuyR+NRU z=~IRJ2bD1PSaF3NNThB{dc5T~sF@-knCD{pcc>S$DKX%7Q`5r41y~h$qmI|T^`VpiGq@XaTbaedi zvh9SfK7*Ip2JrByvZ3;d$D~eqj$2gP@Nw-MH?P#mb=s{^0x(nJlB|mG6u>-lcJxY4 zGUfBR5z25VV|h%)WK}*+;Modlo1E=qY^kF5;&AKmAA%;*j&ycJ8ET}H>h}0ml>-ke zM6`;IUdPL;u({(I65Dc7d%M3U9p@X?<79U14;ygJCR*{IXP20MrMeR9cjby2CexE} zN5h`GWYm+h`#wL>;Yroz#uIbdC!1oJ$);Yc?)cHN#gFWZrf2Yly9jID3aT&a0PCkK z>8`s$4%pN$!y5auSXNk)@FQ&@ykD$}z8B;RzK?&cwFva8vEVINvEQMh@fPmv?lz;Q z>y3CPk?d}O9(>II;^9Iu3zo(Sw!kY36FQy|&6Zs`Q#{U(U|+tpo&_``aw;JO%&1$A zjI24{70g7}0|MI|akkoKN?qX*ZDr$Z;7&Ow*nU@$FX4r6&!FteOR6kfRXhkzU=ixj~9{Gus7dC(FfaoChfCA48w&-ZomNx|7L*f8;w8X-_&; zX8Ksr#MEi943PC~j&c!?8_WBW15D| zFV|JyWb$Bsb?q*y_M)XRf17Hi!u15pt=CgLmegyq-&(#=yOnQ)ht3r`x*NyshxakE zp%@Y8!!O-Z$J7e2V|wpVhYIADc@X4)Tc1%dF&%b=q20yNagSr2Jm$(;n}+3kjs*&m;=xVrit*un{})ymHEd*dslKBq^*R(hOd zcptWS)c9pZI2%)zn#X4GxCX3qD8ilx8=HmVHN2;0=Rr*wNb^XKPU2;sEREzAH2vlt z=$0IH@qr4a5~5d}XyFO4f;zS0GYH56VC&5TJDywNy2rWuLOQXk8MU;F7g$3Gn!X~; z+j&P@T7EPzn*%3|Il?vONZ_N68R_aVP>jhuXx7eKn>;{yI@)~YaIUZcQ<{)+eEWM2 z7O|*s5vv2jW3Y$o5K@c4P{r)+Vv|if0g;PF7jl3d-*ZxGDG#ECfOC7tQC7`96EE0^ z?Dtz)j-Q+yM$GeA#^gdz?NCRk;)13anaabg_P^zCq|ro5^1PHc3vscM>*=IjVgq7< zWjGDix<GPU}Betj$D78pQq^!j}cK@ z^>lI)q>D5TIbx0-_O^1(GlvK^CZJ7n1JMEwLr(|2WWGKEpL@&YLrN+)sxA;|tzQK` zrDcmSh5`=c)~hc?+i-TC+qqeE3$OXO^p8vIx{a3!V0QWHEs+fuomFH13zh*#z z>M$F|sxQ}0Eag`bcwmDWuE0M|N0rSbnJ{m=R_0Uii46@?X(`hI*B2yT+&#Ma1tf&? zr6-0AMglo4mrVz8^Ps8WuC`(UH})(H^K`9OyDu*FOuE-bTKaMOeax(MIcq~A?I+1H z7`(~+N3BR3Nt%8fPb*g&5nmcmXx?)v>r-yYn~gY7;bt=Ye#!*&dVA z%9#U|ScxJACU!XS!3z`5@D*~e6(93uP?h04e#$!*B@D!opPc*1TDz@~MDRsXv~{jQ z&W}nka`w{eeprJs?x=sR5j%(YprAHOYGPS0A;)tSKv$P-kWu!S`I-yAqg)$t-#rzb z;Bungh8m0#OoiY|qP-w})!u|h_cC?2o0P`E;@e@ktx2G$!_3wJtkfc=kQ?~=Of)4P zcCMn1H)<+aPe|{MEa>}#tWj!<05HCP7k@;GG7q)`bK;F$QMOO&kA8PT%{NxJo3B{K9NyMi#TemNe9THS zL>YWD?rH}r=-NkZ|UGm9#n&{CJ>@# z(YkTM_M)FylEgC~Rq_3^^iNabuw)@!nQdi1DptbPx-CaM>ih})SsIod>~bnlO5gt`@5V#li@7-T2}F;2(YaPsKZz|kETAeYpuB~-0A ztXbt8pToJpb*6#%{i>LxBf*lSHjXQPPYG|)8Ht2kbJqD3JE4Ie)Rt1~^yR_R?i}X% znE>nAUW?u^FAJ1m8^n7MX;K)!Co31NjxuMPZhqQ4?~~#F^*W>bvL-Ca*pRwv8AdWR z^!gaHWveLsw}*#Y70FD(PcSo8yY~Rj0~q&2xk-J-BIaak0V^Q^Vz{jT02PMBWG)Qg(OSyWPbN@{E`lk#_pl?{%*Fo!81 z9^#&`)4H);OHm4=Udj$_`%p#c4?Q5$Y01TvmBhB3W+MCA1Dl8xon0>xqs_N%zKO7l zPMfnW4NS*Inb+CHz8?sueORt(F0#}GEV`Gvq4G(`;AF83^N#@oIoFGg3UVFPrJ3e}K zw!rd6``{Vf{v2MOr0|J*!k^oS=>~GP;URv{nB)U*B4=bs)L!F%d7Jb`h0+q*No?Sj z!Z|Qc>d7%rc$NKx8iN2=-u)eCIo3kKEF?uQ{29GJu%{QbCWHegZqS!d^B%cNaIn#R z^Ct@>m8MrH26D`JrlvkJ`3y8UpEPBHLA^J_(X^Wwaf~9ng;WOpko&40U4)nS9LB|ZkD9s)zJ&~NcyDavcD8Gp zN7db9nzG*EmH{#{QO9?z#Mg*22I+|}IW>+cmlWah#XZ-l4Oiz?j=tVi$s%F$~Z{@=)OXiLKcCm^_`g!d+bl zD$xL-y!7$BQMlZ&eSXNLc*xg=`3QzLqGjfvf@T<&^^61_C54@zVSEflh+{BFEHn5V zCLiR+H0xFgIZIQ)5*$x2wghJgQJh@eF5s%&N8~prvWiQjgy{Smm`BHGnEs45qPh>i zzMSBucm3MaSM%UOvQ_g(62qQ#BBd&Zs-97AV~;sB^70Vd&Rn@6y)eeXpoMM5E^6%x zl(2PPC^uzRSoUdZ+fC1lVFKel(v#!~TK79z!sqy#T`ffRvug7rjTc+baF0Z2vNw5f z^cDydNH&_aP4He{$hCQiw8eEW0Z_5{Fi%O$A0j(1sV*`+vl*w)&s5;ae_X_P!NEO&wzx_3SoB;#;zuzfr5`z-jQT+ z%5pix+XiKK%a5NrJURw@iZ?BVPc0CM zN7o39rPStNv(R!S6$!6lNjPHnn4k+!t@28(nkt3TiEMl|?j_7v+HTrpZ2XzWd+2$k z`t2C}q6jaP`{MF4k;PZU{B9UMr>~}dq%NcX-kY7X94mwlz6LGD>5qhfr(eH#|fK$d$DZ1j7m;Oe=z{C-rqWHosrr90(w>dx-8xQL!0Duw- zS5VMaRZ#fXW)X6WC@UaI>QRq2S(uitQc|?ML?k<;Sd&6giM9?$shH?#m;s%-#ZOEP z>SQuXm9DzapT}%wx_XTbF%9L2u2Eb)Ty@-&NRA%k0Bu^2lM_x3#ag`>7^jESYsC9d zG#WMjuF6BDJ-Zu%VwKCoI`|<)hEmrElftc#|G8 z8DhuM;ugz3-~ZXQ`uW_U;(8>{pgDu0#U>lY(U% z_%1G3u@vln%UdhU2Z*bNBC!qzic+@_&?Ai$dt5bwS_xLF(0*bFf+m-Qp|=z8hjcq3Q#-vBVRYDuCJyZ*w-E` z24R+w#*y?EM*=uP-K`nD9UYty;@(oszj4Kp-+vABGBf@bakrOZHqy{$RB(2KG79nt z^6&wbyy2ew%+ffFl5P-~xQ?RopAg7jQp|Sl?k?iIyk1^jJYE7k&Th86ATcp9UOs+a zetsZQ0*LT&a<}#dIw2nXg7^bN5sCo2!Cluv4A3*zD9b#&zYs|Lbd$rB0kr$hg%20{Hu{_iXxC!L4Px;qN<_&Z;f9R*uouMerqAg{+pyb9QGfw z{^r}Sncw03)e)rnzj6O2{jb=63nQg8G{hC1!5+WdQ&p5={$*br;tYmE#D9Mk5#!^x zu>r$?A`m`dpdd(87$|B35d!iF3-XKD@Cgg^iwXP%O4SMBZtVny{(?e+^T3ffAR$qR z7!=A66ov7N0R=@wgn>3NQ9dA8Scu;S3WAD2Y()P8q2&fgR;9JWU%mPT1wldyL9B%Y zLBdd=n5c~qP|yY{02Jke*#KcSd=M}{1STwM4f_oR0gEd;yE$4T(+PL9wuSP#INAQ5 z_(iz5oVKbIGd~aCe@3(&tleQq1u15AxRZzXetAewghd2Ff}#R^q5{Gq zLLjmK2pK@#5XegWg$d&05%>f5S6al8!61pX{#B<)fZrO(Si}|Fpw{lrZhFqn4pPj& zTw?sC`MbOsCI3juBRB#n;q$BF|EzgksOulU{%8RQ`0pu3#@}TtZVmp!2*TPE3i)ja zsrSbe*v{I?7K-feKMU$#I}&AQ|R|0EI+hf?{HP0(>x- z_21DE&MzW|UA1O$ocf%wHiAaOxa zAfJ#pA0IRCp8@m!>gs<*EXn)-a3c9z;4jMnQtyv3vT>Z(}FB<<3AAjcJ z|Ih*w`X3|z6~F(`^$%VD6$Ag3@ITe{4_*Hi1OJuqKh^dBjV_%3-0(o1kXJ!o$lDp2 z6H*W4tq_LIV--a}&#z~nTe2;31l#42F#-UBVm8Ub63?aq(d+hm`*%e7m`d|kU1zMMS=fq1$y z>}Z@PLSF`s7e_H%ZwNUGD#yjmD!$QpTYvgIBV&O)1iwBT-Io%_==FFv-*BARaMofA zgQUYJSk1s8#aF>>HZjYGY>3OYyRAFPv8x;ux1<=e4Q9%w3x?-ZhO;3VRCquR4kj+c z%3)5H(el!g($Y5)p8JfkAUu5hs;m%-E^M@WOSrtPovS`u*;y>A9}7M)i*{KhKHE$) zERRF_GCyxzSysk{i3Px73SAcsoU1giuP|*8=YNIw?EA(**mU!OZY+P_g?WqT{NlpG z63>evkf>3{p@@Rz+uF6u-FkO90Y-924fTbsxY{K+#{`i?-7=RODd>dK7gljYn@`b*&=Lt{pXhto`1||kH#CTl zL@DxPFixR&q_)>UhbXElDk=uIPJ~G05AS_O1@wJ#;7p@F+}oqMyWGkpjeb+iw^NT{ z&M8VxPCiO$>0i}aUtgc2V?y{EFNjXoWbX8v{aj_7|J}{GzzbeWVe?Is@6qU;KF6QC zUbCob3G}r?V$ET(Bm8}Z_rX#ndb=}PGB@s00Loz>m-i5yH*eTYR($t>&W|4tWqY;+ zyj_#v0$}yUl4rhp_3G!heuk77|Nh!cn`@2mk%s#EugIg4gPr{o*6Yz@y393@I6y4v zk=-}%EP>ZMfLa>$jOEX&zQVd+U~Pn!Po6LZp3WG~ApKib<5=^W?lGA=|Fy3pnR70#Rbb{jhia+vUK(fV(KI=H1z%^Y?G-{b>TWquVk5@!ZYZA?H%J z7wi6D@Png0WVnm(Uj0hN@L(kCZ%BIfo+NR=iyqTkrn%q!+> znWySd0w|e2SpU$40}MUPI~Y}Uko`p|ARxePAEfx?!^TGb?9@~WnbNmt?jh1OmzOzG zeg-?++u9Ig1AN4+Gy4n~z*1LN;izegzOuS{Qpe29frEpi%3G4YrGLGu#jLWmwN>)P z%u^3tHC${auGqj*q97XCoC#?oyiK+69WAyL)snKZ$3Hhxv~G{+Lhf3kU1?Rs;@?(t zBSd}x@7yYRqM50wsqGL50c4Zcq1{6niYDhO*DRWPT4h8_+duD<35Sl3YcF2>a z$B{TF4WNc6>l5CE=({RHQAgQMfnGXiXJ@ln`tB5z-Q3)qDElQzrOXOy%n8QbM9_z! z$RaBtV7H{`M@9zqms8(YQ#%mJF#$CWEGB9u#aWMRsvIU7>4#6$1X6Xnv=afHaDd6h zVYKU=kog=hGS_Ci+uJY2{mvYI!USIqEq|GsDKks~n%27@&vRUyoL}(3n-r-Zsn)TW zHoRQj43M=`J@3HPX5k!QY&@SU7LQ9kQe;*90LAJhXjVu_$m1H>nGz>`G|q~Y1!q99 zaUPzDX>C;JYrC#v^0JA?^{ml+`8KB^RCkM~#jqI5{5&MOxK7*c_Y`uqBBZ*Q-T66mCS_4p-s3P)`V^@*55MQI80kokL^pPwJH z-stw$cK}d{x{Vvrlh?2s=$HTY?So)e@iCE#5bvC##d=4CprGJIwx%s>LG34PEws)i z2*1zwOyr5}^khsXL0gTS`$;c1k;d6lm7S|BPZ=$)<1Wc3giSy1cnM z*DBKC@ZmU*7{-bOe7+YfyAqC1bNF#=jF3nkCyp){+0OEmJZ9T|KX;1QlvPw-T+4Ai z)sej})W}?#nVEqKXO3uLt)Zxc`bJ~p;%;tE=PWz;#l^)N$O;`i(Sbjt>(kTI-*k6( z^Mh>?P^cmmne%!g2nlbFdkCX8cT-2OBAuO`DW7*)mCQT^GKaDQ9)8{0@@r{s<~f+J zu@!!$&S_K#>Q)azwrja%$4a&1V!iykIX}#P*Ly*G(QmS(0)GAoo1LBI=!r>p?BgeA zESefXR)`o9v1$Z{K#AOImiD~8zC`$)A7=B_3#Bi44L_ZP%#`Y<_w^|!jBO2ViE2V) zq@dx#zg!-n==Ur%)D$k<{aOof{W6nD*@Q`=*I;x~ZQ2^5oQ% z?#%S`M(grgkdE41g{iHC+r~$_0f<|6X(>%aL_}GR&(~LB5BAEt2bwYG4?L^kQxDq< z3zgr`JE*acs0M!PrA!LAI(v0i)e6;kS6a$BbbE7k@*wHsptq#O*~FG}1~5azV(L_h%~$lB6e8R#vWkpb~EvK|p5$KrG%xo#%2;V4>N0 zc${|6Qw=aRC5<1k6^rpTeN^H>UI5<#Z&7$Th^%!KLq8!e(gCVUnu?Y3Rw4fbQOGUO literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..e2093af548b0894275b785854ff4627e7c1c8864 GIT binary patch literal 12071 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 bbxz29!fbY3R-qF-umP0h)McL{%tQYN{|)~w literal 0 HcmV?d00001 diff --git a/locales/en.json b/locales/en.json index 745fec29d..41e3e4f0f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -985,6 +985,9 @@ }, "enforceSpeedloaderOnStartup": { "title": "Enforce gossip sync on startup" + }, + "persistentServices": { + "title": "Enable persistent LND and Tor (if enabled)" } } }, diff --git a/src/migration/app-migration.ts b/src/migration/app-migration.ts index 3ca4b335d..02efd77cf 100644 --- a/src/migration/app-migration.ts +++ b/src/migration/app-migration.ts @@ -290,6 +290,12 @@ export const appMigration: IAppMigration[] = [ { async beforeLnd(db, i) { await db.executeSql("ALTER TABLE contact ADD label TEXT NULL"); + } + }, + // Version 35 + { + async beforeLnd(db, i) { + setItemObject(StorageItem.persistentServicesEnabled, false); }, }, // Version 35 @@ -299,5 +305,11 @@ export const appMigration: IAppMigration[] = [ await setItemObject(StorageItem.lastScheduledGossipSync, 0); await setItemObject(StorageItem.lastScheduledGossipSyncAttempt, 0); } - } + }, + // Version 34 + { + async beforeLnd(db, i) { + setItemObject(StorageItem.persistentServicesWarningShown, false); + }, + }, ]; diff --git a/src/state/NotificationManager.ts b/src/state/NotificationManager.ts index 86ffe20f4..fd99a4023 100644 --- a/src/state/NotificationManager.ts +++ b/src/state/NotificationManager.ts @@ -41,7 +41,7 @@ export const notificationManager: INotificationManagerModel = { } } else if (PLATFORM === "android") { const granted = await PermissionsAndroid.request( - "android.permission.POST_NOTIFICATIONS" // FIXME PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS needs newer react-native version + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS ); if (granted === "denied" || granted === "never_ask_again") { log.w("Post notification permission was denied", [granted]); diff --git a/src/state/Settings.ts b/src/state/Settings.ts index 5b8035b82..d272fdb87 100644 --- a/src/state/Settings.ts +++ b/src/state/Settings.ts @@ -81,6 +81,8 @@ export interface ISettingsModel { changeLndLogLevel: Thunk; changeLndCompactDb: Thunk; changeEnforceSpeedloaderOnStartup: Thunk; + changePersistentServicesEnabled: Thunk; + changePersistentServicesWarningShown: Thunk; setBitcoinUnit: Action; setFiatUnit: Action; @@ -121,6 +123,8 @@ export interface ISettingsModel { setLndLogLevel: Action; setLndCompactDb: Action; setEnforceSpeedloaderOnStartup: Action; + setPersistentServicesEnabled: Action; + setPersistentServicesWarningShown: Action; bitcoinUnit: keyof IBitcoinUnits; fiatUnit: keyof IFiatRates; @@ -161,6 +165,8 @@ export interface ISettingsModel { lndCompactDb: boolean; zeroConfPeers: string[]; enforceSpeedloaderOnStartup: boolean; + persistentServicesEnabled: boolean; + persistentServicesWarningShown: boolean; } export const settings: ISettingsModel = { @@ -235,6 +241,12 @@ export const settings: ISettingsModel = { actions.setEnforceSpeedloaderOnStartup( await getItemObject(StorageItem.enforceSpeedloaderOnStartup || false), ); + actions.setPersistentServicesEnabled( + (await getItemObject(StorageItem.persistentServicesEnabled)) ?? false, + ); + actions.setPersistentServicesWarningShown( + (await getItemObject(StorageItem.persistentServicesWarningShown)) ?? false, + ); log.d("Done"); }), @@ -439,6 +451,16 @@ export const settings: ISettingsModel = { actions.setEnforceSpeedloaderOnStartup(payload); }), + changePersistentServicesEnabled: thunk(async (actions, payload) => { + await setItemObject(StorageItem.persistentServicesEnabled, payload); + actions.setPersistentServicesEnabled(payload); + }), + + changePersistentServicesWarningShown: thunk(async (actions, payload) => { + await setItemObject(StorageItem.persistentServicesWarningShown, payload); + actions.setPersistentServicesWarningShown(payload); + }), + setBitcoinUnit: action((state, payload) => { state.bitcoinUnit = payload; }), @@ -556,6 +578,12 @@ export const settings: ISettingsModel = { setEnforceSpeedloaderOnStartup: action((state, payload) => { state.enforceSpeedloaderOnStartup = payload; }), + setPersistentServicesEnabled: action((state, payload) => { + state.persistentServicesEnabled = payload; + }), + setPersistentServicesWarningShown: action((state, payload) => { + state.persistentServicesWarningShown = payload; + }), bitcoinUnit: "bitcoin", fiatUnit: "USD", @@ -596,4 +624,6 @@ export const settings: ISettingsModel = { lndLogLevel: DEFAULT_LND_LOG_LEVEL, lndCompactDb: false, enforceSpeedloaderOnStartup: false, + persistentServicesEnabled: false, + persistentServicesWarningShown: false, }; diff --git a/src/state/index.ts b/src/state/index.ts index 4dd3ffdf2..14fc9e49c 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -240,13 +240,13 @@ export const model: IStoreModel = { try { actions.setTorLoading(true); if (PLATFORM === "android") { - await NativeModules.BlixtTor.startTor(); + socksPort = await NativeModules.BlixtTor.startTor(); } else if (PLATFORM === "ios") { const tor = Tor({ stopDaemonOnBackground: false, startDaemonOnActive: true, }); - socksPort = await tor.startIfNotStarted(); + await tor.startIfNotStarted(); } log.i("socksPort", [socksPort]); if (socksPort === 0 && PLATFORM === "ios") { @@ -285,7 +285,12 @@ export const model: IStoreModel = { } } } - + let persistentServicesEnabled = await getItemObjectAsyncStorage(StorageItem.persistentServicesEnabled) ?? false; + let persistentServicesWarningShown = await getItemObjectAsyncStorage(StorageItem.persistentServicesWarningShown) ?? false; + if (persistentServicesEnabled && !persistentServicesWarningShown) { + await setItemObject(StorageItem.persistentServicesWarningShown, true); + await NativeModules.BlixtTor.showMsg(); + } log.v("Running LndMobile.initialize()"); const initReturn = await initialize(); log.i("initialize done", [initReturn]); diff --git a/src/storage/app.ts b/src/storage/app.ts index 4ee528aa4..b587ff773 100644 --- a/src/storage/app.ts +++ b/src/storage/app.ts @@ -81,6 +81,8 @@ export enum StorageItem { // const enums not supported in Babel 7... lndCompactDb = "lndCompactDb", zeroConfPeers = "zeroConfPeers", enforceSpeedloaderOnStartup = "enforceSpeedloaderOnStartup", + persistentServicesEnabled = "persistentServicesEnabled", + persistentServicesWarningShown = "persistentServicesWarningShown", } export const setItem = async (key: StorageItem, value: string) => @@ -181,6 +183,8 @@ export const clearApp = async () => { removeItem(StorageItem.lndLogLevel), removeItem(StorageItem.lndCompactDb), removeItem(StorageItem.enforceSpeedloaderOnStartup), + removeItem(StorageItem.persistentServicesEnabled), + removeItem(StorageItem.persistentServicesWarningShown), ]); }; @@ -264,5 +268,7 @@ export const setupApp = async () => { setItem(StorageItem.lndLogLevel, DEFAULT_LND_LOG_LEVEL), setItemObject(StorageItem.lndCompactDb, false), setItemObject(StorageItem.enforceSpeedloaderOnStartup, false), + setItemObject(StorageItem.persistentServicesEnabled, false), + setItemObject(StorageItem.persistentServicesWarningShown, false), ]); }; diff --git a/src/windows/InitProcess/DEV_Commands.tsx b/src/windows/InitProcess/DEV_Commands.tsx index 22b879f71..4dfdeabeb 100644 --- a/src/windows/InitProcess/DEV_Commands.tsx +++ b/src/windows/InitProcess/DEV_Commands.tsx @@ -107,6 +107,9 @@ export default function DEV_Commands({ navigation, continueCallback }: IProps) { toast(e.message); } }}>contact label fix + @@ -514,6 +517,7 @@ export default function DEV_Commands({ navigation, continueCallback }: IProps) { + diff --git a/src/windows/Settings/Settings.tsx b/src/windows/Settings/Settings.tsx index 3b12fc344..c12229095 100644 --- a/src/windows/Settings/Settings.tsx +++ b/src/windows/Settings/Settings.tsx @@ -697,6 +697,7 @@ ${t("LN.inbound.dialog.msg3")}`; text: t("buttons.yes", { ns: namespaces.common }), onPress: async () => { try { + await NativeModules.BlixtTor.stopTor(); await NativeModules.LndMobile.stopLnd(); await NativeModules.LndMobileTools.killLnd(); } catch (e) { @@ -1363,6 +1364,14 @@ ${t("experimental.tor.disabled.msg2")}`; await changeEnforceSpeedloaderOnStartup(!enforceSpeedloaderOnStartup); }; + // Persistent services + const persistentServicesEnabled = useStoreState((store) => store.settings.persistentServicesEnabled); + const changePersistentServicesEnabled = useStoreActions((store) => store.settings.changePersistentServicesEnabled); + const changePersistentServicesEnabledPress = async () => { + await changePersistentServicesEnabled(!persistentServicesEnabled); + restartNeeded(); + }; + return ( @@ -2070,6 +2079,13 @@ ${t("experimental.tor.disabled.msg2")}`; } + + + + {t("debug.persistentServices.title")} + + + {t("debug.title")} @@ -2339,7 +2355,13 @@ ${t("experimental.tor.disabled.msg2")}`; /> - + + + + {t("debug.bimodalPathFinding.title")} + + +