├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── android-master.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── lvaccaro │ │ └── lamp │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ └── lvaccaro │ │ │ └── lamp │ │ │ ├── LightningCli.kt │ │ │ ├── MainActivity.kt │ │ │ ├── activities │ │ │ ├── BuildInvoiceActivity.kt │ │ │ ├── ChannelsActivity.kt │ │ │ ├── ConsoleActivity.kt │ │ │ ├── LogActivity.kt │ │ │ ├── ScanActivity.kt │ │ │ ├── SendActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ └── UriResultActivity.kt │ │ │ ├── adapters │ │ │ ├── BalanceAdapter.kt │ │ │ └── HashMapAdapter.kt │ │ │ ├── fragments │ │ │ ├── ChannelFragment.kt │ │ │ ├── DecodedInvoiceFragment.kt │ │ │ ├── FundChannelFragment.kt │ │ │ ├── PeerInfoFragment.kt │ │ │ ├── RecyclerViewFragment.kt │ │ │ └── WithdrawFragment.kt │ │ │ ├── handlers │ │ │ ├── BrokenStatus.kt │ │ │ ├── IEventHandler.kt │ │ │ ├── NewBlockHandler.kt │ │ │ ├── NewChannelPayment.kt │ │ │ ├── NewTransaction.kt │ │ │ ├── NodeUpHandler.kt │ │ │ ├── PaidInvoice.kt │ │ │ └── ShutdownNode.kt │ │ │ ├── services │ │ │ ├── CLightningException.kt │ │ │ ├── Globber.kt │ │ │ ├── LightningService.kt │ │ │ └── TorService.kt │ │ │ ├── utils │ │ │ ├── Archive.kt │ │ │ ├── LampKeys.kt │ │ │ ├── LogObserver.kt │ │ │ ├── SimulatorPlugin.kt │ │ │ ├── UI.kt │ │ │ └── Validator.kt │ │ │ └── views │ │ │ ├── HistoryBottomSheet.kt │ │ │ └── PowerImageView.kt │ └── res │ │ ├── drawable-anydpi │ │ ├── ic_camera.xml │ │ ├── ic_channels.xml │ │ └── ic_paste.xml │ │ ├── drawable-hdpi │ │ ├── ic_camera.png │ │ ├── ic_channels.png │ │ ├── ic_notification.png │ │ ├── ic_paste.png │ │ ├── ic_sats.png │ │ └── ic_tor.png │ │ ├── drawable-mdpi │ │ ├── ic_camera.png │ │ ├── ic_channels.png │ │ ├── ic_notification.png │ │ ├── ic_paste.png │ │ ├── ic_sats.png │ │ └── ic_tor.png │ │ ├── drawable-xhdpi │ │ ├── ic_camera.png │ │ ├── ic_channels.png │ │ ├── ic_notification.png │ │ ├── ic_paste.png │ │ ├── ic_sats.png │ │ └── ic_tor.png │ │ ├── drawable-xxhdpi │ │ ├── ic_camera.png │ │ ├── ic_channels.png │ │ ├── ic_notification.png │ │ ├── ic_paste.png │ │ ├── ic_sats.png │ │ └── ic_tor.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_notification.png │ │ ├── ic_sats.png │ │ └── ic_tor.png │ │ ├── drawable │ │ ├── divider.xml │ │ ├── ic_arrow_down.xml │ │ ├── ic_arrow_up.xml │ │ ├── ic_baseline_account_balance_24.xml │ │ ├── ic_baseline_account_box_24.xml │ │ ├── ic_baseline_call_received_24.xml │ │ ├── ic_baseline_send_24.xml │ │ ├── ic_baseline_share_24.xml │ │ ├── ic_blockchain.xml │ │ ├── ic_lamp.xml │ │ ├── ic_lamp1.png │ │ ├── ic_lamp2.png │ │ ├── ic_lamp3.png │ │ ├── ic_lamp4.png │ │ ├── ic_lamp_off.png │ │ ├── ic_lamp_on.png │ │ └── ic_lightning.xml │ │ ├── layout │ │ ├── activity_build_invoice.xml │ │ ├── activity_channels.xml │ │ ├── activity_console.xml │ │ ├── activity_log.xml │ │ ├── activity_main.xml │ │ ├── activity_scan.xml │ │ ├── activity_send.xml │ │ ├── activity_settings.xml │ │ ├── content_main_off.xml │ │ ├── content_main_on.xml │ │ ├── fragment_channel.xml │ │ ├── fragment_decoded_invoice.xml │ │ ├── fragment_fundchannel.xml │ │ ├── fragment_history.xml │ │ ├── fragment_on_main_view.xml │ │ ├── fragment_peer_info.xml │ │ ├── fragment_recyclerview.xml │ │ ├── fragment_withdraw.xml │ │ ├── list_balance.xml │ │ ├── list_channel.xml │ │ └── list_tx.xml │ │ ├── menu │ │ ├── menu.xml │ │ ├── menu_channels.xml │ │ ├── menu_invoice.xml │ │ ├── menu_log.xml │ │ └── menu_scan.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ └── ic_launcher_foreground.png │ │ ├── values │ │ ├── arrays.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── root_preferences.xml │ └── test │ └── java │ └── com │ └── lvaccaro │ └── lamp │ └── ValidatorUnitTest.kt ├── build.gradle ├── doc ├── cmdline-tools-setup.md └── img │ ├── Screen1.png │ ├── Screen2.png │ └── Screen3.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.editorconfig: -------------------------------------------------------------------------------- 1 | # Note that in this case 'import-ordering' rule will be active and 'indent' will be disabled 2 | [api/*.{kt,kts}] 3 | root = true 4 | disabled_rules=indent 5 | 6 | # Comma-separated list of rules to disable (Since 0.34.0) 7 | # Note that rules in any ruleset other than the standard ruleset will need to be prefixed 8 | # by the ruleset identifier. 9 | disabled_rules=no-wildcard-imports,experimental:annotation,my-custom-ruleset 10 | 11 | # Defines the imports layout. The layout can be composed by the following symbols: 12 | # "*" - wildcard. There must be at least one entry of a single wildcard to match all other imports. Matches anything after a specified symbol/import as well. 13 | # "|" - blank line. Supports only single blank lines between imports. No blank line is allowed in the beginning or end of the layout. 14 | # "^" - alias import, e.g. "^android.*" will match all android alias imports, "^" will match all other alias imports. 15 | # import paths - these can be full paths, e.g. "java.util.List.*" as well as wildcard paths, e.g. "kotlin.**" 16 | # Examples (we use ij_kotlin_imports_layout to set an imports layout for both ktlint and IDEA via a single property): 17 | # ij_kotlin_imports_layout=* # alphabetical with capital letters before lower case letters (e.g. Z before a), no blank lines 18 | # ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ # default IntelliJ IDEA style, same as alphabetical, but with "java", "javax", "kotlin" and alias imports in the end of the imports list 19 | # ij_kotlin_imports_layout=android.**,|,^org.junit.**,kotlin.io.Closeable.*,|,*,^ # custom imports layout 20 | 21 | # According to https://kotlinlang.org/docs/reference/coding-conventions.html#names-for-test-methods it is acceptable to write method names 22 | # in natural language. When using natural language, the description tends to be longer. Allow lines containing an identifier between 23 | # backticks to be longer than the maximum line length. (Since 0.41.0) 24 | [**/test/**.kt] 25 | ktlint_ignore_back_ticked_identifier=true -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [vincenzopalazzo, lvaccaro] 2 | custom: [https://btctip.lvaccaro.com] -------------------------------------------------------------------------------- /.github/workflows/android-master.yml: -------------------------------------------------------------------------------- 1 | name: android-master 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | push: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | apk: 13 | name: Generate APK 14 | runs-on: ubuntu-18.04 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: set up JDK 11 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 11 22 | - name: Check koltin formatting 23 | run: bash ./gradlew lintKotlin 24 | - name: Build debug APK 25 | run: bash ./gradlew assembleDebug --stacktrace 26 | - name: Upload APK 27 | uses: actions/upload-artifact@v1 28 | with: 29 | name: app 30 | path: app/build/outputs/apk/debug/app-debug.apk 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'release*' 7 | 8 | jobs: 9 | apk: 10 | name: Generate Signed APK 11 | runs-on: ubuntu-18.04 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: set up JDK 1.8 16 | uses: actions/setup-java@v1 17 | with: 18 | java-version: 1.8 19 | - name: Build debug APK 20 | run: bash ./gradlew assembleRelease --stacktrace 21 | - name: Sign App release 22 | uses: r0adkll/sign-android-release@v1 23 | id: sign_app 24 | with: 25 | releaseDirectory: app/build/outputs/apk/release 26 | signingKeyBase64: ${{ secrets.SIGNING_KEY }} 27 | alias: ${{ secrets.ALIAS }} 28 | keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} 29 | keyPassword: ${{ secrets.KEY_PASSWORD }} 30 | 31 | - uses: actions/upload-artifact@v2 32 | with: 33 | name: Signed app bundle 34 | path: ${{steps.sign_app.outputs.signedReleaseFile}} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | app/release 11 | app/libs 12 | app/build 13 | keystore* 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luca Vaccaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

