├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── assets
│ └── license.txt
│ ├── kotlin
│ └── app
│ │ └── trigger
│ │ ├── AboutActivity.kt
│ │ ├── AbstractCertificateActivity.kt
│ │ ├── AbstractClientKeyPairActivity.kt
│ │ ├── BackupActivity.kt
│ │ ├── BluetoothDoor.kt
│ │ ├── BluetoothTools.kt
│ │ ├── Door.kt
│ │ ├── DoorReply.kt
│ │ ├── DoorStatus.kt
│ │ ├── HttpsDoor.kt
│ │ ├── ImageActivity.kt
│ │ ├── LicenseActivity.kt
│ │ ├── Log.kt
│ │ ├── MainActivity.kt
│ │ ├── MqttDoor.kt
│ │ ├── NukiDoor.kt
│ │ ├── OnTaskCompleted.kt
│ │ ├── QRScanActivity.kt
│ │ ├── QRShowActivity.kt
│ │ ├── Settings.kt
│ │ ├── SetupActivity.kt
│ │ ├── SshDoor.kt
│ │ ├── Utils.kt
│ │ ├── WifiTools.kt
│ │ ├── bluetooth
│ │ └── BluetoothRequestHandler.kt
│ │ ├── https
│ │ ├── CertificateFetchTask.kt
│ │ ├── HttpsClientCertificateActivity.kt
│ │ ├── HttpsClientKeyPairActivity.kt
│ │ ├── HttpsRequestHandler.kt
│ │ ├── HttpsServerCertificateActivity.kt
│ │ ├── HttpsTools.kt
│ │ └── IgnoreExpirationTrustManager.kt
│ │ ├── mqtt
│ │ ├── MqttClientCertificateActivity.kt
│ │ ├── MqttClientKeyPairActivity.kt
│ │ ├── MqttRequestHandler.kt
│ │ └── MqttServerCertificateActivity.kt
│ │ ├── nuki
│ │ ├── NukiCallback.kt
│ │ ├── NukiCommand.kt
│ │ ├── NukiLockActionCallback.kt
│ │ ├── NukiPairingCallback.kt
│ │ ├── NukiReadLockStateCallback.kt
│ │ ├── NukiRequestHandler.kt
│ │ └── NukiTools.kt
│ │ └── ssh
│ │ ├── EcCore.kt
│ │ ├── Encryptor.kt
│ │ ├── GenerateIdentityTask.kt
│ │ ├── KeyPairBean.kt
│ │ ├── PubkeyUtils.kt
│ │ ├── RegisterIdentityTask.kt
│ │ ├── SshKeyPairActivity.kt
│ │ ├── SshRequestHandler.kt
│ │ └── SshTools.kt
│ └── res
│ ├── anim
│ └── pressed.xml
│ ├── drawable-hdpi
│ ├── ic_action_about.png
│ ├── ic_action_edit.png
│ └── ic_action_new.png
│ ├── drawable-mdpi
│ ├── ic_action_about.png
│ ├── ic_action_edit.png
│ └── ic_action_new.png
│ ├── drawable-xhdpi
│ ├── ic_action_about.png
│ ├── ic_action_edit.png
│ ├── ic_action_new.png
│ ├── ic_action_refresh.png
│ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ ├── ic_action_about.png
│ ├── ic_action_edit.png
│ ├── ic_action_new.png
│ ├── ic_action_refresh.png
│ └── ic_launcher.png
│ ├── drawable
│ ├── ic_action_scan_qr.xml
│ ├── ic_action_show_qr.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_lock.xml
│ ├── ic_ring.xml
│ ├── ic_unlock.xml
│ ├── rounded_corners.xml
│ ├── state_closed.png
│ ├── state_disabled.png
│ ├── state_open.png
│ ├── state_unknown.png
│ └── trigger_logo.xml
│ ├── layout
│ ├── activity_about.xml
│ ├── activity_abstract_certificate.xml
│ ├── activity_abstract_client_keypair.xml
│ ├── activity_backup.xml
│ ├── activity_image.xml
│ ├── activity_license.xml
│ ├── activity_main.xml
│ ├── activity_qrscan.xml
│ ├── activity_qrshow.xml
│ ├── activity_setup_bluetooth.xml
│ ├── activity_setup_https.xml
│ ├── activity_setup_mqtt.xml
│ ├── activity_setup_nuki.xml
│ ├── activity_setup_ssh.xml
│ ├── activity_ssh_keypair.xml
│ ├── dialog_change_string.xml
│ ├── dialog_delete_door.xml
│ ├── dialog_ssh_passphrase.xml
│ ├── main_spinner.xml
│ ├── spinner_dropdown_item_settings.xml
│ └── spinner_item_settings.xml
│ ├── menu
│ └── main.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
│ ├── values-de
│ ├── array.xml
│ └── strings.xml
│ ├── values-in
│ └── strings.xml
│ ├── values-ru
│ └── strings.xml
│ ├── values-w820dp
│ └── dimens.xml
│ └── values
│ ├── array.xml
│ ├── dimens.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── default_images.svg
├── docs
├── apk.png
├── documentation.md
├── fdroid.png
├── gplay.png
├── screenshot_bluetooth_settings_part1.png
├── screenshot_door_types.png
├── screenshot_https_manage_tls_certificate.png
├── screenshot_https_settings_part1.png
├── screenshot_https_settings_part2.png
├── screenshot_main_menu.png
├── screenshot_mqtt_settings_part1.png
├── screenshot_nuki_settings_part1.png
├── screenshot_ssh_key_pair.png
├── screenshot_ssh_settings_part1.png
├── screenshot_ssh_settings_part2.png
├── screenshot_states.png
└── screenshots.md
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── metadata
├── en-US
│ ├── changelogs
│ │ ├── 170.txt
│ │ ├── 171.txt
│ │ ├── 180.txt
│ │ ├── 190.txt
│ │ ├── 191.txt
│ │ ├── 192.txt
│ │ ├── 200.txt
│ │ ├── 201.txt
│ │ ├── 202.txt
│ │ ├── 203.txt
│ │ ├── 204.txt
│ │ ├── 205.txt
│ │ ├── 206.txt
│ │ ├── 210.txt
│ │ ├── 211.txt
│ │ ├── 220.txt
│ │ ├── 221.txt
│ │ ├── 222.txt
│ │ ├── 223.txt
│ │ ├── 224.txt
│ │ ├── 225.txt
│ │ ├── 300.txt
│ │ ├── 301.txt
│ │ ├── 310.txt
│ │ ├── 311.txt
│ │ ├── 312.txt
│ │ ├── 313.txt
│ │ ├── 320.txt
│ │ ├── 321.txt
│ │ ├── 322.txt
│ │ ├── 330.txt
│ │ ├── 331.txt
│ │ ├── 332.txt
│ │ ├── 333.txt
│ │ ├── 334.txt
│ │ ├── 335.txt
│ │ ├── 336.txt
│ │ ├── 340.txt
│ │ ├── 341.txt
│ │ ├── 342.txt
│ │ ├── 343.txt
│ │ ├── 344.txt
│ │ ├── 400.txt
│ │ ├── 401.txt
│ │ ├── 402.txt
│ │ ├── 403.txt
│ │ ├── 404.txt
│ │ ├── 405.txt
│ │ ├── 406.txt
│ │ └── 407.txt
│ ├── full_description.txt
│ ├── images
│ │ ├── icon.png
│ │ └── phoneScreenshots
│ │ │ ├── 01_setup.png
│ │ │ ├── 02_settings_https_part1.png
│ │ │ └── 03_settings_https_part2.png
│ └── short_description.txt
└── ru-RU
│ ├── full_description.txt
│ └── short_description.txt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Trigger
2 | =======
3 |
4 | Trigger is an Android App to unlock/lock doors, show the door status and ring a bell.
5 |
6 | Features:
7 | - Support of HTTPS, SSH, Bluetooth/BLE and MQTT.
8 | - Support for Nuki SmartLock.
9 | - Multiple door profiles.
10 | - Auto select profiles by connected WiFi.
11 | - HTTPS server/client certificate support.
12 | - SSH key generation support (ED25519, RSA).
13 | - Custom status images.
14 | - QR code support.
15 | - Support of backup.
16 |
17 | 
18 |
19 | (more [Screenshots](docs/screenshots.md))
20 |
21 |
22 | ## Download
23 |
24 | [
](https://f-droid.org/packages/com.example.trigger/)
25 | [
](https://github.com/mwarning/trigger/releases)
26 |
27 | The minimum supported Android version is 5.0.
28 |
29 | ## Documentation
30 |
31 | For further feature explanations and How-Tos, see the [Documentation](docs/documentation.md) page.
32 |
33 | ## Contribution
34 |
35 | Any help, bugfixes, new features, translations are much appreciated.
36 |
37 | For translations use https://toolate.othing.xyz/projects/trigger/
38 |
39 | ## Similar/Related Projects
40 |
41 | * [Sphincter-Remote](https://github.com/openlab-aux/Sphincter-Remote) / [Sphincter](https://github.com/openlab-aux/sphincter) / [Sphincterd](https://github.com/openlab-aux/sphincterd)
42 | * [D00r-app](https://github.com/h42i/d00r-app) / [D00r-key-server](https://github.com/h42i/d00r-key-server)
43 | * [labadoor](https://github.com/ToLABaki/labadoor) / [DoorLock](https://wiki.tolabaki.gr/w/DoorLock_v3)
44 | * [Krautschlüssel](https://gitlab.com/fiveop/krautschluessel)
45 | * [MetalabDoorWidget](https://github.com/zoff99/MetalabDoorWidget)
46 | * [HACKS](https://github.com/ktt-ol/hacs)
47 | * [Stratum0Widget](https://github.com/Valodim/Stratum0Widget)
48 |
49 | ## License
50 |
51 | This work is licenced under the GNU General Public License version 2 or later (GPLv2).
52 |
53 | Icons: [Googles Material Design](https://material.io/tools/icons/)
54 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | defaultConfig {
8 | minSdk 21
9 | targetSdk 35
10 | compileSdk 35
11 | versionCode 407
12 | versionName "4.0.7"
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 |
24 | flavorDimensions += "store"
25 |
26 | productFlavors {
27 | google {
28 | applicationId 'app.door_trigger'
29 | }
30 | fdroid {
31 | applicationId 'com.example.trigger'
32 | }
33 | }
34 | buildFeatures {
35 | buildConfig = true
36 | }
37 | compileOptions {
38 | sourceCompatibility = JavaVersion.VERSION_17
39 | targetCompatibility = JavaVersion.VERSION_17
40 | }
41 | kotlinOptions {
42 | jvmTarget = "17"
43 | }
44 | namespace 'app.trigger'
45 | }
46 |
47 | dependencies {
48 | implementation 'org.conscrypt:conscrypt-android:2.5.2'
49 | implementation 'org.connectbot:sshlib:2.2.19'
50 | implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
51 | implementation('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false }
52 | implementation 'androidx.documentfile:documentfile:1.0.1'
53 | implementation 'com.github.joshjdevl.libsodiumjni:libsodium-jni-aar:2.0.2'
54 | implementation 'com.google.zxing:core:3.4.1'
55 | implementation 'androidx.core:core-ktx:1.15.0'
56 | implementation 'androidx.appcompat:appcompat:1.7.0'
57 | implementation 'com.google.android.material:material:1.12.0'
58 | implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
59 | implementation 'androidx.navigation:navigation-fragment-ktx:2.8.5'
60 | implementation 'androidx.navigation:navigation-ui-ktx:2.8.5'
61 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
62 | implementation 'androidx.preference:preference-ktx:1.2.1'
63 | testImplementation 'junit:junit:4.13.2'
64 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
65 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
66 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
38 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
54 |
58 |
62 |
67 |
71 |
72 |
76 |
77 |
81 |
82 |
86 |
87 |
91 |
92 |
96 |
97 |
101 |
102 |
105 |
106 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/AboutActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.widget.TextView
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.appcompat.widget.Toolbar
8 |
9 | class AboutActivity : AppCompatActivity() {
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContentView(R.layout.activity_about)
13 | title = getString(R.string.menu_about)
14 |
15 | val toolbar = findViewById(R.id.toolbar)
16 | setSupportActionBar(toolbar)
17 |
18 | findViewById(R.id.versionTv).text = if (BuildConfig.DEBUG) {
19 | BuildConfig.VERSION_NAME + " (debug)"
20 | } else {
21 | BuildConfig.VERSION_NAME
22 | }
23 |
24 | findViewById(R.id.licenseTV).setOnClickListener {
25 | val intent = Intent(this, LicenseActivity::class.java)
26 | startActivity(intent)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/BackupActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.app.Activity
4 | import android.app.AlertDialog
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.os.Bundle
8 | import android.view.View
9 | import android.widget.Button
10 | import android.widget.Toast
11 | import androidx.activity.result.contract.ActivityResultContracts
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.appcompat.widget.Toolbar
14 | import org.json.JSONObject
15 |
16 | class BackupActivity : AppCompatActivity() {
17 | private lateinit var builder: AlertDialog.Builder
18 | private lateinit var exportButton: Button
19 | private lateinit var importButton: Button
20 |
21 | private fun showErrorMessage(title: String, message: String) {
22 | builder.setTitle(title)
23 | builder.setMessage(message)
24 | builder.setPositiveButton(android.R.string.ok, null)
25 | builder.show()
26 | }
27 |
28 | public override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | setContentView(R.layout.activity_backup)
31 |
32 | val toolbar = findViewById(R.id.toolbar)
33 | setSupportActionBar(toolbar)
34 |
35 | builder = AlertDialog.Builder(this)
36 |
37 | importButton = findViewById(R.id.ImportButton)
38 | exportButton = findViewById(R.id.ExportButton)
39 |
40 | importButton.setOnClickListener { v: View? ->
41 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
42 | intent.addCategory(Intent.CATEGORY_OPENABLE)
43 | intent.type = "application/json"
44 | importFileLauncher.launch(intent)
45 | }
46 |
47 | exportButton.setOnClickListener {
48 | val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
49 | intent.addCategory(Intent.CATEGORY_OPENABLE)
50 | intent.putExtra(Intent.EXTRA_TITLE, "trigger-backup.json")
51 | intent.type = "application/json"
52 | exportFileLauncher.launch(intent)
53 | }
54 | }
55 |
56 | private var importFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
57 | if (result.resultCode == Activity.RESULT_OK) {
58 | val intent = result.data ?: return@registerForActivityResult
59 | val uri = intent.data ?: return@registerForActivityResult
60 | importBackup(uri)
61 | }
62 | }
63 |
64 | private var exportFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
65 | if (result.resultCode == Activity.RESULT_OK) {
66 | val intent = result.data ?: return@registerForActivityResult
67 | val uri: Uri = intent.data ?: return@registerForActivityResult
68 | exportBackup(uri)
69 | }
70 | }
71 |
72 | private fun exportBackup(uri: Uri) {
73 | try {
74 | val obj = JSONObject()
75 | var count = 0
76 | for (door in Settings.getDoors()) {
77 | val json_obj = Settings.toJsonObject(door)
78 | json_obj!!.remove("id")
79 | obj.put(door.name, json_obj)
80 | count += 1
81 | }
82 | Utils.writeFile(this, uri, obj.toString().toByteArray())
83 | Toast.makeText(this, "Exported $count entries.", Toast.LENGTH_LONG).show()
84 | } catch (e: Exception) {
85 | showErrorMessage("Error", e.toString())
86 | }
87 | }
88 |
89 | private fun importBackup(uri: Uri) {
90 | try {
91 | val data = Utils.readFile(this, uri)
92 | val json_data = JSONObject(
93 | String(data, 0, data.size)
94 | )
95 | var count = 0
96 | val keys = json_data.keys()
97 | while (keys.hasNext()) {
98 | val key = keys.next()
99 | val obj = json_data.getJSONObject(key)
100 | obj.put("id", Settings.getNewDoorIdentifier())
101 | val door = Settings.fromJsonObject(obj)
102 | if (door != null) {
103 | Settings.storeDoorSetup(door)
104 | }
105 | count += 1
106 | }
107 | Toast.makeText(this, "Imported $count doors", Toast.LENGTH_LONG).show()
108 | } catch (e: Exception) {
109 | showErrorMessage("Error", e.toString())
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/BluetoothDoor.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import org.json.JSONObject
4 |
5 |
6 | class BluetoothDoor(override var id: Int, override var name: String) : Door() {
7 | override val type = Companion.TYPE
8 | var device_name = ""
9 | var service_uuid = ""
10 | var open_query = ""
11 | var close_query = ""
12 | var ring_query = ""
13 | var status_query = ""
14 | var locked_pattern = ""
15 | var unlocked_pattern = ""
16 |
17 | override fun getWiFiSSIDs(): String = ""
18 | override fun getWiFiRequired(): Boolean = false
19 |
20 | override fun parseReply(reply: DoorReply): DoorStatus {
21 | return Utils.genericDoorReplyParser(reply, unlocked_pattern, locked_pattern)
22 | }
23 |
24 | override fun isActionSupported(action: MainActivity.Action): Boolean {
25 | return when (action) {
26 | MainActivity.Action.OPEN_DOOR -> open_query.isNotEmpty()
27 | MainActivity.Action.CLOSE_DOOR -> close_query.isNotEmpty()
28 | MainActivity.Action.RING_DOOR -> ring_query.isNotEmpty()
29 | MainActivity.Action.FETCH_STATE -> status_query.isNotEmpty()
30 | }
31 | }
32 |
33 | fun toJSONObject(): JSONObject {
34 | val obj = JSONObject()
35 | obj.put("id", id)
36 | obj.put("name", name)
37 | obj.put("type", type)
38 |
39 | obj.put("device_name", device_name)
40 | obj.put("service_uuid", service_uuid)
41 | obj.put("open_query", open_query)
42 | obj.put("close_query", close_query)
43 | obj.put("ring_query", ring_query)
44 | obj.put("locked_pattern", locked_pattern)
45 | obj.put("unlocked_pattern", unlocked_pattern)
46 | obj.put("status_query", status_query)
47 |
48 | obj.put("open_image", Utils.serializeBitmap(open_image))
49 | obj.put("closed_image", Utils.serializeBitmap(closed_image))
50 | obj.put("unknown_image", Utils.serializeBitmap(unknown_image))
51 | obj.put("disabled_image", Utils.serializeBitmap(disabled_image))
52 |
53 | return obj
54 | }
55 |
56 | companion object {
57 | const val TYPE = "BluetoothDoorSetup"
58 |
59 | fun fromJSONObject(obj: JSONObject): BluetoothDoor {
60 | val id = obj.getInt("id")
61 | val name = obj.getString("name")
62 | val setup = BluetoothDoor(id, name)
63 |
64 | setup.device_name = obj.optString("device_name", "")
65 | setup.service_uuid = obj.optString("service_uuid", "")
66 | setup.open_query = obj.optString("open_query", "")
67 | setup.close_query = obj.optString("close_query", "")
68 | setup.ring_query = obj.optString("ring_query", "")
69 | setup.locked_pattern = obj.optString("locked_pattern", "")
70 | setup.unlocked_pattern = obj.optString("unlocked_pattern", "")
71 | setup.status_query = obj.optString("status_query", "")
72 |
73 | setup.open_image = Utils.deserializeBitmap(obj.optString("open_image", ""))
74 | setup.closed_image = Utils.deserializeBitmap(obj.optString("closed_image", ""))
75 | setup.unknown_image = Utils.deserializeBitmap(obj.optString("unknown_image", ""))
76 | setup.disabled_image = Utils.deserializeBitmap(obj.optString("disabled_image", ""))
77 |
78 | return setup
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/BluetoothTools.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.bluetooth.BluetoothAdapter
4 | import android.bluetooth.BluetoothDevice
5 | import android.bluetooth.BluetoothSocket
6 | import android.content.Context
7 | import java.lang.reflect.InvocationTargetException
8 |
9 | object BluetoothTools {
10 | private const val TAG = "BluetoothTools"
11 | private var adapter: BluetoothAdapter? = null
12 |
13 | fun init(context: Context?) {
14 | adapter = BluetoothAdapter.getDefaultAdapter()
15 | }
16 |
17 | fun isEnabled(): Boolean {
18 | return adapter != null && adapter!!.isEnabled
19 | }
20 |
21 | fun isSupported(): Boolean {
22 | return adapter != null
23 | }
24 |
25 | fun createRfcommSocket(device: BluetoothDevice): BluetoothSocket? {
26 | var tmp: BluetoothSocket? = null
27 | try {
28 | val class1: Class<*> = device.javaClass
29 | val aclass: Array> = arrayOf(Integer.TYPE as Class<*>)
30 | val method = class1.getMethod("createRfcommSocket", *aclass)
31 | val aobj = arrayOfNulls(1)
32 | aobj[0] = Integer.valueOf(1)
33 | tmp = method.invoke(device, *aobj) as BluetoothSocket
34 | } catch (e: NoSuchMethodException) {
35 | e.printStackTrace()
36 | Log.e(TAG, "createRfcommSocket() failed $e")
37 | } catch (e: InvocationTargetException) {
38 | e.printStackTrace()
39 | Log.e(TAG, "createRfcommSocket() failed $e")
40 | } catch (e: IllegalAccessException) {
41 | e.printStackTrace()
42 | Log.e(TAG, "createRfcommSocket() failed $e")
43 | }
44 | return tmp
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/Door.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.graphics.Bitmap
4 | import app.trigger.DoorStatus.StateCode
5 |
6 | abstract class Door {
7 | // internal id
8 | abstract var id: Int
9 |
10 | // Name of this setup for dropdown menu
11 | abstract var name: String
12 |
13 | // Door mechanism type name
14 | abstract val type: String
15 |
16 | var open_image: Bitmap? = null
17 | var closed_image: Bitmap? = null
18 | var unknown_image: Bitmap? = null
19 | var disabled_image: Bitmap? = null
20 |
21 | // Select setup entry from dropdown if it
22 | // matches any of these SSIDs (comma separated)
23 | abstract fun getWiFiSSIDs(): String
24 |
25 | // only applies for HTTPS, SSH and MQTT so far
26 | abstract fun getWiFiRequired(): Boolean
27 |
28 | // Get image dependent of the door state
29 | //fun getStateImage(state: StateCode?): Bitmap?
30 | fun getStateImage(state: StateCode?): Bitmap? {
31 | return when (state) {
32 | StateCode.OPEN -> open_image
33 | StateCode.CLOSED -> closed_image
34 | StateCode.DISABLED -> disabled_image
35 | StateCode.UNKNOWN -> unknown_image
36 | else -> null
37 | }
38 | }
39 |
40 | fun setStateImage(state: StateCode, bitmap: Bitmap?) {
41 | when (state) {
42 | StateCode.OPEN -> open_image = bitmap
43 | StateCode.CLOSED -> closed_image = bitmap
44 | StateCode.DISABLED -> disabled_image = bitmap
45 | StateCode.UNKNOWN -> unknown_image = bitmap
46 | }
47 | }
48 |
49 | // URL to fetch a https certificate from
50 | // or to send a ssh public key for registration
51 | open fun getRegisterUrl(): String = ""
52 |
53 | // Parse the text reply from
54 | // the door and determine state
55 | abstract fun parseReply(reply: DoorReply): DoorStatus
56 |
57 | // To show/hide respective buttons
58 | abstract fun isActionSupported(action: MainActivity.Action): Boolean
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/DoorReply.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | // reply from door
4 | class DoorReply(val action: MainActivity.Action, val code: ReplyCode, val message: String) {
5 | enum class ReplyCode {
6 | LOCAL_ERROR, // could establish a connection for some reason
7 | REMOTE_ERROR, // the door send some error
8 | SUCCESS, // the door send some message that has yet to be parsed
9 | DISABLED // Internet, WiFi or Bluetooth disabled or not supported
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/DoorStatus.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | // parsed door reply
4 | class DoorStatus(val code: StateCode, val message: String) {
5 | enum class StateCode {
6 | OPEN, CLOSED, UNKNOWN, DISABLED
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/HttpsDoor.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import app.trigger.https.HttpsTools
4 | import app.trigger.ssh.KeyPairBean
5 | import app.trigger.ssh.SshTools
6 | import org.json.JSONObject
7 | import java.security.cert.Certificate
8 |
9 | class HttpsDoor(override var id: Int, override var name: String) : Door() {
10 | override val type = Companion.TYPE
11 |
12 | var require_wifi = false
13 | var open_query = ""
14 | var open_method = "GET"
15 | var close_query = ""
16 | var close_method = "GET"
17 | var ring_query = ""
18 | var ring_method = "GET"
19 | var status_query = ""
20 | var status_method = "GET"
21 | var ssids = ""
22 |
23 | // regex to evaluate the door return message
24 | var unlocked_pattern = ""
25 | var locked_pattern = ""
26 |
27 | var server_certificate: Certificate? = null
28 | var client_certificate: Certificate? = null
29 | var client_keypair: KeyPairBean? = null
30 | var ignore_certificate = false
31 | var ignore_hostname_mismatch = false
32 | var ignore_expiration = false
33 |
34 | override fun getWiFiRequired(): Boolean = require_wifi
35 | override fun getWiFiSSIDs(): String = ssids
36 |
37 | // extract from known urls
38 | override fun getRegisterUrl(): String {
39 | return stripUrls(open_query, ring_query, close_query, status_query)
40 | }
41 |
42 | override fun parseReply(reply: DoorReply): DoorStatus {
43 | return Utils.genericDoorReplyParser(reply, unlocked_pattern, locked_pattern)
44 | }
45 |
46 | override fun isActionSupported(action: MainActivity.Action): Boolean {
47 | return when (action) {
48 | MainActivity.Action.OPEN_DOOR -> open_query.isNotEmpty()
49 | MainActivity.Action.CLOSE_DOOR -> close_query.isNotEmpty()
50 | MainActivity.Action.RING_DOOR -> ring_query.isNotEmpty()
51 | MainActivity.Action.FETCH_STATE -> status_query.isNotEmpty()
52 | }
53 | }
54 |
55 | fun toJSONObject(): JSONObject {
56 | val obj = JSONObject()
57 | obj.put("id", id)
58 | obj.put("name", name)
59 | obj.put("type", type)
60 | obj.put("require_wifi", require_wifi)
61 | obj.put("open_query", open_query)
62 | obj.put("open_method", open_method)
63 | obj.put("close_query", close_query)
64 | obj.put("close_method", close_method)
65 | obj.put("ring_query", ring_query)
66 | obj.put("ring_method", ring_method)
67 | obj.put("status_query", status_query)
68 | obj.put("status_method", status_method)
69 | obj.put("ssids", ssids)
70 | obj.put("unlocked_pattern", unlocked_pattern)
71 | obj.put("locked_pattern", locked_pattern)
72 |
73 | obj.put("open_image", Utils.serializeBitmap(open_image))
74 | obj.put("closed_image", Utils.serializeBitmap(closed_image))
75 | obj.put("unknown_image", Utils.serializeBitmap(unknown_image))
76 | obj.put("disabled_image", Utils.serializeBitmap(disabled_image))
77 |
78 | obj.put("server_certificate", HttpsTools.serializeCertificate(server_certificate))
79 | obj.put("client_certificate", HttpsTools.serializeCertificate(client_certificate))
80 | obj.put("client_keypair", SshTools.serializeKeyPair(client_keypair))
81 |
82 | obj.put("ignore_certificate", ignore_certificate)
83 | obj.put("ignore_hostname_mismatch", ignore_hostname_mismatch)
84 | obj.put("ignore_expiration", ignore_expiration)
85 |
86 | return obj
87 | }
88 |
89 | companion object {
90 | const val TYPE = "HttpsDoorSetup"
91 |
92 | fun fromJSONObject(obj: JSONObject): HttpsDoor {
93 | val id = obj.getInt("id")
94 | val name = obj.getString("name")
95 | val setup = HttpsDoor(id, name)
96 |
97 | val defaultMethod = obj.optString("method", "GET")
98 |
99 | setup.require_wifi = obj.optBoolean("require_wifi", false)
100 | setup.open_query = obj.optString("open_query", "")
101 | setup.open_method = obj.optString("open_method", defaultMethod)
102 | setup.close_query = obj.optString("close_query", "")
103 | setup.close_method = obj.optString("close_method", defaultMethod)
104 | setup.ring_query = obj.optString("ring_query", "")
105 | setup.ring_method = obj.optString("ring_method", defaultMethod)
106 | setup.status_query = obj.optString("status_query", "")
107 | setup.status_method = obj.optString("status_method", defaultMethod)
108 | setup.ssids = obj.optString("ssids", "")
109 | setup.unlocked_pattern = obj.optString("unlocked_pattern", "")
110 | setup.locked_pattern = obj.optString("locked_pattern", "")
111 | setup.open_image = Utils.deserializeBitmap(obj.optString("open_image", ""))
112 | setup.closed_image = Utils.deserializeBitmap(obj.optString("closed_image", ""))
113 | setup.unknown_image = Utils.deserializeBitmap(obj.optString("unknown_image", ""))
114 | setup.disabled_image = Utils.deserializeBitmap(obj.optString("disabled_image", ""))
115 | setup.server_certificate = HttpsTools.deserializeCertificate(obj.optString("server_certificate", ""))
116 | setup.client_certificate = HttpsTools.deserializeCertificate(obj.optString("client_certificate", ""))
117 | setup.client_keypair = SshTools.deserializeKeyPair(obj.optString("client_keypair", ""))
118 |
119 | setup.ignore_certificate = obj.optBoolean("ignore_certificate", false)
120 | setup.ignore_hostname_mismatch = obj.optBoolean("ignore_hostname_mismatch", false)
121 | setup.ignore_expiration = obj.optBoolean("ignore_expiration", false)
122 |
123 | return setup
124 | }
125 |
126 | private fun stripUrls(vararg urls: String): String {
127 | // remove path
128 | val prefix = "https://"
129 | for (url in urls) {
130 | if (url.startsWith(prefix)) {
131 | val i = url.indexOf('/', prefix.length)
132 | if (i > 0) {
133 | return url.substring(0, i)
134 | }
135 | }
136 | return url
137 | }
138 | return ""
139 | }
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/ImageActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.app.Activity
4 | import android.content.DialogInterface
5 | import android.content.Intent
6 | import android.graphics.Bitmap
7 | import android.graphics.BitmapFactory
8 | import android.net.Uri
9 | import android.os.Bundle
10 | import android.view.View
11 | import android.widget.Button
12 | import android.widget.ImageView
13 | import androidx.activity.result.contract.ActivityResultContracts
14 | import androidx.appcompat.app.AlertDialog
15 | import androidx.appcompat.app.AppCompatActivity
16 | import androidx.appcompat.widget.Toolbar
17 | import app.trigger.DoorStatus.StateCode
18 | import java.io.ByteArrayOutputStream
19 |
20 | class ImageActivity : AppCompatActivity() {
21 | private lateinit var door: Door
22 | private lateinit var stateCode: StateCode
23 |
24 | private lateinit var builder: AlertDialog.Builder
25 | private lateinit var setButton: Button
26 | private lateinit var selectButton: Button
27 | private lateinit var deleteButton: Button
28 | private lateinit var imageView: ImageView
29 | private var currentImage: Bitmap? = null
30 |
31 | private fun showErrorMessage(message: String) {
32 | builder.setTitle(R.string.error)
33 | builder.setMessage(message)
34 | builder.setPositiveButton(android.R.string.ok, null)
35 | builder.show()
36 | }
37 |
38 | public override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 | setContentView(R.layout.activity_image)
41 |
42 | val toolbar = findViewById(R.id.toolbar)
43 | setSupportActionBar(toolbar)
44 |
45 | // currentDoor might still not be stored if it is a new one
46 | val currentDoor = SetupActivity.currentDoor
47 | val codeString = intent.getStringExtra("state_code") ?: ""
48 | if (currentDoor == null || codeString.isEmpty()) {
49 | // not expected to happen
50 | finish()
51 | return
52 | }
53 |
54 | door = currentDoor
55 | stateCode = StateCode.valueOf(codeString)
56 | currentImage = door.getStateImage(stateCode)
57 |
58 | imageView = findViewById(R.id.selectedImage)
59 | builder = AlertDialog.Builder(this)
60 | setButton = findViewById(R.id.SetButton)
61 | selectButton = findViewById(R.id.SelectButton)
62 | deleteButton = findViewById(R.id.DeleteButton)
63 |
64 | selectButton.setOnClickListener { _: View? ->
65 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
66 | intent.addCategory(Intent.CATEGORY_OPENABLE)
67 | intent.type = "image/*"
68 | importImageLauncher.launch(intent)
69 | }
70 |
71 | setButton.setOnClickListener { _: View? ->
72 | // persist your value here
73 | door.setStateImage(stateCode, currentImage)
74 | finish()
75 | }
76 |
77 | deleteButton.setOnClickListener { _: View? ->
78 | builder.setTitle(R.string.confirm)
79 | builder.setMessage(R.string.dialog_really_remove_image)
80 | builder.setCancelable(false) // not necessary
81 | builder.setPositiveButton(R.string.yes) { dialog: DialogInterface, _: Int ->
82 | currentImage = null
83 | door.setStateImage(stateCode, null)
84 | updateImageView()
85 | dialog.cancel()
86 | }
87 | builder.setNegativeButton(R.string.no) { dialog: DialogInterface, _: Int -> dialog.cancel() }
88 |
89 | // create dialog box
90 | val alert = builder.create()
91 | alert.show()
92 | }
93 |
94 | updateImageView()
95 | }
96 |
97 | private fun updateImageView() {
98 | if (currentImage == null) {
99 | // show default image
100 | val defaultImageResource = when (stateCode) {
101 | StateCode.OPEN -> R.drawable.state_open
102 | StateCode.CLOSED -> R.drawable.state_closed
103 | StateCode.DISABLED -> R.drawable.state_disabled
104 | StateCode.UNKNOWN -> R.drawable.state_unknown
105 | }
106 | imageView.setImageBitmap(BitmapFactory.decodeResource(resources, defaultImageResource))
107 | deleteButton.isEnabled = false
108 | } else {
109 | imageView.setImageBitmap(currentImage)
110 | deleteButton.isEnabled = true
111 | }
112 | }
113 |
114 | private var importImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
115 | if (result.resultCode == Activity.RESULT_OK) {
116 | val intent = result.data ?: return@registerForActivityResult
117 | val uri = intent.data ?: return@registerForActivityResult
118 | updateImage(uri)
119 | }
120 | }
121 |
122 | private fun updateImage(uri: Uri) {
123 | val maxSize = 800
124 | try {
125 | val data = Utils.readFile(this, uri)
126 | var image = BitmapFactory.decodeByteArray(data, 0, data.size)
127 | if (image == null) {
128 | showErrorMessage("Not a supported image format: " + uri.lastPathSegment)
129 | return
130 | }
131 | val inWidth = image.width
132 | val inHeight = image.height
133 | var outWidth = 0
134 | var outHeight = 0
135 | if (inWidth > inHeight) {
136 | outWidth = maxSize
137 | outHeight = (inHeight * maxSize / inWidth.toFloat()).toInt()
138 | } else {
139 | outHeight = maxSize
140 | outWidth = (inWidth * maxSize / inHeight.toFloat()).toInt()
141 | }
142 | image = Bitmap.createScaledBitmap(image, outWidth, outHeight, false)
143 | val byteStream = ByteArrayOutputStream()
144 | val success = image.compress(Bitmap.CompressFormat.PNG, 0, byteStream)
145 | if (success) {
146 | //Log.d("ImageActivity", "image: " + inWidth + "/" + inHeight + ", compress.length: " + byteStream.toByteArray().length + ", base64: " + Base64.encodeToString(byteStream.toByteArray(), 0).length());
147 | currentImage = image
148 | } else {
149 | throw Exception("Cannot compress image")
150 | }
151 | updateImageView()
152 | } catch (e: Exception) {
153 | showErrorMessage(e.toString())
154 | }
155 | }
156 |
157 | companion object {
158 | private const val TAG = "ImageActivity"
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/LicenseActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.widget.ProgressBar
6 | import android.widget.TextView
7 | import androidx.appcompat.app.AppCompatActivity
8 | import androidx.appcompat.widget.Toolbar
9 | import java.io.BufferedReader
10 | import java.io.IOException
11 | import java.io.InputStreamReader
12 |
13 | class LicenseActivity : AppCompatActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_license)
17 | title = getString(R.string.title_license)
18 |
19 | val toolbar = findViewById(R.id.toolbar)
20 | setSupportActionBar(toolbar)
21 |
22 | // reading the license file can be slow => use a thread
23 | Thread {
24 | try {
25 | val buffer = StringBuffer()
26 | val reader = BufferedReader(InputStreamReader(assets.open("license.txt")))
27 | while (reader.ready()) {
28 | val line = reader.readLine()
29 | if (line != null) {
30 | if (line.trim().isEmpty()){
31 | buffer.append("\n")
32 | } else {
33 | buffer.append(line + "\n")
34 | }
35 | } else {
36 | break
37 | }
38 | }
39 | reader.close()
40 | runOnUiThread {
41 | findViewById(R.id.licenseLoadingBar).visibility = View.GONE
42 | findViewById(R.id.licenceText).text = buffer.toString()
43 | }
44 | } catch (e: IOException) {
45 | e.printStackTrace()
46 | }
47 | }.start()
48 | }
49 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/Log.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.util.Log
4 |
5 | /*
6 | * Wrapper for android.util.Log to disable logging
7 | */
8 | object Log {
9 | private fun contextString(context: Any): String {
10 | return if (context is String) {
11 | context
12 | } else {
13 | context.javaClass.simpleName
14 | }
15 | }
16 |
17 | fun d(context: Any, message: String) {
18 | if (BuildConfig.DEBUG) {
19 | val tag = contextString(context)
20 | Log.d(tag, message)
21 | }
22 | }
23 |
24 | fun w(context: Any, message: String) {
25 | if (BuildConfig.DEBUG) {
26 | val tag = contextString(context)
27 | Log.w(tag, message)
28 | }
29 | }
30 |
31 | fun i(context: Any, message: String) {
32 | if (BuildConfig.DEBUG) {
33 | val tag = contextString(context)
34 | Log.i(tag, message)
35 | }
36 | }
37 |
38 | fun e(context: Any, message: String) {
39 | if (BuildConfig.DEBUG) {
40 | val tag = contextString(context)
41 | Log.e(tag, message)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/MqttDoor.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import app.trigger.https.HttpsTools
4 | import app.trigger.ssh.KeyPairBean
5 | import app.trigger.ssh.SshTools
6 | import org.json.JSONObject
7 | import java.security.cert.Certificate
8 |
9 |
10 | class MqttDoor(override var id: Int, override var name: String) : Door() {
11 | override val type = Companion.TYPE
12 | var require_wifi = false
13 | var username = ""
14 | var password = ""
15 | var server = ""
16 | var status_topic = ""
17 | var command_topic = ""
18 | var retained = false
19 | var qos = 0
20 | var open_command = ""
21 | var close_command = ""
22 | var ring_command = ""
23 | var ssids = ""
24 | var locked_pattern = ""
25 | var unlocked_pattern = ""
26 |
27 | var server_certificate: Certificate? = null
28 | var client_certificate: Certificate? = null
29 | var client_keypair: KeyPairBean? = null
30 | var ignore_certificate = false
31 | var ignore_hostname_mismatch = false
32 | var ignore_expiration = false
33 |
34 | override fun getWiFiRequired(): Boolean = require_wifi
35 | override fun getWiFiSSIDs(): String = ssids
36 |
37 | override fun parseReply(reply: DoorReply): DoorStatus {
38 | return Utils.genericDoorReplyParser(reply, unlocked_pattern, locked_pattern)
39 | }
40 |
41 | override fun isActionSupported(action: MainActivity.Action): Boolean {
42 | return when (action) {
43 | MainActivity.Action.OPEN_DOOR -> open_command.isNotEmpty()
44 | MainActivity.Action.CLOSE_DOOR -> close_command.isNotEmpty()
45 | MainActivity.Action.RING_DOOR -> ring_command.isNotEmpty()
46 | MainActivity.Action.FETCH_STATE -> status_topic.isNotEmpty()
47 | }
48 | }
49 |
50 | fun toJSONObject(): JSONObject {
51 | val obj = JSONObject()
52 | obj.put("id", id)
53 | obj.put("name", name)
54 | obj.put("type", type)
55 |
56 | obj.put("require_wifi", require_wifi)
57 | obj.put("username", username)
58 | obj.put("password", password)
59 | obj.put("server", server)
60 | obj.put("status_topic", status_topic)
61 | obj.put("command_topic", command_topic)
62 | obj.put("retained", retained)
63 | obj.put("qos", qos)
64 |
65 | obj.put("open_command", open_command)
66 | obj.put("close_command", close_command)
67 | obj.put("ring_command", ring_command)
68 | obj.put("ssids", ssids)
69 |
70 | obj.put("unlocked_pattern", unlocked_pattern)
71 | obj.put("locked_pattern", locked_pattern)
72 | obj.put("open_image", Utils.serializeBitmap(open_image))
73 | obj.put("closed_image", Utils.serializeBitmap(closed_image))
74 | obj.put("unknown_image", Utils.serializeBitmap(unknown_image))
75 | obj.put("disabled_image", Utils.serializeBitmap(disabled_image))
76 |
77 | obj.put("server_certificate", HttpsTools.serializeCertificate(server_certificate))
78 | obj.put("client_certificate", HttpsTools.serializeCertificate(client_certificate))
79 | obj.put("client_keypair", SshTools.serializeKeyPair(client_keypair))
80 |
81 | obj.put("ignore_certificate", ignore_certificate)
82 | obj.put("ignore_hostname_mismatch", ignore_hostname_mismatch)
83 | obj.put("ignore_expiration", ignore_expiration)
84 |
85 | return obj
86 | }
87 |
88 | companion object {
89 | const val TYPE = "MqttDoorSetup"
90 |
91 | fun fromJSONObject(obj: JSONObject): MqttDoor {
92 | val id = obj.getInt("id")
93 | val name = obj.getString("name")
94 | val setup = MqttDoor(id, name)
95 |
96 | setup.require_wifi = obj.optBoolean("require_wifi", false)
97 | setup.username = obj.optString("username", "")
98 | setup.password = obj.optString("password", "")
99 | setup.server = obj.optString("server", "")
100 | setup.status_topic = obj.optString("status_topic", "")
101 | setup.command_topic = obj.optString("command_topic", "")
102 | setup.retained = obj.optBoolean("retained", false)
103 | setup.qos = obj.optInt("qos", 0)
104 | setup.open_command = obj.optString("open_command", "")
105 | setup.close_command = obj.optString("close_command", "")
106 | setup.ring_command = obj.optString("ring_command", "")
107 | setup.ssids = obj.optString("ssids", "")
108 |
109 | setup.unlocked_pattern = obj.optString("unlocked_pattern", "")
110 | setup.locked_pattern = obj.optString("locked_pattern", "")
111 |
112 | setup.open_image = Utils.deserializeBitmap(obj.optString("open_image", ""))
113 | setup.closed_image = Utils.deserializeBitmap(obj.optString("closed_image", ""))
114 | setup.unknown_image = Utils.deserializeBitmap(obj.optString("unknown_image", ""))
115 | setup.disabled_image = Utils.deserializeBitmap(obj.optString("disabled_image", ""))
116 |
117 | setup.server_certificate = HttpsTools.deserializeCertificate(obj.optString("server_certificate", ""))
118 | setup.client_certificate = HttpsTools.deserializeCertificate(obj.optString("client_certificate", ""))
119 | setup.client_keypair = SshTools.deserializeKeyPair(obj.optString("client_keypair", ""))
120 |
121 | setup.ignore_certificate = obj.optBoolean("ignore_certificate", false)
122 | setup.ignore_hostname_mismatch = obj.optBoolean("ignore_hostname_mismatch", false)
123 | setup.ignore_expiration = obj.optBoolean("ignore_expiration", false)
124 |
125 | return setup
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/NukiDoor.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.text.Html
4 | import app.trigger.DoorReply.ReplyCode
5 | import app.trigger.DoorStatus.StateCode
6 | import org.json.JSONObject
7 |
8 |
9 | class NukiDoor(override var id: Int, override var name: String) : Door() {
10 | override val type = Companion.TYPE
11 | var device_name = ""
12 | var user_name = "user"
13 | var shared_key = ""
14 | var auth_id: Long = 0
15 | var app_id: Long = 2342
16 |
17 | override fun getWiFiSSIDs(): String = ""
18 | override fun getWiFiRequired(): Boolean = false
19 |
20 | override fun parseReply(reply: DoorReply): DoorStatus {
21 | val msg = Html.fromHtml(reply.message).toString().trim { it <= ' ' }
22 | return when (reply.code) {
23 | ReplyCode.LOCAL_ERROR, ReplyCode.REMOTE_ERROR -> DoorStatus(StateCode.UNKNOWN, msg)
24 | ReplyCode.SUCCESS -> if (reply.message.contains("unlocked")) {
25 | // door unlocked
26 | DoorStatus(StateCode.OPEN, msg)
27 | } else if (reply.message.contains("locked")) {
28 | // door locked
29 | DoorStatus(StateCode.CLOSED, msg)
30 | } else {
31 | DoorStatus(StateCode.UNKNOWN, msg)
32 | }
33 | ReplyCode.DISABLED -> DoorStatus(StateCode.DISABLED, msg)
34 | }
35 | }
36 |
37 | override fun isActionSupported(action: MainActivity.Action): Boolean {
38 | return when (action) {
39 | MainActivity.Action.OPEN_DOOR -> true
40 | MainActivity.Action.CLOSE_DOOR -> true
41 | MainActivity.Action.RING_DOOR -> false
42 | MainActivity.Action.FETCH_STATE -> true
43 | }
44 | }
45 |
46 | fun toJSONObject(): JSONObject {
47 | val obj = JSONObject()
48 | obj.put("id", id)
49 | obj.put("name", name)
50 | obj.put("type", type)
51 | obj.put("device_name", device_name)
52 | obj.put("user_name", user_name)
53 | obj.put("shared_key", shared_key)
54 | obj.put("auth_id", auth_id)
55 | obj.put("app_id", app_id)
56 |
57 | obj.put("open_image", Utils.serializeBitmap(open_image))
58 | obj.put("closed_image", Utils.serializeBitmap(closed_image))
59 | obj.put("unknown_image", Utils.serializeBitmap(unknown_image))
60 | obj.put("disabled_image", Utils.serializeBitmap(disabled_image))
61 |
62 | return obj
63 | }
64 |
65 | companion object {
66 | const val TYPE = "NukiDoorSetup"
67 |
68 | fun fromJSONObject(obj: JSONObject): NukiDoor {
69 | val id = obj.getInt("id")
70 | val name = obj.getString("name")
71 | val setup = NukiDoor(id, name)
72 |
73 | setup.device_name = obj.optString("device_name", "")
74 | setup.user_name = obj.optString("user_name", "")
75 | setup.shared_key = obj.optString("shared_key", "")
76 | setup.auth_id = obj.optLong("auth_id", 0)
77 | setup.app_id = obj.optLong("app_id", 2342)
78 |
79 | setup.open_image = Utils.deserializeBitmap(obj.optString("open_image", ""))
80 | setup.closed_image = Utils.deserializeBitmap(obj.optString("closed_image", ""))
81 | setup.unknown_image = Utils.deserializeBitmap(obj.optString("unknown_image", ""))
82 | setup.disabled_image = Utils.deserializeBitmap(obj.optString("disabled_image", ""))
83 |
84 | return setup
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/OnTaskCompleted.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import app.trigger.DoorReply.ReplyCode
4 |
5 | interface OnTaskCompleted {
6 | fun onTaskResult(setupId: Int, action: MainActivity.Action, code: ReplyCode, message: String)
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/QRScanActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.Manifest
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.widget.Toast
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.appcompat.widget.Toolbar
10 | import app.trigger.ssh.SshTools
11 | import com.google.zxing.BarcodeFormat
12 | import com.google.zxing.ResultPoint
13 | import com.journeyapps.barcodescanner.BarcodeCallback
14 | import com.journeyapps.barcodescanner.BarcodeResult
15 | import com.journeyapps.barcodescanner.DecoratedBarcodeView
16 | import com.journeyapps.barcodescanner.DefaultDecoderFactory
17 | import org.json.JSONException
18 | import org.json.JSONObject
19 | import java.net.URI
20 |
21 | class QRScanActivity : AppCompatActivity(), BarcodeCallback {
22 | private lateinit var barcodeView: DecoratedBarcodeView
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | setContentView(R.layout.activity_qrscan)
27 |
28 | val toolbar = findViewById(R.id.toolbar)
29 | setSupportActionBar(toolbar)
30 |
31 | barcodeView = findViewById(R.id.barcodeScannerView)
32 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
33 | if (Utils.hasCameraPermission(this)) {
34 | startScan()
35 | } else {
36 | enabledCameraForResult.launch(Manifest.permission.CAMERA)
37 | }
38 | } else {
39 | startScan()
40 | }
41 | }
42 |
43 | private fun startScan() {
44 | val formats: Collection = listOf(BarcodeFormat.QR_CODE)
45 | barcodeView.barcodeView.decoderFactory = DefaultDecoderFactory(formats)
46 | barcodeView.initializeFromIntent(intent)
47 | barcodeView.decodeContinuous(this)
48 | }
49 |
50 | override fun onResume() {
51 | super.onResume()
52 | if (Utils.hasCameraPermission(this)) {
53 | barcodeView.resume()
54 | }
55 | }
56 |
57 | override fun onPause() {
58 | super.onPause()
59 | if (Utils.hasCameraPermission(this)) {
60 | barcodeView.pause()
61 | }
62 | }
63 |
64 | private val enabledCameraForResult = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
65 | isGranted -> if (isGranted) {
66 | startScan()
67 | } else {
68 | Toast.makeText(this, R.string.missing_camera_permission, Toast.LENGTH_LONG).show()
69 | finish()
70 | }
71 | }
72 |
73 | private fun decodeSetup(data: String): JSONObject {
74 | try {
75 | val kp = SshTools.parsePrivateKeyPEM(data)
76 | if (kp != null) {
77 | val obj = JSONObject()
78 | obj.put("type", SshDoor.TYPE)
79 | obj.put("name", Settings.getNewDoorName("SSH Door"))
80 | obj.put("keypair", SshTools.serializeKeyPair(kp))
81 | return obj
82 | } else {
83 | // assume raw link
84 | val uri = URI(data.trim { it <= ' ' })
85 | val scheme = uri.scheme
86 | val domain = uri.host
87 | val path = uri.path
88 | val query = uri.query
89 | val port = uri.port
90 | when (scheme) {
91 | "http", "https" -> {
92 | val http_server = domain + if (port > 0) ":$port" else ""
93 | val obj = JSONObject()
94 | obj.put("type", HttpsDoor.TYPE)
95 | obj.put("name", Settings.getNewDoorName(domain))
96 | obj.put("open_query", data)
97 | return obj
98 | }
99 | "mqtt", "mqtts" -> {
100 | val mqtt_server = scheme + "://" + domain + if (port > 0) ":$port" else ""
101 | val obj = JSONObject()
102 | obj.put("type", MqttDoor.TYPE)
103 | obj.put("name", Settings.getNewDoorName(domain))
104 | obj.put("server", mqtt_server)
105 | obj.put("command_topic", path)
106 | obj.put("open_command", query)
107 | return obj
108 | }
109 | "ssh" -> {
110 | val obj = JSONObject()
111 | obj.put("type", SshDoor.TYPE)
112 | obj.put("name", Settings.getNewDoorName(domain))
113 | obj.put("host", domain)
114 | obj.put("port", port)
115 | obj.put("open_command", query)
116 | return obj
117 | }
118 | else -> {}
119 | }
120 | }
121 | } catch (e: Exception) {
122 | // continue
123 | }
124 |
125 | // assume json data, throws exception otherwise
126 | return JSONObject(data)
127 | }
128 |
129 | override fun barcodeResult(result: BarcodeResult) {
130 | try {
131 | val obj = decodeSetup(result.text)
132 | // give entry a new id
133 | obj.put("id", Settings.getNewDoorIdentifier())
134 | val setup = Settings.fromJsonObject(obj)
135 | if (setup != null) {
136 | Settings.storeDoorSetup(setup)
137 | Toast.makeText(this, "Added ${setup.name}", Toast.LENGTH_LONG).show()
138 | } else {
139 | Toast.makeText(this, "Invalid QR Code", Toast.LENGTH_LONG).show()
140 | }
141 | } catch (e: JSONException) {
142 | Toast.makeText(this, "Invalid QR Code", Toast.LENGTH_LONG).show()
143 | } catch (e: IllegalAccessException) {
144 | Toast.makeText(this, "Incompatible QR Code", Toast.LENGTH_LONG).show()
145 | } catch (e: Exception) {
146 | Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
147 | }
148 | finish()
149 | }
150 |
151 | override fun possibleResultPoints(resultPoints: List) {}
152 |
153 | companion object {
154 | private const val TAG = "QRScanActivity"
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/QRShowActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.os.Bundle
4 | import android.widget.ImageView
5 | import android.widget.Toast
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.appcompat.widget.Toolbar
8 | import com.google.zxing.BarcodeFormat
9 | import com.google.zxing.EncodeHintType
10 | import com.google.zxing.MultiFormatWriter
11 | import com.google.zxing.WriterException
12 | import com.journeyapps.barcodescanner.BarcodeEncoder
13 | import org.json.JSONObject
14 |
15 | class QRShowActivity : AppCompatActivity() {
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | setContentView(R.layout.activity_qrshow)
19 |
20 | val toolbar = findViewById(R.id.toolbar)
21 | setSupportActionBar(toolbar)
22 |
23 | val door_id = intent.getIntExtra("door_id", -1)
24 | val door = Settings.getDoor(door_id)
25 | if (door != null) {
26 | title = "$title: ${door.name}"
27 | try {
28 | generateQR(door)
29 | } catch (e: Exception) {
30 | e.printStackTrace()
31 | Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
32 | }
33 | } else {
34 | Toast.makeText(this, "Setup not found.", Toast.LENGTH_LONG).show()
35 | }
36 | }
37 |
38 | private fun getJsonKeys(obj: JSONObject): ArrayList {
39 | val keys = ArrayList()
40 | val it = obj.keys()
41 | while (it.hasNext()) {
42 | keys.add(it.next())
43 | }
44 | return keys
45 | }
46 |
47 | private fun encodeSetup(obj: JSONObject): String {
48 | // do not export internal id
49 | obj.remove("id")
50 |
51 | // remove empty strings, images and null values
52 | val keys = getJsonKeys(obj)
53 | for (key in keys) {
54 | val value = obj.opt(key)
55 | if (value == null) {
56 | obj.remove(key)
57 | } else if (key.endsWith("_image")) {
58 | obj.remove(key)
59 | } else if (value is String) {
60 | if (value.length == 0) {
61 | obj.remove(key)
62 | }
63 | }
64 | }
65 | return obj.toString()
66 | }
67 |
68 | private fun generateQR(door: Door) {
69 | val multiFormatWriter = MultiFormatWriter()
70 | var data_length = 0
71 | try {
72 | val obj = Settings.toJsonObject(door) ?: throw Exception("Failed to convert setup to JSON")
73 | val data = encodeSetup(obj)
74 | data_length = data.length
75 |
76 | // data has to be a string
77 | val hints = mapOf(EncodeHintType.CHARACTER_SET to "utf-8")
78 | val bitMatrix = multiFormatWriter.encode(data, BarcodeFormat.QR_CODE, 1080, 1080, hints)
79 | val barcodeEncoder = BarcodeEncoder()
80 | val bitmap = barcodeEncoder.createBitmap(bitMatrix)
81 | findViewById(R.id.QRView).setImageBitmap(bitmap)
82 | } catch (e: WriterException) {
83 | Toast.makeText(this, "${e.message} ($data_length Bytes)", Toast.LENGTH_LONG).show()
84 | finish()
85 | }
86 | }
87 |
88 | companion object {
89 | private const val TAG = "QRShowActivity"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/SshDoor.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import app.trigger.ssh.KeyPairBean
4 | import app.trigger.ssh.SshTools
5 | import org.json.JSONObject
6 |
7 |
8 | class SshDoor(override var id: Int, override var name: String) : Door() {
9 | override val type = Companion.TYPE
10 |
11 | var require_wifi = false
12 | var keypair: KeyPairBean? = null
13 | var user = ""
14 | var password = ""
15 | var host = ""
16 | var port = 22
17 | var open_command = ""
18 | var close_command = ""
19 | var ring_command = ""
20 | var state_command = ""
21 |
22 | // regex to evaluate the door return message
23 | var unlocked_pattern = ""
24 | var locked_pattern = ""
25 |
26 | var register_url = ""
27 | var ssids = ""
28 | var timeout = 5000 // milliseconds
29 | var passphrase_tmp = ""
30 |
31 | override fun getWiFiRequired(): Boolean = require_wifi
32 | override fun getWiFiSSIDs(): String = ssids
33 |
34 | override fun getRegisterUrl(): String {
35 | return register_url.ifEmpty { host }
36 | }
37 |
38 | override fun parseReply(reply: DoorReply): DoorStatus {
39 | return Utils.genericDoorReplyParser(reply, unlocked_pattern, locked_pattern)
40 | }
41 |
42 | override fun isActionSupported(action: MainActivity.Action): Boolean {
43 | return when (action) {
44 | MainActivity.Action.OPEN_DOOR -> open_command.isNotEmpty()
45 | MainActivity.Action.CLOSE_DOOR -> close_command.isNotEmpty()
46 | MainActivity.Action.RING_DOOR -> ring_command.isNotEmpty()
47 | MainActivity.Action.FETCH_STATE -> state_command.isNotEmpty()
48 | }
49 | }
50 |
51 | fun needsPassphrase(): Boolean {
52 | return keypair != null && keypair!!.encrypted
53 | }
54 |
55 | fun toJSONObject(): JSONObject {
56 | val obj = JSONObject()
57 | obj.put("id", id)
58 | obj.put("name", name)
59 | obj.put("type", type)
60 |
61 | obj.put("require_wifi", require_wifi)
62 | obj.put("keypair", SshTools.serializeKeyPair(keypair))
63 | obj.put("user", user)
64 | obj.put("password", password)
65 | obj.put("host", host)
66 | obj.put("port", port)
67 | obj.put("open_command", open_command)
68 | obj.put("close_command", close_command)
69 | obj.put("ring_command", ring_command)
70 | obj.put("state_command", state_command)
71 |
72 | obj.put("unlocked_pattern", unlocked_pattern)
73 | obj.put("locked_pattern", locked_pattern)
74 | obj.put("open_image", Utils.serializeBitmap(open_image))
75 | obj.put("closed_image", Utils.serializeBitmap(closed_image))
76 | obj.put("unknown_image", Utils.serializeBitmap(unknown_image))
77 | obj.put("disabled_image", Utils.serializeBitmap(disabled_image))
78 |
79 | obj.put("register_url", register_url)
80 | obj.put("ssids", ssids)
81 | obj.put("timeout", timeout)
82 |
83 | return obj
84 | }
85 |
86 | companion object {
87 | const val TYPE = "SshDoorSetup"
88 |
89 | fun fromJSONObject(obj: JSONObject): SshDoor {
90 | val id = obj.getInt("id")
91 | val name = obj.getString("name")
92 | val setup = SshDoor(id, name)
93 |
94 | setup.require_wifi = obj.optBoolean("require_wifi", false)
95 | setup.keypair = SshTools.deserializeKeyPair(obj.optString("keypair", ""))
96 | setup.user = obj.optString("user", "")
97 | setup.password = obj.optString("password", "")
98 | setup.host = obj.optString("host", "")
99 | setup.port = obj.optInt("port", 22)
100 | setup.open_command = obj.optString("open_command", "")
101 | setup.close_command = obj.optString("close_command", "")
102 | setup.ring_command = obj.optString("ring_command", "")
103 | setup.state_command = obj.optString("state_command", "")
104 |
105 | setup.unlocked_pattern = obj.optString("unlocked_pattern", "")
106 | setup.locked_pattern = obj.optString("locked_pattern", "")
107 |
108 | setup.open_image = Utils.deserializeBitmap(obj.optString("open_image", ""))
109 | setup.closed_image = Utils.deserializeBitmap(obj.optString("closed_image", ""))
110 | setup.unknown_image = Utils.deserializeBitmap(obj.optString("unknown_image", ""))
111 | setup.disabled_image = Utils.deserializeBitmap(obj.optString("disabled_image", ""))
112 |
113 | setup.register_url = obj.optString("register_url", "")
114 | setup.ssids = obj.optString("ssids", "")
115 | setup.timeout = obj.optInt("timeout", 5000)
116 |
117 | return setup
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/WifiTools.kt:
--------------------------------------------------------------------------------
1 | package app.trigger
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.wifi.WifiManager
6 |
7 | object WifiTools {
8 | private const val TAG = "WifiTools"
9 | private var wifiManager: WifiManager? = null
10 | private var connectivityManager: ConnectivityManager? = null
11 |
12 | fun init(context: Context) {
13 | wifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
14 | connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
15 | }
16 |
17 | fun matchSSID(ssids: String?, ssid: String): Boolean {
18 | if (ssids != null) {
19 | for (element in ssids.split(",").toTypedArray()) {
20 | val e = element.trim { it <= ' ' }
21 | if (e.isNotEmpty() && e == ssid) {
22 | return true
23 | }
24 | }
25 | }
26 | return false
27 | }
28 |
29 | // Note: needs coarse location permission
30 | fun getCurrentSSID(): String {
31 | // Note: needs coarse location permission
32 | return if (wifiManager != null) {
33 | val info = wifiManager!!.connectionInfo
34 | val ssid = info.ssid
35 | if (ssid.length >= 2 && ssid.startsWith("\"") && ssid.endsWith("\"")) {
36 | // quoted string
37 | ssid.substring(1, ssid.length - 1)
38 | } else {
39 | // hexadecimal string...
40 | ssid
41 | }
42 | } else {
43 | ""
44 | }
45 | }
46 |
47 | /*
48 | public static ArrayList getScannedSSIDs() {
49 | ArrayList ssids;
50 | List results;
51 |
52 | ssids = new ArrayList<>();
53 | if (wifiManager != null) {
54 | results = wifiManager.getScanResults();
55 | if (results != null) {
56 | for (ScanResult result : results) {
57 | ssids.add(result.SSID);
58 | }
59 | }
60 | }
61 |
62 | return ssids;
63 | }
64 |
65 | public static ArrayList getConfiguredSSIDs() {
66 | // Note: needs coarse location permission
67 | List configs;
68 | ArrayList ssids;
69 |
70 | ssids = new ArrayList<>();
71 | if (wifiManager != null) {
72 | configs = wifiManager.getConfiguredNetworks();
73 | if (configs != null) {
74 | for (WifiConfiguration config : configs) {
75 | ssids.add(config.SSID);
76 | }
77 | }
78 | }
79 |
80 | return ssids;
81 | }
82 |
83 | public static WifiConfiguration findConfig(List configs, String ssid) {
84 | for (WifiConfiguration config : configs) {
85 | if (config.SSID.equals(ssid)) {
86 | return config;
87 | }
88 | }
89 | return null;
90 | }
91 |
92 | // connect to the best wifi that is configured by this app and system
93 | void connectBestOf(ArrayList ssids) {
94 | String current_ssid = this.getCurrentSSID();
95 | List configs;
96 | WifiConfiguration config;
97 | List scanned;
98 |
99 | if (wifiManager == null) {
100 | return;
101 | }
102 |
103 | configs = wifiManager.getConfiguredNetworks();
104 | scanned = wifiManager.getScanResults();
105 |
106 | if (scanned == null && configs == null) {
107 | Log.e("Wifi", "Insufficient data for connect.");
108 | return;
109 | }
110 |
111 | // TODO: sort by signal
112 | for (ScanResult scan : scanned) {
113 | config = findConfig(configs, scan.SSID);
114 | if (config != null) {
115 | if (!current_ssid.equals(scan.SSID)) {
116 | wifiManager.disconnect();
117 | wifiManager.enableNetwork(config.networkId, true);
118 | wifiManager.reconnect();
119 | }
120 | break;
121 | }
122 | }
123 | }
124 | */
125 | fun isConnected(): Boolean {
126 | val networks = connectivityManager!!.allNetworks
127 | for (network in networks) {
128 | val networkInfo = connectivityManager!!.getNetworkInfo(network)
129 | if (networkInfo!!.type == ConnectivityManager.TYPE_WIFI) {
130 | return true
131 | }
132 | }
133 | return false
134 | }
135 |
136 | fun isConnectedWithInternet(): Boolean {
137 | if (connectivityManager == null) {
138 | return false
139 | }
140 | val mWifi = connectivityManager!!.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
141 | return mWifi!!.isConnected
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/bluetooth/BluetoothRequestHandler.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.bluetooth
2 |
3 | import app.trigger.BluetoothTools.createRfcommSocket
4 | import android.bluetooth.BluetoothSocket
5 | import app.trigger.DoorReply.ReplyCode
6 | import android.bluetooth.BluetoothAdapter
7 | import app.trigger.*
8 | import java.io.IOException
9 | import java.lang.Exception
10 | import java.util.*
11 |
12 |
13 | class BluetoothRequestHandler(private val listener: OnTaskCompleted, private val setup: BluetoothDoor, private val action: MainActivity.Action) : Thread() {
14 | private var socket: BluetoothSocket? = null
15 | override fun run() {
16 | if (setup.id < 0) {
17 | listener.onTaskResult(setup.id, action, ReplyCode.LOCAL_ERROR, "Internal Error")
18 | return
19 | }
20 |
21 | val adapter = BluetoothAdapter.getDefaultAdapter()
22 | if (adapter == null) {
23 | listener.onTaskResult(setup.id, action, ReplyCode.DISABLED, "Device does not support Bluetooth")
24 | return
25 | }
26 |
27 | if (!adapter.isEnabled) {
28 | // request to enable
29 | listener.onTaskResult(setup.id, action, ReplyCode.DISABLED, "Bluetooth is disabled.")
30 | return
31 | }
32 |
33 | val request = when (action) {
34 | MainActivity.Action.OPEN_DOOR -> setup.open_query
35 | MainActivity.Action.RING_DOOR -> setup.ring_query
36 | MainActivity.Action.CLOSE_DOOR -> setup.close_query
37 | MainActivity.Action.FETCH_STATE -> setup.status_query
38 | }
39 |
40 | if (request.isEmpty()) {
41 | listener.onTaskResult(setup.id, action, ReplyCode.LOCAL_ERROR, "")
42 | return
43 | }
44 | try {
45 | val pairedDevices = adapter.bondedDevices
46 | var address = ""
47 | for (device in pairedDevices) {
48 | if (device.name != null && device.name == setup.device_name
49 | || device.address == setup.device_name.uppercase(Locale.ROOT)
50 | ) {
51 | address = device.address
52 | }
53 | }
54 | if (address.isEmpty()) {
55 | listener.onTaskResult(setup.id, action, ReplyCode.LOCAL_ERROR, "Device not paired yet.")
56 | return
57 | }
58 | val device = adapter.getRemoteDevice(address)
59 | socket = if (setup.service_uuid.isEmpty()) {
60 | createRfcommSocket(device)
61 | } else {
62 | val uuid = UUID.fromString(setup.service_uuid)
63 | device.createRfcommSocketToServiceRecord(uuid)
64 | }
65 | socket!!.connect()
66 |
67 | // Get the BluetoothSocket input and output streams
68 | val tmpIn = socket!!.inputStream
69 | val tmpOut = socket!!.outputStream
70 | tmpOut.write(request.toByteArray())
71 | tmpOut.flush()
72 | val response = try {
73 | val buffer = ByteArray(512)
74 | val bytes = tmpIn.read(buffer)
75 | String(buffer, 0, bytes)
76 | } catch (ioe: IOException) {
77 | listener.onTaskResult(setup.id, action, ReplyCode.REMOTE_ERROR, "Cannot reach remote device.")
78 | return
79 | }
80 | socket!!.close()
81 | listener.onTaskResult(setup.id, action, ReplyCode.SUCCESS, response)
82 | } catch (e: Exception) {
83 | listener.onTaskResult(setup.id, action, ReplyCode.LOCAL_ERROR, e.toString())
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/https/CertificateFetchTask.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.https
2 |
3 | import android.os.AsyncTask
4 | import app.trigger.Log
5 | import java.lang.Exception
6 | import java.net.URL
7 | import java.security.cert.Certificate
8 | import javax.net.ssl.HttpsURLConnection
9 |
10 | class CertificateFetchTask(private val listener: OnTaskCompleted) : AsyncTask() {
11 | interface OnTaskCompleted {
12 | fun onCertificateFetchTaskCompleted(result: Result)
13 | }
14 |
15 | class Result internal constructor(var certificate: Certificate?, var error: String)
16 |
17 | override fun doInBackground(vararg params: Any?): Result {
18 | if (params.size != 1) {
19 | Log.e(TAG, "Unexpected number of params.")
20 | return Result(null, "Internal Error")
21 | }
22 | return try {
23 | var url = URL(params[0] as String)
24 |
25 | // try to establish TLS session only
26 | val port = if (url.port > 0) url.port else url.defaultPort
27 | url = URL("https", url.host, port, "")
28 |
29 | // disable all certification checks
30 | HttpsTools.disableDefaultHostnameVerifier()
31 | HttpsTools.disableDefaultCertificateValidation()
32 | val con = url.openConnection() as HttpsURLConnection
33 | con.connectTimeout = 2000
34 | con.connect()
35 | val certificates = con.serverCertificates
36 | con.disconnect()
37 | if (certificates.size == 0) {
38 | Result(null, "No certificate found.")
39 | } else {
40 | Result(certificates[0], "")
41 | }
42 | } catch (e: Exception) {
43 | Result(null, e.toString())
44 | }
45 | }
46 |
47 | override fun onPostExecute(result: Result) {
48 | listener.onCertificateFetchTaskCompleted(result)
49 | }
50 |
51 | companion object {
52 | const val TAG = "CertificateFetchTask"
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/https/HttpsClientCertificateActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.https
2 |
3 | import android.os.Bundle
4 | import app.trigger.AbstractCertificateActivity
5 | import app.trigger.Door
6 | import app.trigger.HttpsDoor
7 | import app.trigger.SetupActivity
8 | import java.security.cert.Certificate
9 |
10 | class HttpsClientCertificateActivity : AbstractCertificateActivity() {
11 | private lateinit var httpsDoor: HttpsDoor
12 |
13 | override fun getDoor(): Door {
14 | return httpsDoor
15 | }
16 |
17 | override fun getCertificate(): Certificate? {
18 | return httpsDoor.client_certificate
19 | }
20 |
21 | override fun setCertificate(certificate: Certificate?) {
22 | httpsDoor.client_certificate = certificate
23 | }
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | if (SetupActivity.currentDoor is HttpsDoor) {
27 | httpsDoor = SetupActivity.currentDoor as HttpsDoor
28 | } else {
29 | // not expected to happen
30 | finish()
31 | return
32 | }
33 | super.onCreate(savedInstanceState)
34 | }
35 |
36 | companion object {
37 | private const val TAG = "HttpsClientCertificateActivity"
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/https/HttpsClientKeyPairActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.https
2 |
3 | import android.os.Bundle
4 | import app.trigger.ssh.KeyPairBean
5 | import app.trigger.AbstractClientKeyPairActivity
6 | import app.trigger.HttpsDoor
7 | import app.trigger.SetupActivity
8 |
9 |
10 | class HttpsClientKeyPairActivity : AbstractClientKeyPairActivity() {
11 | private lateinit var httpsDoor: HttpsDoor
12 |
13 | override fun getKeyPair(): KeyPairBean? {
14 | return httpsDoor.client_keypair
15 | }
16 |
17 | override fun setKeyPair(keyPair: KeyPairBean?) {
18 | httpsDoor.client_keypair = keyPair
19 | }
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | if (SetupActivity.currentDoor is HttpsDoor) {
23 | httpsDoor = SetupActivity.currentDoor as HttpsDoor
24 | } else {
25 | // not expected to happen
26 | finish()
27 | return
28 | }
29 | super.onCreate(savedInstanceState)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/https/HttpsServerCertificateActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.https
2 |
3 | import android.os.Bundle
4 | import app.trigger.AbstractCertificateActivity
5 | import app.trigger.Door
6 | import app.trigger.HttpsDoor
7 | import app.trigger.SetupActivity
8 | import java.security.cert.Certificate
9 |
10 | class HttpsServerCertificateActivity : AbstractCertificateActivity() {
11 | private lateinit var httpsDoor: HttpsDoor
12 |
13 | override fun getDoor(): Door {
14 | return httpsDoor
15 | }
16 |
17 | override fun getCertificate(): Certificate? {
18 | return httpsDoor.server_certificate
19 | }
20 |
21 | override fun setCertificate(certificate: Certificate?) {
22 | httpsDoor.server_certificate = certificate
23 | }
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | if (SetupActivity.currentDoor is HttpsDoor) {
27 | httpsDoor = SetupActivity.currentDoor as HttpsDoor
28 | } else {
29 | // not expected to happen
30 | finish()
31 | return
32 | }
33 | super.onCreate(savedInstanceState)
34 | }
35 |
36 | companion object {
37 | private const val TAG = "HttpsServerCertificateActivity"
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/https/HttpsTools.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.https
2 |
3 | import kotlin.Throws
4 | import android.util.Base64
5 | import app.trigger.Log
6 | import java.io.ByteArrayInputStream
7 | import java.lang.Exception
8 | import java.security.*
9 | import java.security.cert.Certificate
10 | import java.security.cert.CertificateException
11 | import java.security.cert.CertificateFactory
12 | import java.security.cert.X509Certificate
13 | import javax.net.ssl.*
14 |
15 | object HttpsTools {
16 | private const val TAG = "HttpsTools"
17 | fun isValid(cert: X509Certificate): Boolean {
18 | return try {
19 | cert.checkValidity()
20 | true
21 | } catch (e: Exception) {
22 | false
23 | }
24 | }
25 |
26 | fun isSelfSigned(cert: X509Certificate): Boolean {
27 | return cert.issuerX500Principal.name ==
28 | cert.subjectX500Principal.name
29 | }
30 |
31 | fun serializeCertificate(certificate: Certificate?): String {
32 | if (certificate == null) {
33 | return ""
34 | }
35 | try {
36 | val prefix = "-----BEGIN CERTIFICATE-----"
37 | val mid = Base64.encodeToString(certificate.encoded, Base64.DEFAULT)
38 | val suffix = "-----END CERTIFICATE-----"
39 | return "${prefix}\n${mid}\n${suffix}"
40 | } catch (e: Exception) {
41 | Log.e(TAG, e.toString())
42 | }
43 | return ""
44 | }
45 |
46 | fun deserializeCertificate(certificate: String?): Certificate? {
47 | if (certificate.isNullOrEmpty()) {
48 | return null
49 | }
50 | try {
51 | val derInputStream = ByteArrayInputStream(certificate.toByteArray())
52 | val certificateFactory = CertificateFactory.getInstance("X.509") //KeyStore.getDefaultType() => "BKS"
53 | return certificateFactory.generateCertificate(derInputStream)
54 | } catch (e: Exception) {
55 | Log.e(this, e.toString())
56 | }
57 | return null
58 | }
59 |
60 | // disable any certificate validation
61 | fun disableDefaultHostnameVerifier() {
62 | HttpsURLConnection.setDefaultHostnameVerifier { arg0, arg1 -> true }
63 | }
64 |
65 | // disable any certificate validation
66 | @Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
67 | fun disableDefaultCertificateValidation() {
68 | val trustManager: TrustManager = object : X509TrustManager {
69 | @Throws(CertificateException::class)
70 | override fun checkClientTrusted(cert: Array, authType: String) {
71 | }
72 |
73 | @Throws(CertificateException::class)
74 | override fun checkServerTrusted(cert: Array, authType: String) {
75 | }
76 |
77 | override fun getAcceptedIssuers(): Array {
78 | return arrayOf()
79 | }
80 | }
81 | val trustManagers = arrayOf(trustManager)
82 | val context = SSLContext.getInstance("TLS")
83 | context.init(null, trustManagers, SecureRandom())
84 | HttpsURLConnection.setDefaultSSLSocketFactory(context.socketFactory)
85 | }
86 |
87 | @Throws(NoSuchAlgorithmException::class, KeyStoreException::class, KeyManagementException::class)
88 | fun getSocketFactoryIgnoreCertificateExpiredException(): SSLSocketFactory {
89 | val factory = TrustManagerFactory.getInstance("X509")
90 | factory.init(null as KeyStore?)
91 | val trustManagers = factory.trustManagers
92 | for (i in trustManagers.indices) {
93 | if (trustManagers[i] is X509TrustManager) {
94 | trustManagers[i] = IgnoreExpirationTrustManager(trustManagers[i] as X509TrustManager)
95 | }
96 | }
97 | val sslContext = SSLContext.getInstance("TLS")
98 | sslContext.init(null, trustManagers, null)
99 | return sslContext.socketFactory
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/https/IgnoreExpirationTrustManager.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.https
2 |
3 | import kotlin.Throws
4 | import java.math.BigInteger
5 | import java.security.*
6 | import java.security.cert.CertificateEncodingException
7 | import java.security.cert.CertificateException
8 | import java.security.cert.X509Certificate
9 | import java.util.*
10 | import javax.net.ssl.X509TrustManager
11 |
12 | internal class IgnoreExpirationTrustManager(private val innerTrustManager: X509TrustManager) : X509TrustManager {
13 | @Throws(CertificateException::class)
14 | override fun checkClientTrusted(chain: Array, authType: String) {
15 | innerTrustManager.checkClientTrusted(chain, authType)
16 | }
17 |
18 | @Throws(CertificateException::class)
19 | override fun checkServerTrusted(chain: Array, authType: String) {
20 | val newChain = arrayOf(EternalCertificate(chain[0]))
21 | /*
22 | var chain = chain
23 | chain = Arrays.copyOf(chain, chain.size)
24 | val newChain = arrayOfNulls(chain.size)
25 | newChain[0] = EternalCertificate(chain[0])
26 | System.arraycopy(chain, 1, newChain, 1, chain.size - 1)
27 | chain = newChain
28 | innerTrustManager.checkServerTrusted(chain, authType)
29 | */
30 | innerTrustManager.checkServerTrusted(newChain, authType)
31 | }
32 |
33 | override fun getAcceptedIssuers(): Array {
34 | return innerTrustManager.acceptedIssuers
35 | }
36 |
37 | private inner class EternalCertificate(private val originalCertificate: X509Certificate) : X509Certificate() {
38 | override fun checkValidity() {
39 | // ignore notBefore/notAfter
40 | }
41 |
42 | override fun checkValidity(date: Date) {
43 | // ignore notBefore/notAfter
44 | }
45 |
46 | override fun getVersion(): Int {
47 | return originalCertificate.version
48 | }
49 |
50 | override fun getSerialNumber(): BigInteger {
51 | return originalCertificate.serialNumber
52 | }
53 |
54 | override fun getIssuerDN(): Principal {
55 | return originalCertificate.issuerDN
56 | }
57 |
58 | override fun getSubjectDN(): Principal {
59 | return originalCertificate.subjectDN
60 | }
61 |
62 | override fun getNotBefore(): Date {
63 | return originalCertificate.notBefore
64 | }
65 |
66 | override fun getNotAfter(): Date {
67 | return originalCertificate.notAfter
68 | }
69 |
70 | @Throws(CertificateEncodingException::class)
71 | override fun getTBSCertificate(): ByteArray {
72 | return originalCertificate.tbsCertificate
73 | }
74 |
75 | override fun getSignature(): ByteArray {
76 | return originalCertificate.signature
77 | }
78 |
79 | override fun getSigAlgName(): String {
80 | return originalCertificate.sigAlgName
81 | }
82 |
83 | override fun getSigAlgOID(): String {
84 | return originalCertificate.sigAlgOID
85 | }
86 |
87 | override fun getSigAlgParams(): ByteArray {
88 | return originalCertificate.sigAlgParams
89 | }
90 |
91 | override fun getIssuerUniqueID(): BooleanArray {
92 | return originalCertificate.issuerUniqueID
93 | }
94 |
95 | override fun getSubjectUniqueID(): BooleanArray {
96 | return originalCertificate.subjectUniqueID
97 | }
98 |
99 | override fun getKeyUsage(): BooleanArray {
100 | return originalCertificate.keyUsage
101 | }
102 |
103 | override fun getBasicConstraints(): Int {
104 | return originalCertificate.basicConstraints
105 | }
106 |
107 | @Throws(CertificateEncodingException::class)
108 | override fun getEncoded(): ByteArray {
109 | return originalCertificate.encoded
110 | }
111 |
112 | @Throws(CertificateException::class, NoSuchAlgorithmException::class, InvalidKeyException::class, NoSuchProviderException::class, SignatureException::class)
113 | override fun verify(key: PublicKey) {
114 | originalCertificate.verify(key)
115 | }
116 |
117 | @Throws(CertificateException::class, NoSuchAlgorithmException::class, InvalidKeyException::class, NoSuchProviderException::class, SignatureException::class)
118 | override fun verify(key: PublicKey, sigProvider: String) {
119 | originalCertificate.verify(key, sigProvider)
120 | }
121 |
122 | override fun toString(): String {
123 | return originalCertificate.toString()
124 | }
125 |
126 | override fun getPublicKey(): PublicKey {
127 | return originalCertificate.publicKey
128 | }
129 |
130 | override fun getCriticalExtensionOIDs(): Set {
131 | return originalCertificate.criticalExtensionOIDs
132 | }
133 |
134 | override fun getExtensionValue(oid: String): ByteArray {
135 | return originalCertificate.getExtensionValue(oid)
136 | }
137 |
138 | override fun getNonCriticalExtensionOIDs(): Set {
139 | return originalCertificate.nonCriticalExtensionOIDs
140 | }
141 |
142 | override fun hasUnsupportedCriticalExtension(): Boolean {
143 | return originalCertificate.hasUnsupportedCriticalExtension()
144 | }
145 | }
146 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/mqtt/MqttClientCertificateActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.mqtt
2 |
3 | import android.os.Bundle
4 | import app.trigger.AbstractCertificateActivity
5 | import app.trigger.Door
6 | import app.trigger.MqttDoor
7 | import app.trigger.SetupActivity
8 | import java.security.cert.Certificate
9 |
10 | class MqttClientCertificateActivity : AbstractCertificateActivity() {
11 | private lateinit var mqttDoor: MqttDoor
12 |
13 | override fun getDoor(): Door {
14 | return mqttDoor
15 | }
16 |
17 | override fun getCertificate(): Certificate? {
18 | return mqttDoor.client_certificate
19 | }
20 |
21 | override fun setCertificate(certificate: Certificate?) {
22 | mqttDoor.client_certificate = certificate
23 | }
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | if (SetupActivity.currentDoor is MqttDoor) {
27 | mqttDoor = SetupActivity.currentDoor as MqttDoor
28 | } else {
29 | // not expected to happen
30 | finish()
31 | return
32 | }
33 | super.onCreate(savedInstanceState)
34 | }
35 |
36 | companion object {
37 | private const val TAG = "MqttClientCertificateActivity"
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/mqtt/MqttClientKeyPairActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.mqtt
2 |
3 | import android.os.Bundle
4 | import app.trigger.ssh.KeyPairBean
5 | import app.trigger.AbstractClientKeyPairActivity
6 | import app.trigger.MqttDoor
7 | import app.trigger.SetupActivity
8 |
9 |
10 | class MqttClientKeyPairActivity : AbstractClientKeyPairActivity() {
11 | private lateinit var mqttDoor: MqttDoor
12 |
13 | override fun getKeyPair(): KeyPairBean? {
14 | return mqttDoor.client_keypair
15 | }
16 |
17 | override fun setKeyPair(keyPair: KeyPairBean?) {
18 | mqttDoor.client_keypair = keyPair
19 | }
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | if (SetupActivity.currentDoor is MqttDoor) {
23 | mqttDoor = SetupActivity.currentDoor as MqttDoor
24 | } else {
25 | // not expected to happen
26 | finish()
27 | return
28 | }
29 | super.onCreate(savedInstanceState)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/mqtt/MqttServerCertificateActivity.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.mqtt
2 |
3 | import android.os.Bundle
4 | import app.trigger.AbstractCertificateActivity
5 | import app.trigger.Door
6 | import app.trigger.MqttDoor
7 | import app.trigger.SetupActivity
8 | import java.security.cert.Certificate
9 |
10 | class MqttServerCertificateActivity : AbstractCertificateActivity() {
11 | private lateinit var mqttDoor: MqttDoor
12 |
13 | override fun getDoor(): Door {
14 | return mqttDoor
15 | }
16 |
17 | override fun getCertificate(): Certificate? {
18 | return mqttDoor.server_certificate
19 | }
20 |
21 | override fun setCertificate(certificate: Certificate?) {
22 | mqttDoor.server_certificate = certificate
23 | }
24 |
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | if (SetupActivity.currentDoor is MqttDoor) {
27 | mqttDoor = SetupActivity.currentDoor as MqttDoor
28 | } else {
29 | // not expected to happen
30 | finish()
31 | return
32 | }
33 | super.onCreate(savedInstanceState)
34 | }
35 |
36 | companion object {
37 | private const val TAG = "MqttServerCertificateActivity"
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/nuki/NukiCallback.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.nuki
2 |
3 | import app.trigger.Utils.byteArrayToHexString
4 | import app.trigger.DoorReply.ReplyCode
5 | import android.bluetooth.BluetoothGattCharacteristic
6 | import android.bluetooth.BluetoothGattCallback
7 | import android.bluetooth.BluetoothGatt
8 | import android.bluetooth.BluetoothGattDescriptor
9 | import app.trigger.*
10 | import java.util.*
11 |
12 | internal abstract class NukiCallback(protected val door_id: Int, val action: MainActivity.Action, protected val listener: OnTaskCompleted, private val service_uuid: UUID, private val characteristic_uuid: UUID)
13 | : BluetoothGattCallback() {
14 | /*
15 | * This is mostly called to end the connection quickly instead
16 | * of waiting for the other side to close the connection
17 | */
18 | protected fun closeConnection(gatt: BluetoothGatt) {
19 | Log.d(TAG, "closeConnection")
20 | gatt.close()
21 | NukiRequestHandler.Companion.bluetooth_in_use.set(false)
22 | }
23 |
24 | override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
25 | Log.d(TAG, "onConnectionStateChange, status: "
26 | + NukiRequestHandler.getGattStatus(status)
27 | + ", newState: " + NukiRequestHandler.getGattState(newState))
28 | if (status == BluetoothGatt.GATT_SUCCESS) {
29 | when (newState) {
30 | BluetoothGatt.STATE_CONNECTED -> gatt.discoverServices()
31 | BluetoothGatt.STATE_CONNECTING -> {}
32 | BluetoothGatt.STATE_DISCONNECTED -> closeConnection(gatt)
33 | BluetoothGatt.STATE_DISCONNECTING -> closeConnection(gatt)
34 | else -> closeConnection(gatt)
35 | }
36 | } else {
37 | closeConnection(gatt)
38 | listener.onTaskResult(
39 | door_id, action, ReplyCode.REMOTE_ERROR, "Connection error: ${NukiRequestHandler.getGattStatus(status)}"
40 | )
41 | }
42 | }
43 |
44 | override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
45 | Log.d(TAG, "onServicesDiscovered")
46 |
47 | if (status == BluetoothGatt.GATT_SUCCESS) {
48 | val service = gatt.getService(service_uuid)
49 | if (service == null) {
50 | Log.d(TAG, "Service not found: $service_uuid")
51 | closeConnection(gatt)
52 | listener.onTaskResult(
53 | door_id, action, ReplyCode.REMOTE_ERROR, "Service not found: $service_uuid"
54 | )
55 | return
56 | }
57 | val characteristic = service.getCharacteristic(characteristic_uuid)
58 | if (characteristic == null) {
59 | Log.d(TAG, "Characteristic not found: $characteristic_uuid")
60 | closeConnection(gatt)
61 | listener.onTaskResult(
62 | door_id, action, ReplyCode.REMOTE_ERROR, "Characteristic not found: $characteristic_uuid"
63 | )
64 | return
65 | }
66 | gatt.setCharacteristicNotification(characteristic, true)
67 | val descriptor = characteristic.getDescriptor(CCC_DESCRIPTOR_UUID)
68 | if (descriptor == null) {
69 | Log.d(TAG, "Descriptor not found: $CCC_DESCRIPTOR_UUID")
70 | closeConnection(gatt)
71 | listener.onTaskResult(
72 | door_id, action, ReplyCode.REMOTE_ERROR, "Descriptor not found: $CCC_DESCRIPTOR_UUID"
73 | )
74 | return
75 | }
76 |
77 | //Log.i(TAG, "characteristic properties: " + NukiTools.getProperties(characteristic));
78 | descriptor.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
79 | val ok = gatt.writeDescriptor(descriptor)
80 | if (!ok) {
81 | Log.e(TAG, "descriptor write failed")
82 | closeConnection(gatt)
83 | }
84 | } else {
85 | Log.d(TAG, "Client not found: ${NukiRequestHandler.getGattStatus(status)}")
86 | closeConnection(gatt)
87 | listener.onTaskResult(
88 | door_id, action, ReplyCode.LOCAL_ERROR, "Client not found: ${NukiRequestHandler.getGattStatus(status)}"
89 | )
90 | }
91 | }
92 |
93 | override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) {
94 | Log.d(TAG, "onDescriptorWrite, uiid: ${descriptor.uuid}: ${byteArrayToHexString(descriptor.value)}")
95 |
96 | if (status == BluetoothGatt.GATT_SUCCESS) {
97 | onConnected(gatt, descriptor.characteristic)
98 | } else {
99 | Log.e(TAG, "failed to write to client: ${NukiRequestHandler.getGattStatus(status)}")
100 | closeConnection(gatt)
101 | }
102 | }
103 |
104 | abstract fun onConnected(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic)
105 |
106 | companion object {
107 | private const val TAG = "NukiCallback"
108 |
109 | // Client Characteristic Configuration Descriptor
110 | val CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
111 |
112 | // Pairing UUIDs
113 | val PAIRING_SERVICE_UUID = UUID.fromString("a92ee100-5501-11e4-916c-0800200c9a66")
114 | val PAIRING_GDIO_XTERISTIC_UUID = UUID.fromString("a92ee101-5501-11e4-916c-0800200c9a66")
115 |
116 | // Keyturner UUIDs
117 | val KEYTURNER_SERVICE_UUID = UUID.fromString("a92ee200-5501-11e4-916c-0800200c9a66")
118 | val KEYTURNER_GDIO_XTERISTIC_UUID = UUID.fromString("a92ee201-5501-11e4-916c-0800200c9a66")
119 | val KEYTURNER_USDIO_XTERISTIC_UUID = UUID.fromString("a92ee202-5501-11e4-916c-0800200c9a66")
120 | }
121 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/nuki/NukiCommand.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.nuki
2 |
3 | import org.libsodium.jni.Sodium
4 | import android.util.Log
5 | import java.util.*
6 |
7 | open class NukiCommand(var command: Int) {
8 | internal class NukiRequest(var command_id: Int) : NukiCommand(0x0001) {
9 | fun generate(): ByteArray {
10 | return NukiTools.concat(NukiTools.from16(command), NukiTools.from16(command_id))
11 | }
12 | }
13 |
14 | internal class NukiAuthIdConfirm(var authenticator: ByteArray, var auth_id: Long) : NukiCommand(0x001E) {
15 | fun generate(): ByteArray {
16 | return NukiTools.concat(NukiTools.from16(command), authenticator, NukiTools.from32_auth_id(auth_id))
17 | }
18 | }
19 |
20 | internal class NukiAuthData(var authenticator: ByteArray, // 0x00: App, 0x01: Bridge, 0x02 Fob, 0x03 Keypad
21 | var id_type: Int, // The ID of the Nuki App, Nuki Bridge or Nuki Fob to be authorized. same as auth_id????
22 | var app_id: Long, var name: String, var nonce: ByteArray) : NukiCommand(0x0006) {
23 | fun generate(): ByteArray {
24 | return NukiTools.concat(NukiTools.from16(command), authenticator, NukiTools.from8(id_type), NukiTools.from32_app_id(app_id), NukiTools.nameToBytes(name, 32), nonce)
25 | }
26 | }
27 |
28 | internal class NukiError(var error_code: Int, var command_id: Int) : NukiCommand(0x0012) {
29 | fun asString(): String {
30 | return "Nuki Error: " + NukiTools.getError(error_code)
31 | }
32 | }
33 |
34 | internal class NukiPublicKey(var public_key: ByteArray) : NukiCommand(0x0003) {
35 | fun generate(): ByteArray {
36 | return NukiTools.concat(NukiTools.from16(command), public_key)
37 | }
38 | }
39 |
40 | internal class NukiChallenge(var nonce: ByteArray) : NukiCommand(0x0004) {
41 | fun generate(): ByteArray {
42 | return NukiTools.concat(NukiTools.from16(command), nonce)
43 | }
44 |
45 | init {
46 | if (nonce.size != 32) {
47 | Log.e("NukiChallenge", "invalid nonce length: " + nonce.size + " (expected " + 32 + ")")
48 | }
49 | }
50 | }
51 |
52 | internal class NukiAuthID(var authenticator: ByteArray, var auth_id: Long, var uuid: ByteArray, var nonce: ByteArray) : NukiCommand(0x0007) {
53 | fun verify(shared_key: ByteArray?, nonce: ByteArray?): Boolean {
54 | val valueR = NukiTools.concat(NukiTools.from32_auth_id(auth_id), uuid, this.nonce, nonce!!)
55 | val authenticator = ByteArray(Sodium.crypto_auth_hmacsha256_bytes())
56 | if (Sodium.crypto_auth_hmacsha256(authenticator, valueR, valueR.size, shared_key) != 0) {
57 | Log.e("NukiAuthID", "crypto_auth_hmacsha256 failed")
58 | return false
59 | }
60 | return Arrays.equals(this.authenticator, authenticator)
61 | }
62 | }
63 |
64 | internal class NukiStatus(var status: Int) : NukiCommand(0x000E) {
65 | fun generate(): ByteArray {
66 | return NukiTools.concat(NukiTools.from16(command), NukiTools.from8(status))
67 | }
68 |
69 | companion object {
70 | const val STATUS_COMPLETE = 0x00 // Returned to signal the successful completion of a command
71 | const val STATUS_ACCEPTED = 0x01 // Returned to signal that a command has been accepted but the completion status will be signaled later.
72 | }
73 | }
74 |
75 | internal class NukiStates(var nuki_state: Int, var lock_state: Int, var lock_trigger: Int, var current_time: String, var time_offset: Int, var battery_critical: Int) : NukiCommand(0x000C)
76 | internal class NukiLockAction(var lock_action: Int, var app_id: Long, var flags: Int, // optional
77 | var name_suffix: String?, var nonce: ByteArray?) : NukiCommand(0x000D) {
78 | constructor(lock_action: Int, app_id: Long, flags: Int, nonce: ByteArray?) : this(lock_action, app_id, flags, null, nonce)
79 |
80 | fun generate(): ByteArray {
81 | val name_suffix_padded = if (name_suffix == null) {
82 | ByteArray(0)
83 | } else {
84 | NukiTools.nameToBytes(name_suffix, 20)
85 | }
86 | return NukiTools.concat(NukiTools.from16(command), NukiTools.from8(lock_action), NukiTools.from32_app_id(app_id), NukiTools.from8(flags), name_suffix_padded, nonce!!)
87 | }
88 |
89 | init {
90 | if (nonce!!.size != 32) {
91 | Log.e("NukiLockAction", "nonce has wrong length: " + nonce!!.size)
92 | }
93 | }
94 | }
95 |
96 | internal class NukiAuthAuthentication(private var authenticator: ByteArray) : NukiCommand(0x0005) {
97 | fun generate(): ByteArray {
98 | return NukiTools.concat(NukiTools.from16(command), authenticator)
99 | }
100 | }
101 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/nuki/NukiLockActionCallback.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.nuki
2 |
3 | import app.trigger.Utils.byteArrayToHexString
4 | import app.trigger.Utils.hexStringToByteArray
5 | import app.trigger.DoorReply.ReplyCode
6 | import android.bluetooth.BluetoothGattCharacteristic
7 | import android.bluetooth.BluetoothGatt
8 | import app.trigger.nuki.NukiCommand.NukiRequest
9 | import app.trigger.nuki.NukiCommand.NukiChallenge
10 | import app.trigger.nuki.NukiCommand.NukiStates
11 | import app.trigger.nuki.NukiCommand.NukiStatus
12 | import app.trigger.nuki.NukiCommand.NukiError
13 | import app.trigger.nuki.NukiCommand.NukiLockAction
14 | import app.trigger.*
15 |
16 | internal class NukiLockActionCallback(door_id: Int, action: MainActivity.Action, listener: OnTaskCompleted, setup: NukiDoor, lock_action: Int)
17 | : NukiCallback(door_id, action, listener, KEYTURNER_SERVICE_UUID, KEYTURNER_USDIO_XTERISTIC_UUID) {
18 | var auth_id: Long
19 | var app_id: Long
20 | var lock_action: Int
21 | var shared_key: ByteArray
22 | var data = ByteArray(0)
23 |
24 | override fun onConnected(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
25 | Log.d(TAG, "onConnected")
26 | val nr = NukiRequest(0x04)
27 | val request: ByteArray? = NukiRequestHandler.encrypt_message(shared_key, auth_id, nr.generate(), null)
28 | characteristic.value = request
29 | val ok = gatt.writeCharacteristic(characteristic)
30 | if (!ok) {
31 | Log.e(TAG, "initial writeCharacteristic failed")
32 | closeConnection(gatt)
33 | }
34 | }
35 |
36 | override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
37 | Log.d(TAG, "onCharacteristicChanged, uiid: ${characteristic.uuid}: ${byteArrayToHexString(characteristic.value)}")
38 | data = if (data.isEmpty()) {
39 | characteristic.value
40 | } else {
41 | NukiTools.concat(data, characteristic.value)
42 | }
43 | val message: ByteArray? = NukiRequestHandler.decrypt_message(shared_key, data)
44 | val command: NukiCommand? = NukiRequestHandler.parse(message)
45 | if (command == null) {
46 | Log.d(TAG, "NukiCommand is null")
47 | return
48 | } else {
49 | data = ByteArray(0)
50 | }
51 | if (command is NukiChallenge) {
52 | Log.d(TAG, "NukiCommand.NukiChallenge")
53 | val nla = NukiLockAction(lock_action, app_id, 0x00, command.nonce)
54 | val response: ByteArray? = NukiRequestHandler.encrypt_message(shared_key, auth_id, nla.generate(), null)
55 | characteristic.value = response
56 | val ok = gatt.writeCharacteristic(characteristic)
57 | if (!ok) {
58 | Log.e(TAG, "writeCharacteristic failed for NukiLockAction")
59 | closeConnection(gatt)
60 | }
61 | } else if (command is NukiStatus) {
62 | Log.d(TAG, "NukiCommand.NukiStatus")
63 | if (command.status == NukiStatus.STATUS_COMPLETE) {
64 | // do not wait until the Nuki closes the connection
65 | closeConnection(gatt)
66 | }
67 | } else if (command is NukiStates) {
68 | Log.d(TAG, "NukiCommand.NukiStates")
69 | val ns = command
70 | var extra = ""
71 | if (ns.battery_critical == 0x01) {
72 | extra = " (Battery Critical!)"
73 | }
74 | listener.onTaskResult(door_id, action, ReplyCode.SUCCESS, NukiTools.getLockState(ns.lock_state) + extra)
75 | } else if (command is NukiError) {
76 | Log.d(TAG, "NukiCommand.NukiError")
77 | listener.onTaskResult(door_id, action, ReplyCode.REMOTE_ERROR, command.asString())
78 | closeConnection(gatt)
79 | } else {
80 | Log.e(TAG, "Unhandled command")
81 | closeConnection(gatt)
82 | }
83 | }
84 |
85 | companion object {
86 | private const val TAG = "LockActionCallback"
87 | }
88 |
89 | init {
90 | shared_key = hexStringToByteArray(setup.shared_key)
91 | auth_id = setup.auth_id
92 | app_id = setup.app_id
93 | this.lock_action = lock_action
94 | }
95 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/nuki/NukiReadLockStateCallback.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.nuki
2 |
3 | import app.trigger.Utils.byteArrayToHexString
4 | import app.trigger.Utils.hexStringToByteArray
5 | import app.trigger.DoorReply.ReplyCode
6 | import android.bluetooth.BluetoothGattCharacteristic
7 | import android.bluetooth.BluetoothGatt
8 | import app.trigger.*
9 |
10 | internal class NukiReadLockStateCallback(door_id: Int, action: MainActivity.Action, listener: OnTaskCompleted, setup: NukiDoor)
11 | : NukiCallback(door_id, action, listener, KEYTURNER_SERVICE_UUID, KEYTURNER_USDIO_XTERISTIC_UUID) {
12 | var auth_id: Long
13 | var shared_key: ByteArray
14 | var data: ByteArray = ByteArray(0)
15 |
16 | override fun onConnected(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
17 | Log.d(TAG, "onConnected")
18 | val nr = NukiCommand.NukiRequest(0x0c)
19 | val request: ByteArray? = NukiRequestHandler.encrypt_message(shared_key, auth_id, nr.generate(), null)
20 | characteristic.value = request
21 | val ok = gatt.writeCharacteristic(characteristic)
22 | if (!ok) {
23 | Log.e(TAG, "initial writeCharacteristic failed")
24 | closeConnection(gatt)
25 | }
26 | }
27 |
28 | override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
29 | Log.d(TAG, "onCharacteristicChanged, uiid: ${characteristic.uuid}: ${byteArrayToHexString(characteristic.value)}")
30 | data = if (data == null) {
31 | characteristic.value
32 | } else {
33 | NukiTools.concat(data, characteristic.value)
34 | }
35 | val message: ByteArray? = NukiRequestHandler.decrypt_message(shared_key, data)
36 | val m: NukiCommand? = NukiRequestHandler.parse(message)
37 | if (m == null) {
38 | Log.d(TAG, "NukiCommand is null")
39 | return
40 | } else {
41 | data = ByteArray(0)
42 | }
43 |
44 | if (m is NukiCommand.NukiStates) {
45 | val ns = m
46 | var extra = ""
47 | if (ns.battery_critical == 0x01) {
48 | extra = " (Battery Critical!)"
49 | }
50 | listener.onTaskResult(
51 | door_id, action, ReplyCode.SUCCESS, NukiTools.getLockState(ns.lock_state) + extra
52 | )
53 |
54 | // do not wait until the Nuki closes the connection
55 | closeConnection(gatt)
56 | } else if (m is NukiCommand.NukiError) {
57 | listener.onTaskResult(door_id, action, ReplyCode.REMOTE_ERROR, m.asString())
58 | closeConnection(gatt)
59 | } else {
60 | Log.e(TAG, "Unhandled command.")
61 | closeConnection(gatt)
62 | }
63 | }
64 |
65 | companion object {
66 | private const val TAG = "ReadLockStateCallback"
67 | }
68 |
69 | init {
70 | shared_key = hexStringToByteArray(setup.shared_key)
71 | auth_id = setup.auth_id
72 | }
73 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/ssh/GenerateIdentityTask.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.ssh
2 |
3 | import android.os.AsyncTask
4 | import app.trigger.Log
5 | import com.trilead.ssh2.crypto.keys.Ed25519Provider
6 | import java.lang.Exception
7 | import java.security.KeyPairGenerator
8 | import java.security.SecureRandom
9 |
10 | internal class GenerateIdentityTask(var listener: OnTaskCompleted) : AsyncTask() {
11 | var keypair: KeyPairBean? = null
12 |
13 | companion object {
14 | private const val TAG = "GenerateIdentityTask"
15 |
16 | init {
17 | Log.d(TAG, "Ed25519Provider.insertIfNeeded2")
18 | // Since this class deals with Ed25519 keys, we need to make sure this is available.
19 | Ed25519Provider.insertIfNeeded()
20 | }
21 | }
22 |
23 | interface OnTaskCompleted {
24 | fun onGenerateIdentityTaskCompleted(message: String?, keypair: KeyPairBean?)
25 | }
26 |
27 | private fun convertAlgorithmName(algorithm: String): String {
28 | return if ("EdDSA" == algorithm) {
29 | KeyPairBean.KEY_TYPE_ED25519
30 | } else {
31 | algorithm
32 | }
33 | }
34 |
35 | override fun doInBackground(vararg params: Any?): String? {
36 | if (params.size != 1) {
37 | Log.e(TAG, "Unexpected number of params.")
38 | return "Internal Error"
39 | }
40 | keypair = try {
41 | val type = params[0] as String
42 | if (type == "ED25519") {
43 | createKeyPair(KeyPairBean.KEY_TYPE_ED25519, 256)
44 | } else if (type == "ECDSA-384") {
45 | createKeyPair(KeyPairBean.KEY_TYPE_EC, 384)
46 | } else if (type == "ECDSA-521") {
47 | createKeyPair(KeyPairBean.KEY_TYPE_EC, 521)
48 | } else if (type == "RSA-2048") {
49 | createKeyPair(KeyPairBean.KEY_TYPE_RSA, 2048)
50 | } else if (type == "RSA-4096") {
51 | createKeyPair(KeyPairBean.KEY_TYPE_RSA, 4096)
52 | } else if (type == "DSA-1024") {
53 | createKeyPair(KeyPairBean.KEY_TYPE_DSA, 1024)
54 | } else {
55 | return "Unknown key type: $type"
56 | }
57 | } catch (e: Exception) {
58 | return e.message
59 | }
60 | return "Done"
61 | }
62 |
63 | override fun onPostExecute(message: String?) {
64 | listener.onGenerateIdentityTaskCompleted(message, keypair)
65 | }
66 |
67 | fun createKeyPair(type: String, bits: Int): KeyPairBean? {
68 | val random = SecureRandom()
69 |
70 | // Work around JVM bug
71 | //random.nextInt();
72 | //random.setSeed(entropy); //TODO!
73 | try {
74 | val keyPairGen = KeyPairGenerator.getInstance(type)
75 | keyPairGen.initialize(bits, random)
76 | val pair = keyPairGen.generateKeyPair()
77 | val priv = pair.private
78 | val pub = pair.public
79 |
80 | //Log.d(TAG, "PrivateKey: " + priv.getAlgorithm() + " " + priv.getFormat() + " " + priv.getEncoded().length);
81 | //Log.d(TAG, "PublicKey: " + pub.getAlgorithm() + " " + pub.getFormat() + " " + pub.getEncoded().length);
82 | val secret = "" // password for encrypted key
83 |
84 | //Log.d(TAG, "private: " + PubkeyUtils.formatKey(priv));
85 | Log.d(TAG, "public: ${PubkeyUtils.formatKey(pub)}") // public: Key[algorithm=EdDSA, format=X.509, bytes=44]
86 | val privateKey = PubkeyUtils.getEncodedPrivate(priv, secret).clone()
87 | val publicKey = pub.encoded.clone()
88 | return KeyPairBean(type, privateKey, publicKey, false)
89 | } catch (e: Exception) {
90 | Log.e(TAG, e.toString())
91 | }
92 | return null
93 | }
94 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/ssh/KeyPairBean.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.ssh
2 |
3 | import app.trigger.Log
4 | import com.trilead.ssh2.crypto.PEMDecoder
5 | import java.io.IOException
6 | import java.io.Serializable
7 | import java.lang.Exception
8 | import java.lang.RuntimeException
9 | import java.lang.StringBuilder
10 | import java.security.NoSuchAlgorithmException
11 | import java.security.spec.InvalidKeySpecException
12 | import java.util.*
13 |
14 | /*
15 | * A wrapper for a private key in PEM (text) format.
16 | * This is necessary for type inference mechanism.
17 | * The public key is derived/extracted if needed.
18 | */
19 | class KeyPairBean(val type: String, val privateKey: ByteArray, val publicKey: ByteArray, val encrypted: Boolean) : Serializable {
20 | var nickname = ""
21 |
22 | val openSSHPublicKey: String?
23 | get() {
24 | try {
25 | val pk = PubkeyUtils.decodePublic(publicKey, type)
26 | return PubkeyUtils.convertToOpenSSHFormat(pk, nickname)
27 | } catch (e: Exception) {
28 | Log.e(TAG, "getOpenSSHPublicKey: " + e.message)
29 | }
30 | return null
31 | }
32 | val openSSHPrivateKey: String?
33 | get() {
34 | try {
35 | return if (type == KEY_TYPE_IMPORTED) {
36 | String(privateKey)
37 | } else {
38 | val pk = PubkeyUtils.decodePrivate(privateKey, type)
39 | PubkeyUtils.exportPEM(pk, null)
40 | }
41 | } catch (e: Exception) {
42 | Log.e(TAG, "getOpenSSHPrivateKey: " + e.message)
43 | }
44 | return null
45 | }
46 |
47 | // 256 bit, but this might give the wrong impression regarding security
48 | val description: String
49 | get() = if (KEY_TYPE_IMPORTED == type) {
50 | var type = ""
51 | try {
52 | val struct = PEMDecoder.parsePEM(String(privateKey).toCharArray())
53 | type = when (struct.pemType) {
54 | PEMDecoder.PEM_RSA_PRIVATE_KEY -> "RSA"
55 | PEMDecoder.PEM_DSA_PRIVATE_KEY -> "DSA"
56 | PEMDecoder.PEM_EC_PRIVATE_KEY -> "EC"
57 | PEMDecoder.PEM_OPENSSH_PRIVATE_KEY -> "OpenSSH"
58 | else -> throw RuntimeException("Unexpected key type: ${struct.pemType}")
59 | }
60 | } catch (e: IOException) {
61 | Log.e(TAG, "Error decoding IMPORTED public key: $e")
62 | }
63 | String.format("%s unknown-bit", type)
64 | } else {
65 | var bits: Int? = null
66 | try {
67 | bits = PubkeyUtils.getBitStrength(publicKey, type)
68 | } catch (ignored: NoSuchAlgorithmException) {
69 | } catch (ignored: InvalidKeySpecException) {
70 | }
71 | val sb = StringBuilder()
72 | if (KEY_TYPE_RSA == type) {
73 | sb.append(String.format(Locale.getDefault(), "RSA %d-bit", bits))
74 | } else if (KEY_TYPE_DSA == type) {
75 | sb.append(String.format(Locale.getDefault(), "DSA %d-bit", 1024))
76 | } else if (KEY_TYPE_EC == type) {
77 | sb.append(String.format(Locale.getDefault(), "EC %d-bit", bits))
78 | } else if (KEY_TYPE_ED25519 == type) {
79 | sb.append("ED25519") // 256 bit, but this might give the wrong impression regarding security
80 | } else {
81 | sb.append("Unknown key type")
82 | }
83 | if (encrypted) {
84 | sb.append(" (encrypted)")
85 | }
86 | sb.toString()
87 | }
88 |
89 | companion object {
90 | private const val TAG = "KeyPairBean"
91 | const val KEY_TYPE_RSA = "RSA"
92 | const val KEY_TYPE_DSA = "DSA"
93 | const val KEY_TYPE_IMPORTED = "IMPORTED" // imported PEM key
94 | const val KEY_TYPE_EC = "EC"
95 | const val KEY_TYPE_ED25519 = "ED25519"
96 | }
97 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/ssh/RegisterIdentityTask.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.ssh
2 |
3 | import app.trigger.Utils.readInputStreamWithTimeout
4 | import app.trigger.Utils.rebuildAddress
5 | import app.trigger.Utils.createSocketAddress
6 | import java.io.DataOutputStream
7 | import java.lang.Exception
8 | import java.net.Socket
9 |
10 | internal class RegisterIdentityTask(private val listener: OnTaskCompleted, private val address: String, private val keypair: KeyPairBean) : Thread() {
11 | interface OnTaskCompleted {
12 | fun onRegisterIdentityTaskCompleted(message: String?)
13 | }
14 |
15 | override fun run() {
16 | try {
17 | val addr = createSocketAddress(
18 | rebuildAddress(address, 0)
19 | )
20 | if (addr.port == 0) {
21 | listener.onRegisterIdentityTaskCompleted("Missing port, use :")
22 | return
23 | }
24 | val client = Socket(addr.address, addr.port)
25 | val os = client.getOutputStream()
26 | val `is` = client.getInputStream()
27 | val writer = DataOutputStream(os)
28 |
29 | // send public key in PEM format
30 | os.write(keypair.openSSHPublicKey!!.toByteArray())
31 | os.flush()
32 | val reply = readInputStreamWithTimeout(`is`, 1024, 1000)
33 | client.close()
34 | if (reply.isNotEmpty()) {
35 | listener.onRegisterIdentityTaskCompleted(reply)
36 | } else {
37 | listener.onRegisterIdentityTaskCompleted("Done")
38 | }
39 | } catch (e: Exception) {
40 | listener.onRegisterIdentityTaskCompleted(e.toString())
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/app/trigger/ssh/SshTools.kt:
--------------------------------------------------------------------------------
1 | package app.trigger.ssh
2 |
3 | import app.trigger.Log
4 | import app.trigger.Utils.byteArrayToHexString
5 | import app.trigger.Utils.hexStringToByteArray
6 | import org.json.JSONObject
7 | import org.json.JSONException
8 | import com.trilead.ssh2.crypto.PEMDecoder
9 | import com.trilead.ssh2.crypto.Base64
10 | import java.io.*
11 | import java.lang.Exception
12 | import java.security.KeyPair
13 |
14 |
15 | object SshTools {
16 | private const val TAG = "SshTools"
17 |
18 | fun serializeKeyPair(keypair: KeyPairBean?): String? {
19 | if (keypair == null) {
20 | return ""
21 | }
22 |
23 | try {
24 | val obj = JSONObject()
25 | obj.put("type", keypair.type)
26 | obj.put("privateKey", byteArrayToHexString(keypair.privateKey))
27 | obj.put("publicKey", byteArrayToHexString(keypair.publicKey))
28 | obj.put("encrypted", keypair.encrypted)
29 | return obj.toString()
30 | } catch (e: JSONException) {
31 | Log.e(TAG, "serializeKeyPair: $e")
32 | }
33 | return null
34 | }
35 |
36 | fun deserializeKeyPair(str: String?): KeyPairBean? {
37 | return if (str == null || str.length == 0) {
38 | null
39 | } else try {
40 | val obj = JSONObject(str)
41 | KeyPairBean(
42 | obj.getString("type"),
43 | hexStringToByteArray(obj.getString("privateKey")),
44 | hexStringToByteArray(obj.getString("publicKey")),
45 | obj.getBoolean("encrypted")
46 | )
47 | } catch (e: JSONException) {
48 | Log.e(TAG, "deserializeKeyPair: $e")
49 |
50 | // fallback for old
51 | deserializeKeyPair_3_2_3(str)
52 | }
53 | }
54 |
55 | fun deserializeKeyPair_3_2_3(str: String?): KeyPairBean? {
56 | if (str == null || str.length == 0) {
57 | return null
58 | }
59 | try {
60 | return parsePrivateKeyPEM(str)
61 | } catch (e: Exception) {
62 | Log.e(TAG, "deserialize error: $e")
63 | }
64 | return null
65 | }
66 |
67 | // for <= 1.9.1
68 | fun deserializeKeyPair_1_9_1(str: String?): KeyPairBean? {
69 | if (str == null || str.length == 0) {
70 | return null
71 | }
72 | try {
73 | // base64 string to bytes
74 | val bytes = Base64.decode(str.toCharArray())
75 |
76 | // bytes to KeyPairData
77 | val bais = ByteArrayInputStream(bytes)
78 | val ios = ObjectInputStream(bais)
79 | val obj = ios.readObject() as KeyPairData
80 | return parsePrivateKeyPEM(String(obj.prvkey))
81 | } catch (e: Exception) {
82 | Log.e(TAG, "deserialize error: $e")
83 | }
84 | return null
85 | }
86 |
87 | private fun readPKCS8Key(keyData: ByteArray): KeyPair? {
88 | val reader = BufferedReader(InputStreamReader(ByteArrayInputStream(keyData)))
89 |
90 | // parse the actual key once to check if its encrypted
91 | // then save original file contents into our database
92 | try {
93 | val keyBytes = ByteArrayOutputStream()
94 | var line: String?
95 | var inKey = false
96 |
97 | while (reader.readLine().also { line = it } != null) {
98 | if (line == PubkeyUtils.PKCS8_START) {
99 | inKey = true
100 | } else if (line == PubkeyUtils.PKCS8_END) {
101 | break
102 | } else if (inKey) {
103 | keyBytes.write(line!!.toByteArray(charset("US-ASCII")))
104 | }
105 | }
106 | if (keyBytes.size() > 0) {
107 | val decoded = Base64.decode(keyBytes.toString().toCharArray())
108 | return PubkeyUtils.recoverKeyPair(decoded)
109 | }
110 | } catch (e: Exception) {
111 | return null
112 | }
113 | return null
114 | }
115 |
116 | private fun convertAlgorithmName(algorithm: String): String {
117 | return if ("EdDSA" == algorithm) {
118 | KeyPairBean.KEY_TYPE_ED25519
119 | } else {
120 | algorithm
121 | }
122 | }
123 |
124 | fun parsePrivateKeyPEM(keyData: String): KeyPairBean? {
125 | val kp2 = readPKCS8Key(keyData.toByteArray())
126 | if (kp2 != null) {
127 | val algorithm = convertAlgorithmName(kp2.private.algorithm)
128 | return KeyPairBean(algorithm, kp2.private.encoded, kp2.public.encoded, false)
129 | } else {
130 | try {
131 | val struct = PEMDecoder.parsePEM(keyData.toCharArray())
132 | val encrypted = PEMDecoder.isPEMEncrypted(struct)
133 | return if (!encrypted) {
134 | val kp = PEMDecoder.decode(struct, null)
135 | val algorithm = convertAlgorithmName(kp.private.algorithm)
136 | KeyPairBean(algorithm, kp.private.encoded, kp.public.encoded, encrypted)
137 | } else {
138 | KeyPairBean(KeyPairBean.KEY_TYPE_IMPORTED, keyData.toByteArray(), ByteArray(0), encrypted)
139 | }
140 | } catch (e: IOException) {
141 | Log.e(TAG, "Problem parsing imported private key: $e")
142 | }
143 | }
144 | return null
145 | }
146 |
147 | // helper class that holds the content of the old id_rsa/id_rsa.pub file content (PEM format)
148 | private class KeyPairData(val prvkey: ByteArray, val pubkey: ByteArray) : Serializable
149 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-hdpi/ic_action_about.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-hdpi/ic_action_edit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-hdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-mdpi/ic_action_about.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-mdpi/ic_action_edit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-mdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xhdpi/ic_action_about.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xhdpi/ic_action_edit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xhdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xhdpi/ic_action_refresh.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xxhdpi/ic_action_about.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xxhdpi/ic_action_edit.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xxhdpi/ic_action_new.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_action_scan_qr.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_action_show_qr.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
15 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_lock.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_ring.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_unlock.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/rounded_corners.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/state_closed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable/state_closed.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/state_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable/state_disabled.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/state_open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable/state_open.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/state_unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/drawable/state_unknown.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/trigger_logo.xml:
--------------------------------------------------------------------------------
1 |
6 |
11 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_about.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
21 |
22 |
23 |
24 |
29 |
30 |
38 |
39 |
47 |
48 |
55 |
56 |
63 |
64 |
71 |
72 |
79 |
80 |
87 |
88 |
95 |
96 |
103 |
104 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_abstract_certificate.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
18 |
19 |
20 |
21 |
27 |
28 |
35 |
36 |
44 |
45 |
52 |
53 |
62 |
63 |
64 |
65 |
70 |
71 |
78 |
79 |
86 |
87 |
94 |
95 |
96 |
97 |
102 |
103 |
110 |
111 |
118 |
119 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_abstract_client_keypair.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
21 |
22 |
23 |
31 |
32 |
41 |
42 |
49 |
50 |
51 |
52 |
60 |
61 |
65 |
66 |
73 |
74 |
81 |
82 |
83 |
84 |
88 |
89 |
96 |
97 |
103 |
104 |
105 |
106 |
107 |
108 |
115 |
116 |
123 |
124 |
131 |
132 |
139 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_backup.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
19 |
20 |
21 |
22 |
28 |
29 |
35 |
36 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
21 |
22 |
23 |
30 |
31 |
38 |
39 |
46 |
47 |
54 |
55 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_license.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
21 |
22 |
23 |
28 |
29 |
34 |
35 |
40 |
41 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
14 |
15 |
19 |
20 |
21 |
22 |
27 |
28 |
36 |
37 |
43 |
44 |
55 |
56 |
67 |
68 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_qrscan.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_qrshow.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
20 |
21 |
22 |
23 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_change_string.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
14 |
15 |
26 |
27 |
33 |
34 |
41 |
42 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_delete_door.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
15 |
16 |
22 |
23 |
30 |
31 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_ssh_passphrase.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
24 |
25 |
29 |
30 |
37 |
38 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/main_spinner.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/spinner_dropdown_item_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/spinner_item_settings.xml:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-de/array.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-in/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tentang aplikasi
4 | Cadangan
5 | Duplikasi
6 | Sunting
7 | Ekspor
8 | Impor
9 | Baru
10 | Pindai code QR
11 | Tampilkan Kode QR
12 | Keterangan:
13 | Lisensi:
14 | Versi:
15 | Membatalkan
16 | Hapus
17 | Dari papan klip
18 | OK
19 | Simpan
20 | Membatalkan
21 | Sertifikat
22 | URL sertifikat
23 | Papan klip kosong
24 | Mengonfirmasi
25 | Selesai
26 | Kesalahan
27 | Ekspor
28 | Impor
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/array.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - Generic HTTPS
5 | - Generic SSH
6 | - Generic MQTT
7 | - Generic Bluetooth
8 | - Nuki SmartLock
9 |
10 |
11 |
12 | - HttpsDoorSetup
13 | - SshDoorSetup
14 | - MqttDoorSetup
15 | - BluetoothDoorSetup
16 | - NukiDoorSetup
17 |
18 |
19 |
20 | - GET
21 | - POST
22 | - PUT
23 |
24 |
25 |
26 | - GET
27 | - POST
28 | - PUT
29 |
30 |
31 |
32 | - ED25519
33 | - ECDSA (nistp384)
34 | - ECDSA (nistp521)
35 | - RSA (2048)
36 | - RSA (4096)
37 |
38 |
39 |
40 | - Fire and Forget (0)
41 | - At least once (1)
42 | - Exactly once (2)
43 |
44 |
45 |
46 | - 0
47 | - 1
48 | - 2
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 | 10dp
7 | 20dp
8 | 50dp
9 |
10 | 16sp
11 | 18sp
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '2.0.21'
5 |
6 | repositories {
7 | google()
8 | mavenCentral()
9 | }
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:8.7.3'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 |
14 |
15 | // NOTE: Do not place your application dependencies here; they belong
16 | // in the individual module build.gradle files
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/apk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/apk.png
--------------------------------------------------------------------------------
/docs/documentation.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | ## Compatible Locks
4 |
5 | Trigger makes rather generic requests and needs to be configured depending on the door system. No off the shelf door systems has been tested. So no advice can be given here. While the Nuki SmartLock has basic support now, it is rather expensive.
6 |
7 | ## Door Status
8 |
9 | Trigger reads the door status from the text returned of the HTTPS query, SSH command or MQTT server. You can set a regular expression for the `Reply Pattern (locked)` and `Reply Pattern (unlocked)` settings. The default values are `LOCKED` and `UNLOCKED`. The complete return message is always displayed in the App for a short time (with possible HTML elements stripped and truncated if needed).
10 |
11 | An example of a regular expression pattern is `"state"\s*:\s*"open"`. This would match a part of a typical JSON response.
12 |
13 | ## Auto-Select/Limit Door By SSID
14 |
15 | The door setup can be selected depending on what WiFi network the device is connected to. This can help to avoid to switch the door setup by hand. Even multiple SSIDs can be set as a comma separated list. As of release 3.1.1, this disables the setup if the connected WiFi SSID does not match.
16 |
17 | ## SSH Key Registration
18 |
19 | SSH Public Keys can be send to an IP address and port (e.g. `192.168.1.1:3333`) to be registered. There is a field and button in the SSH Key Managment for that. A simple example server to collect keys using netcat: `
20 | nc -l -k -p 3333 -c 'read key; echo "$key" >> ssh_keys.txt; echo "Your key was received!"'`. This was tested with Ncat 7.80 (https://nmap.org/ncat). Other netcat implementations might need other parameters.
21 |
22 | ## Import Link As QR-Code
23 |
24 | Instead of QR-Codes imported from other Trigger Apps, you can import simple links like `https://example.com/open?pass=secret` as QR-Code to create a simple HTTPS based door setup. Links starting with `mqtt://` and `mqtts://` will be used for MQTT and `ssh://` for SSH based door setups.
25 |
26 | HTTP(s) links may also contain a username and password to be used for basic access authentication, e.g.: `https://user:password@example.com/open_door`.
27 |
28 | ## Import SSH Key As QR-Code
29 |
30 | Use e.g. `qrencode -t ansiutf8 < ~/.ssh/id_ed25519` to show an SSH private key as QR-Code. Scan with trigger and add the server address, user, command etc..
31 |
32 | ## Limit SSH Access To The Server
33 |
34 | To limit a user to call only one command via SSH, put a line into `~/.ssh/authorized_keys` on the SSH server. E.g.:
35 |
36 | ```
37 | no-port-forwarding,no-x11-forwarding,no-agent-forwarding,command="/home/pi/bin/controldoor.sh" ssh-[keyver] [pubkeydata] [comment]
38 | ```
39 |
40 | The command send by Trigger is available as environment variable called `SSH_ORIGINAL_COMMAND` in the command script.
41 |
42 | ## MQTT Server Setup
43 |
44 | Use these two sites as a HowTo:
45 |
46 | * [Run Local MQTT Broker on OpenWrt](https://www.onetransistor.eu/2019/05/run-local-mqtt-broker-on-openwrt-router.html)
47 | * [Mosquitto on OpenWrt Router](https://www.onetransistor.eu/2019/05/mosquitto-mqtt-on-openwrt-router.html)
48 | * [Mosquitto with TLS Certificate](https://www.onetransistor.eu/2019/05/mosquitto-mqtt-tls-certificate.html)
49 |
50 | ## Nuki Smartlock Pairing
51 |
52 | Steps to pair Trigger with the Nuki Smartlock:
53 |
54 | 1. Nuki: Press the door knob button for 3 seconds until the ring lights up (pairing mode).
55 | 2. Phone: Enable Bluetooth.
56 | 3. Phone: Pair phone and Nuki Smartlock.
57 | 4. Trigger: Start App and add a new door entry with door type "Nuki SmartLock".
58 | 5. Trigger: Enter the name of the paired Nuki Smartlock into the "Lock Name/MAC" field (something like "Nuki_1DAB5E34").
59 | 6. Trigger: Enter some user name (not very important) and save the door setup.
60 | 7. Nuki: If the Nuki smartlock is not in pairing mode anymore, just press the button again.
61 | 8. Trigger: Press the open door button in Trigger. This will cause Trigger to register with the Nuki Smartlock.
62 |
63 | Now you should be able to send open/close commands.
64 |
65 | (tested with Android 10 and Nuki SmartLock 2.0)
66 |
67 | ## Build Trigger from Sources
68 |
69 | On Linux based systems:
70 |
71 | ```
72 | ./gradlew assembleRelease
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/fdroid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/fdroid.png
--------------------------------------------------------------------------------
/docs/gplay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/gplay.png
--------------------------------------------------------------------------------
/docs/screenshot_bluetooth_settings_part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_bluetooth_settings_part1.png
--------------------------------------------------------------------------------
/docs/screenshot_door_types.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_door_types.png
--------------------------------------------------------------------------------
/docs/screenshot_https_manage_tls_certificate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_https_manage_tls_certificate.png
--------------------------------------------------------------------------------
/docs/screenshot_https_settings_part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_https_settings_part1.png
--------------------------------------------------------------------------------
/docs/screenshot_https_settings_part2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_https_settings_part2.png
--------------------------------------------------------------------------------
/docs/screenshot_main_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_main_menu.png
--------------------------------------------------------------------------------
/docs/screenshot_mqtt_settings_part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_mqtt_settings_part1.png
--------------------------------------------------------------------------------
/docs/screenshot_nuki_settings_part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_nuki_settings_part1.png
--------------------------------------------------------------------------------
/docs/screenshot_ssh_key_pair.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_ssh_key_pair.png
--------------------------------------------------------------------------------
/docs/screenshot_ssh_settings_part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_ssh_settings_part1.png
--------------------------------------------------------------------------------
/docs/screenshot_ssh_settings_part2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_ssh_settings_part2.png
--------------------------------------------------------------------------------
/docs/screenshot_states.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/docs/screenshot_states.png
--------------------------------------------------------------------------------
/docs/screenshots.md:
--------------------------------------------------------------------------------
1 | # Screenshots
2 |
3 | A collection of screenshots of Trigger.
4 |
5 | ## General
6 |
7 |
8 |
9 | ## HTTPS Door
10 |
11 |
12 |
13 | ## SSH Door
14 |
15 |
16 |
17 | ## MQTT Door
18 |
19 |
20 |
21 | ## Bluetooth/BLE Door
22 |
23 |
24 |
25 | ## Nuki Door
26 |
27 |
28 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Apr 12 08:38:00 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/170.txt:
--------------------------------------------------------------------------------
1 | * button press animation
2 | * fix item selection in drop-down list
3 | * fetch/import/export https certificate
4 | * ignore certificate host name mismatch (replaces "ignore errors")
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/171.txt:
--------------------------------------------------------------------------------
1 | * register ssh public key (send to some address)
2 | * remove button for ssh key pair / https certificate
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/180.txt:
--------------------------------------------------------------------------------
1 | * QR-code import/export
2 | * clone setups
3 | * fix update from previous versions
4 | * bluetooth support (untested!)
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/190.txt:
--------------------------------------------------------------------------------
1 | * custom status images
2 | * backup data
3 | * QR codes are smaller now
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/191.txt:
--------------------------------------------------------------------------------
1 | * add camera permission
2 | * add package metadata
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/192.txt:
--------------------------------------------------------------------------------
1 | * allow different ssh key sizes
2 | * store ssh keys in a more efficient format
3 | * always remove html tags from returned text
4 | * allow imported images to use entire screen width
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/200.txt:
--------------------------------------------------------------------------------
1 | * add MQTT support
2 | * parse response the same way for every door type
3 | * remove refresh menu item
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/201.txt:
--------------------------------------------------------------------------------
1 | * only auto select by ssid on app start or wifi reconnect
2 | * more translatable strings
3 | * prevent image from touching the buttons
4 | * change some button labels and positions
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/202.txt:
--------------------------------------------------------------------------------
1 | * allow to scan qr code with http/https/tcp/ssl/ssh link. Makes a "guess" about parameters.
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/203.txt:
--------------------------------------------------------------------------------
1 | * fix missing german translation
2 | * fix error when setup name is already taken
3 | * fix upgrade error (not critical)
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/204.txt:
--------------------------------------------------------------------------------
1 | * fix crash when sharing the screen space
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/205.txt:
--------------------------------------------------------------------------------
1 | * fix "entry exists" error when saving setting
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/206.txt:
--------------------------------------------------------------------------------
1 | * register ssh public key address does not need start with tcp://
2 | * allow ssh register endpoint to send back a response
3 | * disable text suggestions
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/210.txt:
--------------------------------------------------------------------------------
1 | * add ring button for door bells
2 | * hide unused open/close/ring buttons
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/211.txt:
--------------------------------------------------------------------------------
1 | * fix update of buttons
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/220.txt:
--------------------------------------------------------------------------------
1 | * Add HTTP GET/PUT support
2 | * Improve Bluetooth support
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/221.txt:
--------------------------------------------------------------------------------
1 | * use username/password for MQTT
2 | * do not default to GET if http request method is invalid
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/222.txt:
--------------------------------------------------------------------------------
1 | * add support for Nuki SmartLock (pairing, lock, unlock)
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/223.txt:
--------------------------------------------------------------------------------
1 | * SSH: fix hang in key register process
2 | * SSH: key register url defaults to host address
3 | * Nuki: show (read only) settings
4 | * Nuki: more error messages
5 | * HTTPS: show messages also in case of http error
6 | * show settings in summary field
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/224.txt:
--------------------------------------------------------------------------------
1 | * drop permission access fine location
2 | * do not overwrite existing file on backup
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/225.txt:
--------------------------------------------------------------------------------
1 | * add option to allow https/ssh/mqtt to be used over the Internet
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/300.txt:
--------------------------------------------------------------------------------
1 | * add option to disable https certificate expiration
2 | * change app id to please the google play app store
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/301.txt:
--------------------------------------------------------------------------------
1 | * update ssh library (jcraft.com/jsch - 0.1.54)
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/310.txt:
--------------------------------------------------------------------------------
1 | * allow regex to match locked/unlocked message status of https, ssh, mqtt and bluetooth
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/311.txt:
--------------------------------------------------------------------------------
1 | * fix some SSH key crashes and import issues
2 | * force SSID match setting if used
3 | * fix SSID matching on Android 10
4 | * use Android target SDK version 29
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/312.txt:
--------------------------------------------------------------------------------
1 | * add build flavors
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/313.txt:
--------------------------------------------------------------------------------
1 | * add connected ssid to error message
2 | * make Android 10 read the SSID
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/320.txt:
--------------------------------------------------------------------------------
1 | * use new Android SDK version 30 and AndroidX
2 | * change file picker library
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/321.txt:
--------------------------------------------------------------------------------
1 | * fix crash when selecting a background picture
2 | * fix crash when using the file selector on Android 11
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/322.txt:
--------------------------------------------------------------------------------
1 | * fix crash on message display (Android 11 only)
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/330.txt:
--------------------------------------------------------------------------------
1 | * use Android storage framework
2 | * switch to SSH code from ConnectBot
3 | * adds support for ED25519 an others
4 | * allow SSH key import via QR-code
5 | * add option to ignore HTTPS certificte validity
6 | * allow export/import SSH keys via clipboard
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/331.txt:
--------------------------------------------------------------------------------
1 | * fix SSH_ORIGINAL_COMMAND for command in .ssh/authorized_keys
2 | * prevent quick sequential calls of the status command
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/332.txt:
--------------------------------------------------------------------------------
1 | * add status patterns for MQTT
2 | * allow key and password authentication for SSH
3 | * call status call only once on connection change
4 | * make SSH execution timeout configurable
5 | * defaults to 5 seconds
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/333.txt:
--------------------------------------------------------------------------------
1 | * support old style SSH keypair value
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/334.txt:
--------------------------------------------------------------------------------
1 | * fix some HTTPS hostname verification issues
2 | * make HTTP method explicit
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/335.txt:
--------------------------------------------------------------------------------
1 | * add support for HTTP Basic Authentication
2 | * e.g. https://user:password@example.com/open_door
3 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/336.txt:
--------------------------------------------------------------------------------
1 | * fix HTTP Basic Authentication
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/340.txt:
--------------------------------------------------------------------------------
1 | * add MQTT support for TLS client/server certificates
2 | * "Wifi needed" now ignores Internet availability
3 | * fix deletion of key and certificates
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/341.txt:
--------------------------------------------------------------------------------
1 | * HTTPS with server certificate and client certificate + key
2 | * fix TLS certificate activity titles (client vs. server certificate)
3 | * MQTT now uses mqtt:// and mqtts:// as link schemes
4 | * fix update to Trigger >=3.4.0 for TLS certificates
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/342.txt:
--------------------------------------------------------------------------------
1 | * fix broken WiFi SSID match
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/343.txt:
--------------------------------------------------------------------------------
1 | * show dialog to temporarly disable wifi checks
2 | * add support for passphrase protected SSH keys
3 | * fix potential crash on Android 5 when scanning a QR code
4 | * make sure new setups (e.g. via QR code) have unique names
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/344.txt:
--------------------------------------------------------------------------------
1 | * fix crash when there are two SSH doors
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/400.txt:
--------------------------------------------------------------------------------
1 | * convert codebase to Kotlin
2 | * request bluetooth scan permissions for Android 12
3 | * support adaptive and monochrome launcher icon
4 | * fix require WiFi for SSH door setup
5 | * change license to GPL-3.0-or-later
6 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/401.txt:
--------------------------------------------------------------------------------
1 | * update to SDK version 35 (Android 15)
2 | * remove support for deprecated SharedPreferences
3 | * caused major rewrite
4 | * fix UTF-8 character support for QR code export
5 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/402.txt:
--------------------------------------------------------------------------------
1 | * make icon rounded
2 | * fix crash in for client-pair activity for HTTPS and MQTT
3 | * lots of small layout improvements
4 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/403.txt:
--------------------------------------------------------------------------------
1 | * fix toobar and system status bar overlap (Android 15)
2 | * fix crash in the Nuki settings
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/404.txt:
--------------------------------------------------------------------------------
1 | * support separate HTTP method per query
2 | * replace some deprecated permission code
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/405.txt:
--------------------------------------------------------------------------------
1 | * fix storage of certificates
2 | * show default images in image selection
3 | * fix MQTT door certificate settings storage
4 | * add Russian translation, thanks tokito@gmail.com
5 | * add Indonesian translation, thanks yurtpage+translate@gmail.com
6 | * improve German translation, thanks moritzwarning@web.de
7 | * replace remaining deprecated permission code
8 | * make more strings translatable
9 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/406.txt:
--------------------------------------------------------------------------------
1 | * set door state to open/closed in case
2 | the open/close command is successful
3 | * this applies only if the reply patterns are set empty
4 | * translation improvements
5 |
--------------------------------------------------------------------------------
/metadata/en-US/changelogs/407.txt:
--------------------------------------------------------------------------------
1 | * really minor cleanup and fixes
2 |
--------------------------------------------------------------------------------
/metadata/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Open and close doors, ring door bells and see the door state.
2 | Supported HTTPS/SSH/MQTT/Bluetooth and the Nuki SmartLock.
3 |
4 | Features:
5 |
6 | * Support of HTTPS, SSH, Bluetooth and MQTT.
7 | * Support for Nuki SmartLock.
8 | * Multiple door profiles.
9 | * Auto select profiles by a SSID of connected WiFi.
10 | * HTTPS server/client certificate support.
11 | * SSH key generation (ED25519, RSA) with a passphrase.
12 | * Custom status images.
13 | * QR code support.
14 | * Support of backup.
15 |
--------------------------------------------------------------------------------
/metadata/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/metadata/en-US/images/icon.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/01_setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/metadata/en-US/images/phoneScreenshots/01_setup.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/02_settings_https_part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/metadata/en-US/images/phoneScreenshots/02_settings_https_part1.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/03_settings_https_part2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mwarning/trigger/c0b9a351a6f7b58341c5fb24650e383287d18533/metadata/en-US/images/phoneScreenshots/03_settings_https_part2.png
--------------------------------------------------------------------------------
/metadata/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Open doors via HTTPS/SSH/Bluetooth/MQTT.
--------------------------------------------------------------------------------
/metadata/ru-RU/full_description.txt:
--------------------------------------------------------------------------------
1 | Открыть или закрыть двери, звонить и посмотреть состояние двери.
2 | Поддерживаются общие HTTPS/SSH/MQTT/Bluetooth и Nuki SmartLock.
3 |
4 | Возможности:
5 |
6 | - Поддержка HTTPS, SSH, Bluetooth и MQTT.
7 | - Поддержка Nuki SmartLock.
8 | - Многодверные профили.
9 | - Автоматически выбирать профили по имени подсоединёной сети WiFi.
10 | - Поддержка HTTPS серверного или клиентского сертификата.
11 | - Генерация SSH ключей (ED25519, RSA) с парольными фразами.
12 | - Пользовательские изображения состояния двери.
13 | - Поддержка QR кодов.
14 | - Резервная копия.
15 |
16 |
--------------------------------------------------------------------------------
/metadata/ru-RU/short_description.txt:
--------------------------------------------------------------------------------
1 | Открывайте двери через Wi-Fi, используя HTTPS/SSH/Bluetooth/MQTT.
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Trigger"
16 | include ':app'
17 |
--------------------------------------------------------------------------------