:bulb: Lamp :zap:

3 | 4 | 5 | 6 |

7 | :bulb: c-lightning Android Mobile Porting :zap: 8 |

9 | 10 |

11 | 12 | GitHub Workflow Status (branch) 13 | 14 | 15 | GitHub release (latest SemVer) 16 | 17 | GitHub all releases 18 | 19 | GitHub 20 | 21 |

22 | 23 |

24 | 25 | 26 | 27 |

28 |
29 | 30 | ## Table of Content 31 | 32 | - Introduction 33 | - How to Use 34 | - Build 35 | - How to Contribute 36 | - References 37 | - License 38 | 39 | ## Introduction 40 | 41 | > This is an experimenting lightning wallet. Use it on testnet or only with amounts you can afford to lose on mainnet. 42 | 43 | Touch the lamp to download and run c-lightning from cross-compiled binaries for Android are available [here](https://github.com/clightning4j/lightning_ndk/releases). 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ## How to Use 52 | 53 | ### Bitcoin Setup 54 | 55 | ##### Automatic with esplora plugin 56 | 57 | This is the default behaviour. 58 | 59 | Lamp is using [the C Esplora plugin for C-lightning](https://github.com/clightning4j/esplora_clnd_plugin) as the Bitcoin backend of the lightning node (to fetch chain/blocks/transactions information and send transactions). 60 | 61 | You can point it to your own [Esplora](github.com/Blockstream/esplora) instance in the settings, and it uses [blockstream.info](https://blockstream.info) by default. 62 | 63 | 64 | ##### Manually with bitcoind rpc node 65 | On Lamp settings, disable Esplora plugin and set the current Bitcoin RPC options: 66 | 67 | - Bitcoin RPC username 68 | - Bitcoin RPC password 69 | - Bitcoin RPC host (default 127.0.0.1) 70 | - Bitcoin RPC port (default 18332 for testnet) 71 | 72 | ### Tor Setup 73 | 74 | ##### Automatic with internal tor service 75 | 76 | Lamp is using tor hidden service as default. A new hidden service will be created at the first running time. 77 | 78 | ##### Manually with Orbot 79 | 80 | Open [Orbot](https://github.com/guardianproject/Orbot) and setup a fixed tor address by menu: Onion Services -> Hosted Services -> set a service name and port 9735. Restarting tor to discover and copy the local address. 81 | 82 | On Lamp settings, enable proxy using orbot localhost gateway: 83 | 84 | - proxy: 127.0.0.1:9050 85 | - announce address: tor_address 86 | - bind address: 127.0.0.1:9735 87 | 88 | Read the following instructions at [Tor on clightning](https://lightning.readthedocs.io/TOR.html) to setup address on different network scenario. 89 | 90 | ## Building 91 | 92 | * [in Linux using cmdline tools](doc/cmdline-tools-setup.md) 93 | 94 | ## How to Contribute 95 | 96 | Just propose new stuff to add or bug fixing with a new PR. 97 | 98 | ### Code Style 99 | [![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/) 100 | 101 | > We live in a world where robots can drive a car, so we shouldn't just write code, we should write elegant code. 102 | 103 | This repository use [ktlint](https://github.com/pinterest/ktlint) to maintains the code of the repository elegant, so 104 | before submit the code check the Kotlin format with the following command on the root of the directory 105 | 106 | ```bash 107 | ./gradlew formatKotlin 108 | ``` 109 | 110 | ## References 111 | 112 | - [ABCore](https://github.com/greenaddress/abcore) Android Bitcoin Core wallet 113 | - [bitcoin_ndk](https://github.com/greenaddress/bitcoin_ndk) ndk build of bitcoin core and knots 114 | - [clightning_ndk](https://github.com/clightning4j/lightning_ndk) android cross-compilation of c-lightning for Android >= 24 Api 115 | - [c-lightning](https://github.com/ElementsProject/lightning) Lightning Network implementation in C 116 | - [esplora plugin](https://github.com/clightning4j/esplora_clnd_plugin) C-Lightning plugin for esplora 117 | 118 | ## License 119 | 120 |
121 | 122 |
123 | 124 | c-lightning Android Mobile Porting 125 | 126 | Copyright (c) 2019-2021 Luca Vaccaro 127 | 128 | This program is free software; you can redistribute it and/or modify 129 | it under the terms of the GNU General Public License as published by 130 | the Free Software Foundation; either version 2 of the License. 131 | 132 | This program is distributed in the hope that it will be useful, 133 | but WITHOUT ANY WARRANTY; without even the implied warranty of 134 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 135 | GNU General Public License for more details. 136 | 137 | You should have received a copy of the GNU General Public License along 138 | with this program; if not, write to the Free Software Foundation, Inc., 139 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 140 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: "org.jmailen.kotlinter" 5 | 6 | android { 7 | compileSdkVersion 28 8 | buildToolsVersion "29.0.3" 9 | defaultConfig { 10 | applicationId "com.lvaccaro.lamp" 11 | minSdkVersion 24 12 | targetSdkVersion 28 13 | versionCode 37 14 | versionName "3.7" 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | vectorDrawables.useSupportLibrary = true 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | testOptions { 26 | unitTests.returnDefaultValues = true 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | kotlinOptions { 35 | jvmTarget = JavaVersion.VERSION_1_8.toString() 36 | } 37 | 38 | lintOptions { 39 | abortOnError false 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation fileTree(dir: 'libs', include: ['*.jar']) 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 46 | implementation 'androidx.appcompat:appcompat:1.3.0' 47 | implementation 'androidx.core:core-ktx:1.5.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 49 | implementation 'androidx.preference:preference-ktx:1.1.1' 50 | implementation 'org.tukaani:xz:1.8' 51 | implementation 'org.apache.commons:commons-compress:1.20' 52 | implementation 'com.google.zxing:core:3.4.0' 53 | implementation 'me.dm7.barcodescanner:zxing:1.9.13' 54 | implementation 'com.google.android.material:material:1.3.0' 55 | implementation "org.jetbrains.anko:anko-commons:0.10.4" 56 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 57 | 58 | testImplementation 'junit:junit:4.13.2' 59 | androidTestImplementation 'androidx.test:runner:1.3.0' 60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 61 | } 62 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/lvaccaro/lamp/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.lvaccaro.lamp", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 25 | 28 | 29 | 30 | 35 | 38 | 39 | 44 | 47 | 48 | 52 | 55 | 56 | 60 | 63 | 64 | 68 | 71 | 72 | 76 | 79 | 80 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clightning4j/lamp/e3fe170cd3dd314222f228127fc0b9d6ca97c7ed/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/LightningCli.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import androidx.preference.PreferenceManager 6 | import com.lvaccaro.lamp.services.LightningService 7 | import org.json.JSONObject 8 | import java.io.File 9 | import java.io.InputStream 10 | import java.util.logging.Logger 11 | 12 | class LightningCli { 13 | 14 | val command = "lightning-cli" 15 | val log = Logger.getLogger(LightningService::class.java.name) 16 | 17 | @Throws(Exception::class) 18 | fun exec(c: Context, options: Array, json: Boolean = true): InputStream { 19 | val binaryDir = c.rootDir() 20 | val lightningDir = File(c.rootDir(), ".lightning") 21 | val sharedPref = PreferenceManager.getDefaultSharedPreferences(c) 22 | val network = sharedPref.getString("network", "testnet").toString() 23 | 24 | val args = arrayOf( 25 | String.format("%s/cli/%s", binaryDir.canonicalPath, command), 26 | String.format("--network=%s", network), 27 | String.format("--lightning-dir=%s", lightningDir.path), 28 | String.format("--%s", if (json == true) "json" else "raw") 29 | ) 30 | 31 | val pb = ProcessBuilder((args + options).asList()) 32 | pb.directory(binaryDir) 33 | // pb.redirectErrorStream(true) 34 | 35 | val process = pb.start() 36 | val code = process.waitFor() 37 | if (code != 0) { 38 | val error = process.errorStream.toText() 39 | val input = process.inputStream.toText() 40 | log.info(error) 41 | log.info(input) 42 | throw Exception(if (!error.isEmpty()) error else input) 43 | } 44 | return process.inputStream 45 | } 46 | } 47 | 48 | // extension to convert inputStream in text 49 | fun InputStream.toText(): String { 50 | val reader = bufferedReader() 51 | val builder = StringBuilder() 52 | var line = reader.readLine() 53 | while (line != null) { 54 | if (!line.startsWith("**")) { 55 | builder.append(line + "\r\n") 56 | } 57 | line = reader.readLine() 58 | } 59 | return builder.toString() 60 | } 61 | 62 | // extension to convert inputStream in json object 63 | fun InputStream.toJSONObject(): JSONObject { 64 | val text = toText() 65 | val json = JSONObject(text) 66 | return json 67 | } 68 | 69 | // extension to provide the rootDir 70 | fun Context.rootDir(): File { 71 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 72 | return noBackupFilesDir 73 | } 74 | return filesDir 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/activities/BuildInvoiceActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.activities 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.view.inputmethod.InputMethodManager 7 | import android.widget.Toast 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.lvaccaro.lamp.LightningCli 10 | import com.lvaccaro.lamp.R 11 | import com.lvaccaro.lamp.toJSONObject 12 | import com.lvaccaro.lamp.utils.UI 13 | import kotlinx.android.synthetic.main.activity_build_invoice.balanceText 14 | import kotlinx.android.synthetic.main.activity_build_invoice.btcButton 15 | import kotlinx.android.synthetic.main.activity_build_invoice.btclnLayout 16 | import kotlinx.android.synthetic.main.activity_build_invoice.copyButton 17 | import kotlinx.android.synthetic.main.activity_build_invoice.copyShareLayout 18 | import kotlinx.android.synthetic.main.activity_build_invoice.descriptionText 19 | import kotlinx.android.synthetic.main.activity_build_invoice.expiredText 20 | import kotlinx.android.synthetic.main.activity_build_invoice.expiredTitle 21 | import kotlinx.android.synthetic.main.activity_build_invoice.labelText 22 | import kotlinx.android.synthetic.main.activity_build_invoice.lightningButton 23 | import kotlinx.android.synthetic.main.activity_build_invoice.qrImage 24 | import kotlinx.android.synthetic.main.activity_build_invoice.shareButton 25 | import org.jetbrains.anko.contentView 26 | import org.jetbrains.anko.doAsync 27 | import org.json.JSONObject 28 | import java.lang.Exception 29 | import java.text.SimpleDateFormat 30 | import java.util.Date 31 | 32 | class BuildInvoiceActivity : AppCompatActivity() { 33 | 34 | private val cli = LightningCli() 35 | private var decoded: JSONObject? = null 36 | 37 | override fun onCreate(savedInstanceState: Bundle?) { 38 | super.onCreate(savedInstanceState) 39 | setContentView(R.layout.activity_build_invoice) 40 | balanceText.requestFocus() 41 | 42 | lightningButton.setOnClickListener { 43 | val amount = balanceText.text.toString() 44 | var sat = if (amount.isEmpty()) "any" else (amount.toDouble() * 1000).toLong().toString() 45 | doAsync { invoice(sat, labelText.text.toString(), descriptionText.text.toString()) } 46 | } 47 | 48 | btcButton.setOnClickListener { 49 | doAsync { generate() } 50 | } 51 | } 52 | 53 | fun generate() { 54 | try { 55 | val res = cli.exec(this, arrayOf("newaddr"), true).toJSONObject() 56 | runOnUiThread { showAddress(res["address"] as String) } 57 | } catch (e: Exception) { 58 | runOnUiThread { 59 | Toast.makeText( 60 | this, 61 | e.localizedMessage, 62 | Toast.LENGTH_LONG 63 | ).show() 64 | } 65 | } 66 | } 67 | 68 | fun invoice(amount: String, label: String, description: String) { 69 | try { 70 | val res = cli.exec( 71 | this, 72 | arrayOf( 73 | "invoice", 74 | amount, 75 | label, 76 | description 77 | ), 78 | true 79 | ).toJSONObject() 80 | runOnUiThread { showInvoice(res["bolt11"] as String) } 81 | } catch (e: Exception) { 82 | runOnUiThread { 83 | Toast.makeText( 84 | this, 85 | e.localizedMessage, 86 | Toast.LENGTH_LONG 87 | ).show() 88 | } 89 | } 90 | } 91 | 92 | fun showAddress(address: String) { 93 | labelText.visibility = View.GONE 94 | descriptionText.visibility = View.GONE 95 | btclnLayout.visibility = View.GONE 96 | copyShareLayout.visibility = View.VISIBLE 97 | 98 | copyButton.setOnClickListener { UI.copyToClipboard(this, "address", address) } 99 | shareButton.setOnClickListener { UI.share(this, "address", address) } 100 | 101 | // hide keyboard 102 | val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 103 | inputMethodManager.hideSoftInputFromWindow(contentView?.windowToken, 0) 104 | 105 | // show bolt11 106 | val qr = UI.getQrCode(address) 107 | qrImage.setImageBitmap(qr) 108 | 109 | expiredTitle.visibility = View.VISIBLE 110 | expiredText.visibility = View.VISIBLE 111 | expiredTitle.text = "Address" 112 | expiredText.text = address 113 | } 114 | 115 | fun showInvoice(bolt11: String) { 116 | balanceText.isEnabled = false 117 | labelText.isEnabled = false 118 | descriptionText.isEnabled = false 119 | labelText.visibility = View.GONE 120 | btclnLayout.visibility = View.GONE 121 | copyShareLayout.visibility = View.VISIBLE 122 | 123 | copyButton.setOnClickListener { UI.copyToClipboard(this, "bolt11", bolt11) } 124 | shareButton.setOnClickListener { UI.share(this, "bolt11", bolt11) } 125 | 126 | // hide keyboard 127 | val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 128 | inputMethodManager.hideSoftInputFromWindow(contentView?.windowToken, 0) 129 | 130 | // show bolt11 131 | val qr = UI.getQrCode(bolt11) 132 | qrImage.setImageBitmap(qr) 133 | 134 | // get expired time 135 | doAsync { decodeInvoice(bolt11) } 136 | } 137 | 138 | private fun decodeInvoice(bolt11: String) { 139 | val res = cli.exec(this, arrayOf("decodepay", bolt11), true) 140 | .toJSONObject() 141 | decoded = res 142 | val created_at = res["created_at"] as Int 143 | val expiry = res["expiry"] as Int 144 | val date = Date(created_at * 1000L + expiry) 145 | runOnUiThread { 146 | expiredTitle.visibility = View.VISIBLE 147 | expiredText.visibility = View.VISIBLE 148 | expiredText.text = SimpleDateFormat("HH:mm:ss, dd MMM yyyy").format(date) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/activities/ChannelsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.activities 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.Menu 6 | import android.view.MenuItem 7 | import android.view.ViewGroup 8 | import android.widget.ProgressBar 9 | import android.widget.TextView 10 | import android.widget.Toast 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.recyclerview.widget.LinearLayoutManager 13 | import androidx.recyclerview.widget.RecyclerView 14 | import com.lvaccaro.lamp.LightningCli 15 | import com.lvaccaro.lamp.R 16 | import com.lvaccaro.lamp.fragments.ChannelFragment 17 | import com.lvaccaro.lamp.fragments.FundChannelFragment 18 | import com.lvaccaro.lamp.toJSONObject 19 | import kotlinx.android.synthetic.main.activity_channels.toolbar 20 | import org.jetbrains.anko.doAsync 21 | import org.json.JSONArray 22 | import org.json.JSONObject 23 | import java.lang.Exception 24 | import kotlin.collections.ArrayList 25 | 26 | typealias ChannelClickListener = (JSONObject) -> Unit 27 | 28 | class ChannelAdapter( 29 | val list: ArrayList, 30 | private val onClickListener: ChannelClickListener 31 | ) : 32 | RecyclerView.Adapter() { 33 | 34 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelViewHolder { 35 | val inflater = LayoutInflater.from(parent.context) 36 | return ChannelViewHolder(inflater, parent) 37 | } 38 | 39 | override fun onBindViewHolder(holder: ChannelViewHolder, position: Int) { 40 | val item: JSONObject = list[position] 41 | holder.bind(item) 42 | holder.itemView.setOnClickListener { onClickListener(item) } 43 | } 44 | 45 | override fun getItemCount(): Int = list.size 46 | } 47 | 48 | class ChannelViewHolder(inflater: LayoutInflater, parent: ViewGroup) : 49 | RecyclerView.ViewHolder(inflater.inflate(R.layout.list_channel, parent, false)) { 50 | 51 | fun bind(channel: JSONObject) { 52 | val cid = channel.getString("channel_id") 53 | val msatoshi_to_us = channel.getDouble("msatoshi_to_us") / 1000 54 | val msatoshi_total = channel.getDouble("msatoshi_total") / 1000 55 | itemView.findViewById(R.id.cid).text = "CID: ${cid.subSequence(0,8)}..." 56 | itemView.findViewById(R.id.status).text = channel.getString("state") 57 | itemView.findViewById(R.id.mysats).text = "My balance: $msatoshi_to_us sat" 58 | itemView.findViewById(R.id.availablesats).text = "Available to receive: $msatoshi_total sat" 59 | itemView.findViewById(R.id.progressBar).apply { 60 | max = msatoshi_total.toInt() 61 | progress = msatoshi_to_us.toInt() 62 | } 63 | } 64 | } 65 | 66 | class ChannelsActivity : AppCompatActivity() { 67 | 68 | lateinit var recyclerView: RecyclerView 69 | 70 | override fun onCreate(savedInstanceState: Bundle?) { 71 | super.onCreate(savedInstanceState) 72 | setContentView(R.layout.activity_channels) 73 | setSupportActionBar(toolbar) 74 | 75 | recyclerView = findViewById(R.id.recycler_view) 76 | recyclerView.layoutManager = LinearLayoutManager(this) 77 | recyclerView.adapter = ChannelAdapter( 78 | ArrayList(), 79 | this::showChannel 80 | ) 81 | 82 | doAsync { refresh() } 83 | } 84 | 85 | private fun showChannel(channel: JSONObject) { 86 | val bundle = Bundle() 87 | bundle.putString("channel", channel.toString()) 88 | val fragment = ChannelFragment() 89 | fragment.arguments = bundle 90 | fragment.show(supportFragmentManager, "ChannelFragment") 91 | } 92 | 93 | private fun refresh() { 94 | try { 95 | val res = LightningCli().exec( 96 | this@ChannelsActivity, 97 | arrayOf("listpeers"), 98 | true 99 | ).toJSONObject() 100 | 101 | val channels = ArrayList() 102 | val peers = res["peers"] as JSONArray 103 | for (i in 0 until peers.length()) { 104 | val peer = peers.get(i) as? JSONObject 105 | val peerChannels = peer?.get("channels") as JSONArray 106 | for (j in 0 until peerChannels.length()) { 107 | val channel = peerChannels.get(j) as JSONObject 108 | channel.put("peer_id", peer.getString("id")) 109 | channels.add(channel) 110 | } 111 | } 112 | 113 | runOnUiThread { 114 | val total = channels.sumBy { it.getInt("msatoshi_to_us") / 1000 } 115 | findViewById(R.id.total_text).text = "$total sat in channels" 116 | val recyclerView = findViewById(R.id.recycler_view) 117 | val adapter = recyclerView.adapter as ChannelAdapter 118 | adapter.apply { 119 | list.clear() 120 | list.addAll(channels) 121 | notifyDataSetChanged() 122 | } 123 | } 124 | } catch (e: Exception) { 125 | runOnUiThread { 126 | Toast.makeText( 127 | this@ChannelsActivity, 128 | "Channel funded", 129 | Toast.LENGTH_LONG 130 | ).show() 131 | } 132 | } 133 | } 134 | 135 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 136 | // Inflate the menu; this adds items to the action bar if it is present. 137 | menuInflater.inflate(R.menu.menu_channels, menu) 138 | return true 139 | } 140 | 141 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 142 | return when (item.itemId) { 143 | R.id.action_add -> { 144 | val bottomSheetDialog = 145 | FundChannelFragment() 146 | bottomSheetDialog.show(supportFragmentManager, "Fund channel") 147 | true 148 | } 149 | else -> super.onOptionsItemSelected(item) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/activities/ConsoleActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.activities 2 | 3 | import android.os.AsyncTask 4 | import android.os.Bundle 5 | import android.widget.EditText 6 | import android.widget.ImageButton 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.lvaccaro.lamp.LightningCli 9 | import com.lvaccaro.lamp.R 10 | import com.lvaccaro.lamp.toText 11 | import java.lang.Exception 12 | 13 | class ConsoleActivity : AppCompatActivity() { 14 | 15 | private lateinit var editTextResult: EditText 16 | private lateinit var editTextCmd: EditText 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | setContentView(R.layout.activity_console) 21 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 22 | 23 | editTextCmd = findViewById(R.id.edit_text_console_message) 24 | editTextResult = findViewById(R.id.edit_text_result_command) 25 | 26 | findViewById(R.id.send).setOnClickListener { 27 | val textContent = editTextCmd.text.toString() 28 | if (textContent != "") { 29 | if (textContent.equals("clean", true)) { 30 | // Command to clean console 31 | editTextResult.setText("") 32 | editTextCmd.setText("") 33 | } else { 34 | CommandTask().execute(textContent) 35 | } 36 | } 37 | } 38 | } 39 | 40 | inner class CommandTask : AsyncTask() { 41 | 42 | lateinit var params: String 43 | override fun onPreExecute() { 44 | super.onPreExecute() 45 | editTextCmd.setText("") 46 | } 47 | 48 | override fun doInBackground(vararg params: String): String { 49 | this.params = params[0] 50 | val args = params[0].split(" ").toTypedArray() 51 | try { 52 | return LightningCli() 53 | .exec(this@ConsoleActivity, args, true).toText() 54 | } catch (e: Exception) { 55 | e.printStackTrace() 56 | return e.localizedMessage ?: "Error, params: $args" 57 | } 58 | } 59 | 60 | override fun onPostExecute(result: String?) { 61 | super.onPostExecute(result) 62 | editTextResult.append("$ lightning-cli $params\n") 63 | editTextResult.append(result ?: "") 64 | editTextResult.append("\n") 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/activities/LogActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.text.method.ScrollingMovementMethod 6 | import android.util.Log 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import android.view.View 10 | import android.widget.EditText 11 | import android.widget.ProgressBar 12 | import android.widget.Toast 13 | import androidx.appcompat.app.AppCompatActivity 14 | import com.lvaccaro.lamp.R 15 | import com.lvaccaro.lamp.rootDir 16 | import com.lvaccaro.lamp.utils.UI 17 | import kotlinx.android.synthetic.main.activity_log.* 18 | import org.jetbrains.anko.doAsync 19 | import java.io.File 20 | import java.io.RandomAccessFile 21 | 22 | class LogActivity : AppCompatActivity() { 23 | 24 | companion object { 25 | val TAG = LogActivity::class.java.canonicalName 26 | } 27 | 28 | private var daemon = "lightningd" 29 | private val maxBufferToLoad = 200 30 | private var sizeBuffer = 0 31 | 32 | // UI component 33 | private lateinit var editText: EditText 34 | private lateinit var progressBar: ProgressBar 35 | 36 | override fun onCreate(savedInstanceState: Bundle?) { 37 | super.onCreate(savedInstanceState) 38 | setContentView(R.layout.activity_log) 39 | setSupportActionBar(toolbar) 40 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 41 | editText = findViewById(R.id.edit_text_container_log) 42 | editText.apply { 43 | movementMethod = ScrollingMovementMethod() 44 | isVerticalScrollBarEnabled = true 45 | } 46 | progressBar = findViewById(R.id.loading_status) 47 | progressBar.max = maxBufferToLoad 48 | readLog() 49 | } 50 | 51 | override fun onResume() { 52 | super.onResume() 53 | readLog() 54 | } 55 | 56 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 57 | menuInflater.inflate(R.menu.menu_log, menu) 58 | return true 59 | } 60 | 61 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 62 | return when (item.itemId) { 63 | R.id.action_lightning -> { 64 | daemon = "lightningd" 65 | readLog() 66 | true 67 | } 68 | R.id.action_tor -> { 69 | daemon = "tor" 70 | readLog() 71 | true 72 | } 73 | R.id.action_share_log -> { 74 | shareLogByIntent() 75 | true 76 | } 77 | else -> super.onOptionsItemSelected(item) 78 | } 79 | } 80 | 81 | private fun shareLogByIntent() { 82 | doAsync { 83 | val shareIntent = Intent(Intent.ACTION_SEND).apply { 84 | type = "text/plain" 85 | val logFile = File(rootDir(), "$daemon.log") 86 | if (!logFile.exists()) { 87 | runOnUiThread { 88 | UI.showMessageOnToast(applicationContext, "No log file found") 89 | } 90 | return@doAsync 91 | } 92 | val body = StringBuilder() 93 | body.append("------- LOG $daemon.log CONTENT ----------").append("\n") 94 | val lines = logFile.readLines() 95 | val sizeNow = lines.size 96 | var difference = 0 97 | if (sizeNow > 450) sizeNow - 200 98 | for (at in difference until sizeNow) { 99 | val line = lines[at] 100 | body.append(line).append("\n") 101 | } 102 | putExtra(Intent.EXTRA_TEXT, body.toString()) 103 | } 104 | if (shareIntent.resolveActivity(packageManager) != null) { 105 | startActivity(Intent.createChooser(shareIntent, null)) 106 | return@doAsync 107 | } 108 | runOnUiThread { 109 | UI.showMessageOnToast(applicationContext, "Intent resolving error") 110 | } 111 | } 112 | } 113 | 114 | private fun readLog() { 115 | title = "Log $daemon" 116 | val logFile = File(rootDir(), "$daemon.log") 117 | if (!logFile.exists()) { 118 | UI.showMessageOnToast(this, "No log file found") 119 | return 120 | } 121 | editText.setText("") 122 | doAsync { 123 | runOnUiThread { 124 | Toast.makeText(this@LogActivity, "Loading", Toast.LENGTH_SHORT).show() 125 | progressBar.visibility = View.VISIBLE 126 | } 127 | val randomAccessFile = RandomAccessFile(logFile, "r") 128 | read(randomAccessFile, editText) 129 | } 130 | } 131 | 132 | private fun read(randomAccessFile: RandomAccessFile, et: EditText) { 133 | Log.d(TAG, "Start to read the file with RandomAccessFile") 134 | // Set the position at the end of the file 135 | val fileSize = randomAccessFile.length() - 1 136 | randomAccessFile.seek(fileSize) 137 | // The maximum dimension of this object is one line 138 | val lineBuilder = StringBuilder() 139 | // This contains the each line of the logger, the line of the logger are fixed 140 | // to the propriety *maxBufferToLoad* 141 | val logBuilder = StringBuilder() 142 | for (pointer in fileSize downTo 1) { 143 | randomAccessFile.seek(pointer) 144 | val character = randomAccessFile.read().toChar() 145 | lineBuilder.append(character) 146 | if (character.equals('\n', false)) { 147 | sizeBuffer++ 148 | logBuilder.append(lineBuilder.reverse().toString()) 149 | lineBuilder.clear() 150 | runOnUiThread { 151 | this.progressBar.progress = sizeBuffer 152 | } 153 | if (sizeBuffer == maxBufferToLoad) break 154 | } 155 | } 156 | Log.d(TAG, "Print lines to EditText") 157 | val lines = logBuilder.toString().split("\n").reversed() 158 | runOnUiThread { 159 | lines.forEach { 160 | if (it.trim().isNotEmpty() && it.length < 400) 161 | et.append(it.plus("\n")) 162 | } 163 | progressBar.visibility = View.GONE 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/activities/ScanActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.activities 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.ClipboardManager 6 | import android.content.Context 7 | import android.content.pm.PackageManager 8 | import android.os.Build 9 | import android.os.Bundle 10 | import android.util.Log 11 | import android.view.Menu 12 | import android.view.MenuItem 13 | import androidx.appcompat.app.AppCompatActivity 14 | import com.google.zxing.Result 15 | import com.lvaccaro.lamp.R 16 | import me.dm7.barcodescanner.zxing.ZXingScannerView 17 | 18 | class ScanActivity : AppCompatActivity(), ZXingScannerView.ResultHandler { 19 | private val TAG = "ScanActivity" 20 | lateinit var mScannerView: ZXingScannerView 21 | 22 | public override fun onCreate(state: Bundle?) { 23 | super.onCreate(state) 24 | supportActionBar?.setDisplayShowHomeEnabled(true) 25 | supportActionBar?.setDisplayHomeAsUpEnabled(true) 26 | 27 | mScannerView = ZXingScannerView(this) 28 | setContentView(mScannerView) 29 | mScannerView.setAutoFocus(true) 30 | mScannerView.setAspectTolerance(0.5f) 31 | 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && 33 | checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED 34 | ) { 35 | requestPermissions((arrayOf(Manifest.permission.CAMERA)), 101) 36 | return 37 | } 38 | } 39 | 40 | public override fun onResume() { 41 | super.onResume() 42 | mScannerView.setResultHandler(this) // Register ourselves as a handler for scan results. 43 | mScannerView.startCamera() 44 | } 45 | 46 | public override fun onPause() { 47 | super.onPause() 48 | mScannerView.stopCamera() 49 | } 50 | 51 | override fun onStop() { 52 | super.onStop() 53 | mScannerView.stopCamera() 54 | } 55 | 56 | override fun onRequestPermissionsResult( 57 | requestCode: Int, 58 | permissions: Array, 59 | grantResults: IntArray 60 | ) { 61 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 62 | if (requestCode == 101 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 63 | mScannerView?.startCamera() 64 | } 65 | } 66 | 67 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 68 | menuInflater.inflate(R.menu.menu_scan, menu) 69 | return true 70 | } 71 | 72 | override fun handleResult(rawResult: Result?) { 73 | Log.d(TAG, rawResult?.text) 74 | val result = rawResult?.text ?: "" 75 | if (result.isEmpty()) { 76 | mScannerView.resumeCameraPreview(this) 77 | return 78 | } 79 | 80 | runOnUiThread { 81 | val intent = intent 82 | intent.putExtra("text", result) 83 | setResult(Activity.RESULT_OK, intent) 84 | finish() 85 | } 86 | } 87 | 88 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 89 | return when (item.itemId) { 90 | android.R.id.home -> { 91 | onBackPressed() 92 | return true 93 | } 94 | R.id.action_paste -> { 95 | val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 96 | val clip = clipboard.primaryClip 97 | val item = clip?.getItemAt(0) 98 | val text = item?.text.toString() 99 | val intent = intent 100 | intent.putExtra("text", text) 101 | setResult(Activity.RESULT_OK, intent) 102 | finish() 103 | true 104 | } 105 | else -> super.onOptionsItemSelected(item) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/activities/UriResultActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.activities 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.appcompat.app.AlertDialog 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.lvaccaro.lamp.LightningCli 10 | import com.lvaccaro.lamp.fragments.FundChannelFragment 11 | import com.lvaccaro.lamp.fragments.WithdrawFragment 12 | import com.lvaccaro.lamp.services.CLightningException 13 | import com.lvaccaro.lamp.toJSONObject 14 | import com.lvaccaro.lamp.utils.LampKeys 15 | import com.lvaccaro.lamp.utils.Validator 16 | import org.json.JSONObject 17 | import java.lang.Exception 18 | 19 | open class UriResultActivity() : AppCompatActivity() { 20 | 21 | val cli = LightningCli() 22 | val TAG = "UriResultActivity" 23 | 24 | fun parse(text: String) { 25 | // Check is if a Bitcoin payment 26 | val isBitcoinAddress = Validator.isBitcoinAddress(text) 27 | val isBitcoinURI = Validator.isBitcoinURL(text) 28 | val isBoltPayment = Validator.isBolt11(text) 29 | val isURINodeConnect = Validator.isLightningNodURI(text) 30 | var resultCommand: JSONObject? = null 31 | try { 32 | if (isBitcoinAddress) { 33 | val parm = HashMap() 34 | Log.d(TAG, "*** Bitcoin address") 35 | parm.put(LampKeys.ADDRESS_KEY, text) 36 | runOnUiThread { 37 | showWithdraw(parm) 38 | } 39 | } else if (isBitcoinURI) { 40 | Log.d(TAG, "*** Bitcoin URI") 41 | val result = Validator.doParseBitcoinURL(text) 42 | runOnUiThread { showWithdraw(result) } 43 | } else if (isBoltPayment) { 44 | Log.d(TAG, "*** Bolt payment") 45 | val bolt11 = Validator.getBolt11(text) 46 | runOnUiThread { showDecodePay(bolt11) } 47 | } else if (isURINodeConnect) { 48 | Log.d(TAG, "*** Node URI connect $text") 49 | resultCommand = runCommandCLightning(LampKeys.CONNECT_COMMAND, arrayOf(text)) 50 | runOnUiThread { showConnect(resultCommand!!["id"].toString()) } 51 | } else { 52 | resultCommand = JSONObject() 53 | resultCommand.put("message", "No action found") 54 | } 55 | } catch (ex: CLightningException) { 56 | // FIXME: This have sense? 57 | Log.e(TAG, ex.localizedMessage) 58 | resultCommand = JSONObject(ex.localizedMessage) 59 | ex.printStackTrace() 60 | } finally { 61 | if (resultCommand == null) { 62 | return 63 | } 64 | var message = "" 65 | if (resultCommand.has(LampKeys.MESSAGE_JSON_KEY)) { 66 | message = resultCommand.get(LampKeys.MESSAGE_JSON_KEY).toString() 67 | } else if (resultCommand.has("id")) { 68 | message = "Connected to node" 69 | } 70 | runOnUiThread { 71 | showMessageOnToast( 72 | message, 73 | Toast.LENGTH_LONG 74 | ) 75 | } 76 | } 77 | } 78 | 79 | fun runCommandCLightning(command: String, parameter: Array): JSONObject { 80 | try { 81 | val payload = ArrayList() 82 | payload.add(command) 83 | payload.addAll(parameter) 84 | payload.forEach { Log.d(TAG, "***** $it") } 85 | val rpcResult = 86 | cli.exec(this, payload.toTypedArray()).toJSONObject() 87 | Log.d(TAG, rpcResult.toString()) 88 | return rpcResult 89 | } catch (ex: Exception) { 90 | // FIXME: This have sense? 91 | val answer = JSONObject(ex.localizedMessage) 92 | showMessageOnToast(answer[LampKeys.MESSAGE_JSON_KEY].toString(), Toast.LENGTH_LONG) 93 | throw CLightningException(ex.cause) 94 | } 95 | } 96 | 97 | private fun showDecodePay(bolt11: String) { 98 | val intent = Intent(this, SendActivity::class.java) 99 | intent.putExtra("bolt11", bolt11) 100 | startActivity(intent) 101 | } 102 | 103 | private fun showConnect(id: String) { 104 | AlertDialog.Builder(this) 105 | .setTitle("connect") 106 | .setMessage(id) 107 | .setPositiveButton("fund channel") { _, _ -> 108 | // Open fund channel fragment 109 | val bottomSheetDialog = 110 | FundChannelFragment() 111 | val args = Bundle() 112 | args.putString("uri", id) 113 | bottomSheetDialog.arguments = args 114 | bottomSheetDialog.show(supportFragmentManager, "Fund channel") 115 | } 116 | .setNegativeButton("cancel") { _, _ -> } 117 | .show() 118 | } 119 | 120 | private fun showWithdraw(param: HashMap?) { 121 | val bottomSheetDialog = WithdrawFragment() 122 | val bundle = Bundle() 123 | val address = param?.get(LampKeys.ADDRESS_KEY) ?: "" 124 | val networkCheck = Validator.isCorrectNetwork(cli, this.applicationContext, address) 125 | if (networkCheck != null) { 126 | showMessageOnToast(networkCheck, Toast.LENGTH_LONG) 127 | return 128 | } 129 | var amount = "" 130 | if (param!!.contains(LampKeys.AMOUNT_KEY)) { 131 | // FIXME(vincenzopalazzo): create a converted class to set the set the correct ammounet. 132 | // For instance, Validator.toMilliSatoshi() 133 | amount = (param!![LampKeys.AMOUNT_KEY]!!.toDouble() * 100000000).toLong().toString() 134 | } 135 | bundle.putString(LampKeys.ADDRESS_KEY, address) 136 | bundle.putString(LampKeys.AMOUNT_KEY, amount) 137 | bottomSheetDialog.arguments = bundle 138 | bottomSheetDialog.show(supportFragmentManager, "WithdrawFragment") 139 | } 140 | 141 | protected fun showMessageOnToast(message: String, duration: Int = Toast.LENGTH_LONG) { 142 | if (message.isEmpty()) return 143 | runOnUiThread { 144 | Toast.makeText( 145 | this, message, 146 | duration 147 | ).show() 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/adapters/BalanceAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.lvaccaro.lamp.R 8 | 9 | data class Balance(val title: String, val subtitle: String, val value: String) 10 | 11 | typealias BalanceClickListener = (Int) -> Unit 12 | 13 | class BalanceAdapter( 14 | val list: ArrayList, 15 | private val onClickListener: BalanceClickListener? 16 | ) : 17 | RecyclerView.Adapter() { 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BalanceViewHolder { 20 | val inflater = LayoutInflater.from(parent.context) 21 | return BalanceViewHolder(inflater, parent) 22 | } 23 | 24 | override fun onBindViewHolder(holder: BalanceViewHolder, position: Int) { 25 | val item: Balance = list[position] 26 | holder.bind(item.title, item.subtitle, item.value) 27 | holder.itemView.setOnClickListener { onClickListener?.invoke(position) } 28 | } 29 | 30 | override fun getItemCount(): Int = list.size 31 | } 32 | 33 | class BalanceViewHolder(inflater: LayoutInflater, parent: ViewGroup) : 34 | RecyclerView.ViewHolder(inflater.inflate(R.layout.list_balance, parent, false)) { 35 | fun bind(title: String, subtitle: String, value: String) { 36 | itemView.findViewById(R.id.title).text = title 37 | itemView.findViewById(R.id.subtitle).text = subtitle 38 | itemView.findViewById(R.id.value).text = value 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/adapters/HashMapAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import android.widget.TextView 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.lvaccaro.lamp.utils.UI 8 | import org.json.JSONObject 9 | 10 | class HashMapAdapter(val map: LinkedHashMap) : 11 | RecyclerView.Adapter() { 12 | 13 | class ViewHolder(inflater: LayoutInflater, parent: ViewGroup) : 14 | RecyclerView.ViewHolder(inflater.inflate(android.R.layout.two_line_list_item, parent, false)) { 15 | 16 | fun bind(key: String, value: String) { 17 | itemView.findViewById(android.R.id.text1).text = key 18 | itemView.findViewById(android.R.id.text2).text = value 19 | } 20 | } 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 23 | val inflater = LayoutInflater.from(parent.context) 24 | return ViewHolder( 25 | inflater, 26 | parent 27 | ) 28 | } 29 | 30 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 31 | val key = map.keys.toList()[position] 32 | val value = map.values.toList()[position] 33 | holder.bind(key, value) 34 | holder.itemView.setOnClickListener { 35 | UI.copyToClipboard( 36 | holder.itemView.context, 37 | key, 38 | value 39 | ) 40 | } 41 | } 42 | 43 | override fun getItemCount(): Int = map.count() 44 | 45 | companion object { 46 | fun from(json: JSONObject): LinkedHashMap { 47 | val temp = json.keys() 48 | val hashMap = LinkedHashMap() 49 | while (temp.hasNext()) { 50 | val key = temp.next() 51 | val value = json[key].toString() 52 | hashMap.put(key, value) 53 | } 54 | return hashMap 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/lvaccaro/lamp/fragments/ChannelFragment.kt: -------------------------------------------------------------------------------- 1 | package com.lvaccaro.lamp.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.Button 8 | import android.widget.Toast 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import androidx.recyclerview.widget.RecyclerView 11 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 12 | import com.lvaccaro.lamp.LightningCli 13 | import com.lvaccaro.lamp.R 14 | import com.lvaccaro.lamp.adapters.HashMapAdapter 15 | import com.lvaccaro.lamp.toJSONObject 16 | import com.lvaccaro.lamp.utils.UI 17 | import org.jetbrains.anko.doAsync 18 | import org.json.JSONObject 19 | import java.lang.Exception 20 | 21 | class ChannelFragment : BottomSheetDialogFragment() { 22 | 23 | override fun onCreateView( 24 | inflater: LayoutInflater, 25 | container: ViewGroup?, 26 | savedInstanceState: Bundle? 27 | ): View? { 28 | val view = inflater.inflate(R.layout.fragment_channel, container, false) 29 | val data = arguments?.getString("channel") ?: "" 30 | val channel = JSONObject(data) 31 | 32 | val recyclerView = view.findViewById(R.id.recycler_view) 33 | recyclerView.apply { 34 | layoutManager = LinearLayoutManager(context) 35 | adapter = HashMapAdapter( 36 | HashMapAdapter.from(channel) 37 | ) 38 | } 39 | 40 | val closeButton = view.findViewById