From ee00d9defbefa8a24e1de28b8fadcf45f0fe522d Mon Sep 17 00:00:00 2001 From: Florianisme Date: Sun, 19 Mar 2023 11:56:47 +0100 Subject: [PATCH 01/72] #7 Add remote shutdown to Device configuration layout --- .../main/res/layout/content_modify_device.xml | 492 ++++++++++++------ app/src/main/res/values/strings.xml | 7 + 2 files changed, 328 insertions(+), 171 deletions(-) diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index f3d2edf..1cbc34e 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -1,202 +1,352 @@ - + android:fillViewport="true"> - - - - - + + + + - + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:hint="@string/add_device_name" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_title_general"> - - - + + + - + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:hint="@string/add_device_status_ip" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_name_layout" + app:placeholderText="192.168.0.100"> + + - - - - - + + - + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:hint="@string/add_device_mac_address" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_title_connectivity" + app:placeholderText="AB:12:CD:34:EF:56"> - - - + + + + + + + + + + + + + + + + - + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:text="@string/add_device_shutdown" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_port_layout" /> - - - - - - - + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:text="@string/add_device_shutdown_enable" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_title_remote_shutdown" /> + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:hint="@string/add_device_secure_on" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_title_security"> + + + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 758bc01..1055724 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,13 @@ Broadcast Address Autofill Broadcast Address SecureOn Password (optional) + Remote Shutdown + Enable Remote Shutdown + SSH Address + SSH Port + Username + Password + Shutdown Command Save Delete From 0070cd57e82c76f209aedd8e63f5144b694c4dda Mon Sep 17 00:00:00 2001 From: Florianisme Date: Sun, 19 Mar 2023 12:14:22 +0100 Subject: [PATCH 02/72] #7 Add database migration and adjust Entity model --- .../wakeonlan/persistence/AppDatabase.java | 2 +- .../persistence/DatabaseInstanceManager.java | 3 ++- .../persistence/entities/DeviceEntity.java | 15 +++++++++++++ .../migrations/MigrationFrom3To4.java | 22 +++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/AppDatabase.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/AppDatabase.java index c43ce16..7df44bc 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/AppDatabase.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/AppDatabase.java @@ -5,7 +5,7 @@ import de.florianisme.wakeonlan.persistence.entities.DeviceEntity; -@Database(entities = {DeviceEntity.class}, version = 3, exportSchema = false) +@Database(entities = {DeviceEntity.class}, version = 4, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public abstract DeviceDao deviceDao(); } \ No newline at end of file diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/DatabaseInstanceManager.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/DatabaseInstanceManager.java index 579a555..2dcc533 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/DatabaseInstanceManager.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/DatabaseInstanceManager.java @@ -6,6 +6,7 @@ import de.florianisme.wakeonlan.persistence.migrations.MigrationFrom1To2; import de.florianisme.wakeonlan.persistence.migrations.MigrationFrom2To3; +import de.florianisme.wakeonlan.persistence.migrations.MigrationFrom3To4; public class DatabaseInstanceManager { @@ -15,7 +16,7 @@ public static synchronized AppDatabase getInstance(Context context) { if (appDatabase == null) { appDatabase = Room.databaseBuilder(context, AppDatabase.class, "database-name") .allowMainThreadQueries() - .addMigrations(new MigrationFrom1To2(), new MigrationFrom2To3()) + .addMigrations(new MigrationFrom1To2(), new MigrationFrom2To3(), new MigrationFrom3To4()) .build(); } return appDatabase; diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java index 7257362..94a49ed 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java @@ -29,6 +29,21 @@ public class DeviceEntity { @ColumnInfo(name = "secure_on_password") public String secureOnPassword; + @ColumnInfo(name = "ssh_ip") + public String sshIp; + + @ColumnInfo(name = "ssh_port") + public Integer sshPort; + + @ColumnInfo(name = "ssh_user") + public String sshUsername; + + @ColumnInfo(name = "ssh_password") + public String sshPassword; + + @ColumnInfo(name = "ssh_command") + public String sshCommand; + @Ignore public DeviceEntity(int id, String name, String macAddress, String broadcastAddress, int port, String statusIp, String secureOnPassword) { this.id = id; diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java new file mode 100644 index 0000000..454f050 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java @@ -0,0 +1,22 @@ +package de.florianisme.wakeonlan.persistence.migrations; + +import androidx.annotation.NonNull; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +public class MigrationFrom3To4 extends Migration { + + public MigrationFrom3To4() { + super(3, 4); + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_ip' TEXT"); + database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_port' INTEGER"); + database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_user' TEXT"); + database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_password' TEXT"); + database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_command' TEXT"); + } + +} From aa3ebd9668bbf670ce356f2b8c783d63eb4a9ae3 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Sun, 19 Mar 2023 12:24:19 +0100 Subject: [PATCH 03/72] #7 Add new fields to internal models --- .../persistence/entities/DeviceEntity.java | 4 ++-- .../mapper/DeviceEntityMapper.java | 3 ++- .../migrations/MigrationFrom3To4.java | 2 +- .../wakeonlan/persistence/models/Device.java | 18 ++++++++++++++- .../ui/backup/model/DeviceBackupModel.java | 23 ++++++++++++++++++- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java index 94a49ed..de94f57 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java @@ -29,8 +29,8 @@ public class DeviceEntity { @ColumnInfo(name = "secure_on_password") public String secureOnPassword; - @ColumnInfo(name = "ssh_ip") - public String sshIp; + @ColumnInfo(name = "ssh_address") + public String sshAddress; @ColumnInfo(name = "ssh_port") public Integer sshPort; diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java index 3744839..bd03503 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java @@ -10,7 +10,8 @@ public Device entityToModel(DeviceEntity entity) { if (entity == null) { return new Device(); } - return new Device(entity.id, entity.name, entity.macAddress, entity.broadcastAddress, entity.port, entity.statusIp, entity.secureOnPassword); + return new Device(entity.id, entity.name, entity.macAddress, entity.broadcastAddress, entity.port, entity.statusIp, entity.secureOnPassword, + entity.sshAddress, entity.sshPort, entity.sshUsername, entity.sshPassword, entity.sshCommand); } @Override diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java index 454f050..7420bf4 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java @@ -12,7 +12,7 @@ public MigrationFrom3To4() { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_ip' TEXT"); + database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_address' TEXT"); database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_port' INTEGER"); database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_user' TEXT"); database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_password' TEXT"); diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java index 343e56e..8e7ff4e 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java @@ -16,7 +16,18 @@ public class Device { public String secureOnPassword; - public Device(int id, String name, String macAddress, String broadcastAddress, int port, String statusIp, String secureOnPassword) { + public String sshAddress; + + public int sshPort; + + public String sshUsername; + + public String sshPassword; + + public String sshCommand; + + public Device(int id, String name, String macAddress, String broadcastAddress, int port, String statusIp, String secureOnPassword, + String sshAddress, int sshPort, String sshUsername, String sshPassword, String sshCommand) { this.id = id; this.name = name; this.macAddress = macAddress; @@ -24,6 +35,11 @@ public Device(int id, String name, String macAddress, String broadcastAddress, i this.port = port; this.statusIp = statusIp; this.secureOnPassword = secureOnPassword; + this.sshAddress = sshAddress; + this.sshPort = sshPort; + this.sshUsername = sshUsername; + this.sshPassword = sshPassword; + this.sshCommand = sshCommand; } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java index 870228d..5c49e7b 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java @@ -43,6 +43,21 @@ to JSON with its obfuscated name ("a", "b", and so on). @JsonAlias("g") public String secureOnPassword; + @JsonProperty("ssh_address") + public String sshAddress; + + @JsonProperty("ssh_port") + public int sshPort; + + @JsonProperty("ssh_username") + public String sshUsername; + + @JsonProperty("ssh_password") + public String sshPassword; + + @JsonProperty("ssh_command") + public String sshCommand; + public DeviceBackupModel(Device device) { this.id = device.id; this.name = device.name; @@ -51,10 +66,16 @@ public DeviceBackupModel(Device device) { this.port = device.port; this.statusIp = device.statusIp; this.secureOnPassword = device.secureOnPassword; + this.sshAddress = device.sshAddress; + this.sshPort = device.sshPort; + this.sshUsername = device.sshUsername; + this.sshPassword = device.sshPassword; + this.sshCommand = device.sshCommand; } public Device toModel() { - return new Device(this.id, this.name, this.macAddress, this.broadcastAddress, this.port, this.statusIp, this.secureOnPassword); + return new Device(this.id, this.name, this.macAddress, this.broadcastAddress, this.port, this.statusIp, this.secureOnPassword, + this.sshAddress, this.sshPort, this.sshUsername, this.sshPassword, this.sshCommand); } @SuppressWarnings("unused") From 4c8ff5c46f3d9ad35a3f0cd07a7fb1cada857870 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Sun, 19 Mar 2023 21:56:10 +0100 Subject: [PATCH 04/72] #7 Restructure inputs in View --- .../main/res/layout/content_modify_device.xml | 248 +++++++++--------- 1 file changed, 129 insertions(+), 119 deletions(-) diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index 1cbc34e..50b445d 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -180,7 +180,7 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/device_port_layout" /> - - - - - - - - + + + + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" + android:text="@string/device_title_security" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_remote_shutdown_command" /> - + + app:layout_constraintTop_toBottomOf="@id/device_remote_shutdown_container"> Date: Sun, 19 Mar 2023 21:56:44 +0100 Subject: [PATCH 05/72] #7 Add switch state to device models and entity --- .../wakeonlan/persistence/entities/DeviceEntity.java | 3 +++ .../wakeonlan/persistence/mapper/DeviceEntityMapper.java | 2 +- .../wakeonlan/persistence/migrations/MigrationFrom3To4.java | 1 + .../de/florianisme/wakeonlan/persistence/models/Device.java | 5 ++++- .../wakeonlan/ui/backup/model/DeviceBackupModel.java | 6 +++++- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java index de94f57..a674270 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java @@ -29,6 +29,9 @@ public class DeviceEntity { @ColumnInfo(name = "secure_on_password") public String secureOnPassword; + @ColumnInfo(name = "enable_remote_shutdown", defaultValue = "false") + public boolean enableRemoteShutdown; + @ColumnInfo(name = "ssh_address") public String sshAddress; diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java index bd03503..a90df77 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java @@ -11,7 +11,7 @@ public Device entityToModel(DeviceEntity entity) { return new Device(); } return new Device(entity.id, entity.name, entity.macAddress, entity.broadcastAddress, entity.port, entity.statusIp, entity.secureOnPassword, - entity.sshAddress, entity.sshPort, entity.sshUsername, entity.sshPassword, entity.sshCommand); + entity.enableRemoteShutdown, entity.sshAddress, entity.sshPort, entity.sshUsername, entity.sshPassword, entity.sshCommand); } @Override diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java index 7420bf4..b61de9f 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/migrations/MigrationFrom3To4.java @@ -12,6 +12,7 @@ public MigrationFrom3To4() { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'enable_remote_shutdown' BOOLEAN"); database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_address' TEXT"); database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_port' INTEGER"); database.execSQL("ALTER TABLE 'Devices' ADD COLUMN 'ssh_user' TEXT"); diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java index 8e7ff4e..43828f7 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java @@ -16,6 +16,8 @@ public class Device { public String secureOnPassword; + public boolean remoteShutdownEnabled; + public String sshAddress; public int sshPort; @@ -27,7 +29,7 @@ public class Device { public String sshCommand; public Device(int id, String name, String macAddress, String broadcastAddress, int port, String statusIp, String secureOnPassword, - String sshAddress, int sshPort, String sshUsername, String sshPassword, String sshCommand) { + boolean remoteShutdownEnabled, String sshAddress, int sshPort, String sshUsername, String sshPassword, String sshCommand) { this.id = id; this.name = name; this.macAddress = macAddress; @@ -35,6 +37,7 @@ public Device(int id, String name, String macAddress, String broadcastAddress, i this.port = port; this.statusIp = statusIp; this.secureOnPassword = secureOnPassword; + this.remoteShutdownEnabled = remoteShutdownEnabled; this.sshAddress = sshAddress; this.sshPort = sshPort; this.sshUsername = sshUsername; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java index 5c49e7b..41dfbf1 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/model/DeviceBackupModel.java @@ -43,6 +43,9 @@ to JSON with its obfuscated name ("a", "b", and so on). @JsonAlias("g") public String secureOnPassword; + @JsonProperty("remote_shutdown_enabled") + public boolean remoteShutdownEnabled; + @JsonProperty("ssh_address") public String sshAddress; @@ -66,6 +69,7 @@ public DeviceBackupModel(Device device) { this.port = device.port; this.statusIp = device.statusIp; this.secureOnPassword = device.secureOnPassword; + this.remoteShutdownEnabled = device.remoteShutdownEnabled; this.sshAddress = device.sshAddress; this.sshPort = device.sshPort; this.sshUsername = device.sshUsername; @@ -75,7 +79,7 @@ public DeviceBackupModel(Device device) { public Device toModel() { return new Device(this.id, this.name, this.macAddress, this.broadcastAddress, this.port, this.statusIp, this.secureOnPassword, - this.sshAddress, this.sshPort, this.sshUsername, this.sshPassword, this.sshCommand); + this.remoteShutdownEnabled, this.sshAddress, this.sshPort, this.sshUsername, this.sshPassword, this.sshCommand); } @SuppressWarnings("unused") From 300818a60a718139f8eff39b0d6ac13acaf3e312 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Sun, 19 Mar 2023 21:57:19 +0100 Subject: [PATCH 06/72] #7 Add basic validation of remote shutdown inputs --- .../ui/modify/ModifyDeviceActivity.java | 55 ++++++++++++++++++- .../ConditionalInputNotEmptyValidator.java | 37 +++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java index 3ef36f3..d73c341 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java @@ -10,18 +10,25 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SwitchCompat; +import androidx.constraintlayout.widget.ConstraintLayout; import com.google.android.material.textfield.MaterialAutoCompleteTextView; import com.google.android.material.textfield.TextInputEditText; +import com.google.common.collect.Lists; import java.io.IOException; import java.net.InetAddress; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.function.Supplier; import de.florianisme.wakeonlan.R; import de.florianisme.wakeonlan.databinding.ActivityModifyDeviceBinding; import de.florianisme.wakeonlan.persistence.repository.DeviceRepository; import de.florianisme.wakeonlan.ui.modify.watcher.autocomplete.MacAddressAutocomplete; +import de.florianisme.wakeonlan.ui.modify.watcher.validator.ConditionalInputNotEmptyValidator; import de.florianisme.wakeonlan.ui.modify.watcher.validator.InputNotEmptyValidator; import de.florianisme.wakeonlan.ui.modify.watcher.validator.MacValidator; import de.florianisme.wakeonlan.ui.modify.watcher.validator.SecureOnPasswordValidator; @@ -38,6 +45,13 @@ public abstract class ModifyDeviceActivity extends AppCompatActivity { protected TextInputEditText deviceSecureOnPassword; protected ImageButton broadcastAutofill; protected MaterialAutoCompleteTextView devicePorts; + protected ConstraintLayout deviceRemoteShutdownContainer; + protected SwitchCompat deviceEnableRemoteShutdown; + protected TextInputEditText deviceSshAddressInput; + protected TextInputEditText deviceSshPortInput; + protected TextInputEditText deviceSshUsernameInput; + protected TextInputEditText deviceSshPasswordInput; + protected TextInputEditText deviceSshCommandInput; @Override protected void onCreate(Bundle savedInstanceState) { @@ -54,6 +68,14 @@ protected void onCreate(Bundle savedInstanceState) { deviceSecureOnPassword = binding.device.deviceSecureOnPassword; broadcastAutofill = binding.device.broadcastAutofill; + deviceRemoteShutdownContainer = binding.device.deviceRemoteShutdownContainer; + deviceEnableRemoteShutdown = binding.device.deviceSwitchRemoteShutdown; + deviceSshAddressInput = binding.device.deviceShutdownAddress; + deviceSshPortInput = binding.device.deviceShutdownPort; + deviceSshUsernameInput = binding.device.deviceShutdownUsername; + deviceSshPasswordInput = binding.device.deviceShutdownPassword; + deviceSshCommandInput = binding.device.deviceShutdownCommand; + setSupportActionBar(binding.toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close); @@ -61,6 +83,7 @@ protected void onCreate(Bundle savedInstanceState) { deviceRepository = DeviceRepository.getInstance(this); addValidators(); addAutofillClickHandler(); + setRemoteDeviceShutdownSwitchListener(); addDevicePortsAdapter(); } @@ -78,6 +101,11 @@ public void onClick(View v) { }); } + private void setRemoteDeviceShutdownSwitchListener() { + deviceEnableRemoteShutdown.setOnCheckedChangeListener((buttonView, isChecked) -> + deviceRemoteShutdownContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE)); + } + private void addValidators() { deviceMacInput.addTextChangedListener(new MacValidator(deviceMacInput)); deviceMacInput.addTextChangedListener(new MacAddressAutocomplete()); @@ -85,17 +113,35 @@ private void addValidators() { deviceNameInput.addTextChangedListener(new InputNotEmptyValidator(deviceNameInput)); deviceBroadcastInput.addTextChangedListener(new InputNotEmptyValidator(deviceBroadcastInput)); deviceSecureOnPassword.addTextChangedListener(new SecureOnPasswordValidator(deviceSecureOnPassword)); + + List> remoteShutdownEnabledSupplier = Collections.singletonList(() -> deviceEnableRemoteShutdown.isChecked()); + List> statusIpFallbackAvailable = + Lists.newArrayList(() -> deviceEnableRemoteShutdown.isChecked(), () -> isEmpty(deviceStatusIpInput)); + + deviceSshAddressInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshAddressInput, statusIpFallbackAvailable)); + deviceSshUsernameInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshUsernameInput, remoteShutdownEnabledSupplier)); + deviceSshPasswordInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshPasswordInput, remoteShutdownEnabledSupplier)); + deviceSshCommandInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshCommandInput, remoteShutdownEnabledSupplier)); } protected boolean assertInputsNotEmptyAndValid() { return deviceMacInput.getError() == null && isNotEmpty(deviceMacInput) && deviceNameInput.getError() == null && isNotEmpty(deviceNameInput) && deviceBroadcastInput.getError() == null && isNotEmpty(deviceBroadcastInput) && - deviceStatusIpInput.getError() == null && deviceSecureOnPassword.getError() == null; + deviceStatusIpInput.getError() == null && + deviceSecureOnPassword.getError() == null && + deviceSshAddressInput.getError() == null && + deviceSshUsernameInput.getError() == null && + deviceSshPasswordInput.getError() == null && + deviceSshCommandInput.getError() == null; } private boolean isNotEmpty(TextInputEditText inputEditText) { - return inputEditText.getText() != null && inputEditText.getText().length() != 0; + return !isEmpty(inputEditText); + } + + private boolean isEmpty(TextInputEditText inputEditText) { + return inputEditText.getText() == null || inputEditText.getText().length() == 0; } private void addDevicePortsAdapter() { @@ -119,6 +165,11 @@ private void triggerValidators() { deviceNameInput.setText(deviceNameInput.getText()); deviceBroadcastInput.setText(deviceBroadcastInput.getText()); deviceMacInput.setText(deviceMacInput.getText()); + deviceSecureOnPassword.setText(deviceSecureOnPassword.getText()); + deviceSshAddressInput.setText(deviceSshAddressInput.getText()); + deviceSshUsernameInput.setText(deviceSshUsernameInput.getText()); + deviceSshPasswordInput.setText(deviceSshPasswordInput.getText()); + deviceSshCommandInput.setText(deviceSshCommandInput.getText()); } abstract protected void persistDevice(); diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java new file mode 100644 index 0000000..260931b --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java @@ -0,0 +1,37 @@ +package de.florianisme.wakeonlan.ui.modify.watcher.validator; + +import android.widget.EditText; + +import java.util.List; +import java.util.function.Supplier; + +import de.florianisme.wakeonlan.R; + +public class ConditionalInputNotEmptyValidator extends Validator { + + private final List> conditionalSupplierList; + + public ConditionalInputNotEmptyValidator(EditText editTextView, List> conditionalSupplierList) { + super(editTextView); + this.conditionalSupplierList = conditionalSupplierList; + } + + @Override + public ValidationResult validate(String text) { + if (inputShouldBeValidated()) { + return null == text || text.isEmpty() ? ValidationResult.INVALID : ValidationResult.VALID; + } else { + return ValidationResult.VALID; + } + } + + private boolean inputShouldBeValidated() { + return conditionalSupplierList.stream().allMatch(Supplier::get); + } + + @Override + int getErrorMessageStringId() { + return R.string.add_device_error_name_empty; + } + +} From 4015b3f25a63a151d753bf7ce1b44728a7b9443e Mon Sep 17 00:00:00 2001 From: Florianisme Date: Tue, 21 Mar 2023 21:21:28 +0100 Subject: [PATCH 07/72] #7 Add shutdown button to device card --- app/src/main/res/layout/device_list_item.xml | 9 +++++++++ app/src/main/res/values/strings.xml | 1 + 2 files changed, 10 insertions(+) diff --git a/app/src/main/res/layout/device_list_item.xml b/app/src/main/res/layout/device_list_item.xml index aba6ea7..53b0f9c 100644 --- a/app/src/main/res/layout/device_list_item.xml +++ b/app/src/main/res/layout/device_list_item.xml @@ -71,9 +71,18 @@ style="?attr/borderlessButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginEnd="8dp" android:text="@string/device_list_startup" android:textColor="@color/primaryDarkColor" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1055724..b7875ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,7 @@ Turn on + Shutdown No devices set up Tap the \"+\" icon to add your first device Computer Illustration From 19232a1fb40360dace6f235304e4e98f71a0a9d0 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Tue, 21 Mar 2023 21:40:42 +0100 Subject: [PATCH 08/72] #7 Add proof-of-concept implementation for SSH control --- app/build.gradle | 5 +++ .../wakeonlan/shutdown/ShutdownExecutor.java | 40 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java diff --git a/app/build.gradle b/app/build.gradle index 19379f9..430d813 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,6 +29,11 @@ dependencies { implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" + // SSH + implementation 'com.hierynomus:sshj:0.31.0' + implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' + implementation(name: 'android-ping', ext: 'aar') implementation project(path: ':shared-models') } \ No newline at end of file diff --git a/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java new file mode 100644 index 0000000..57a3bf5 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java @@ -0,0 +1,40 @@ +package de.florianisme.wakeonlan.shutdown; + +import android.util.Log; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.Security; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import de.florianisme.wakeonlan.persistence.models.Device; + +public class ShutdownExecutor { + + private static final Executor executor = Executors.newSingleThreadExecutor(); + + static { + // Override Android's BC implementation with official BC Provider + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + Security.insertProviderAt(new BouncyCastleProvider(), 1); + } + + public static void shutdownDevice(Device device) { + try (SSHClient sshClient = new SSHClient()) { + sshClient.addHostKeyVerifier((hostname, port, key) -> true); + sshClient.connect(device.sshAddress, device.sshPort); + sshClient.authPassword(device.sshUsername, device.sshPassword); + Session session = sshClient.startSession(); + + session.allocateDefaultPTY(); + Session.Command exec = session.exec(device.sshCommand); + exec.wait(2000); + } catch (Exception e) { + Log.e(ShutdownExecutor.class.getSimpleName(), "Error during SSH execution", e); + } + } +} From fb1d0dd003f4b191ae00e8ea8e55a7bdeb361dee Mon Sep 17 00:00:00 2001 From: Florianisme Date: Wed, 22 Mar 2023 22:04:32 +0100 Subject: [PATCH 09/72] #7 Restore missing "Security" heading --- .../main/res/layout/content_modify_device.xml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index 50b445d..f975908 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -321,19 +321,19 @@ android:singleLine="true" /> - - + + + app:layout_constraintTop_toBottomOf="@id/device_title_security"> Date: Wed, 22 Mar 2023 22:04:52 +0100 Subject: [PATCH 10/72] #7 Persist new fields correctly --- .../wakeonlan/persistence/entities/DeviceEntity.java | 9 ++++++++- .../wakeonlan/persistence/mapper/DeviceEntityMapper.java | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java index a674270..152aea1 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/entities/DeviceEntity.java @@ -48,7 +48,8 @@ public class DeviceEntity { public String sshCommand; @Ignore - public DeviceEntity(int id, String name, String macAddress, String broadcastAddress, int port, String statusIp, String secureOnPassword) { + public DeviceEntity(int id, String name, String macAddress, String broadcastAddress, int port, String statusIp, String secureOnPassword, + boolean enableRemoteShutdown, String sshAddress, Integer sshPort, String sshUsername, String sshPassword, String sshCommand) { this.id = id; this.name = name; this.macAddress = macAddress; @@ -56,6 +57,12 @@ public DeviceEntity(int id, String name, String macAddress, String broadcastAddr this.port = port; this.statusIp = statusIp; this.secureOnPassword = secureOnPassword; + this.enableRemoteShutdown = enableRemoteShutdown; + this.sshAddress = sshAddress; + this.sshPort = sshPort; + this.sshUsername = sshUsername; + this.sshPassword = sshPassword; + this.sshCommand = sshCommand; } diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java index a90df77..8ce3365 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/mapper/DeviceEntityMapper.java @@ -19,6 +19,7 @@ public DeviceEntity modelToEntity(Device model) { if (model == null) { return new DeviceEntity(); } - return new DeviceEntity(model.id, model.name, model.macAddress, model.broadcastAddress, model.port, model.statusIp, model.secureOnPassword); + return new DeviceEntity(model.id, model.name, model.macAddress, model.broadcastAddress, model.port, model.statusIp, model.secureOnPassword, + model.remoteShutdownEnabled, model.sshAddress, model.sshPort, model.sshUsername, model.sshPassword, model.sshCommand); } } From 72d7fe4dd0e151bd1b0da816990ca1d6411048d9 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Wed, 22 Mar 2023 22:05:15 +0100 Subject: [PATCH 11/72] #7 Allow SSH port to be null by default --- .../de/florianisme/wakeonlan/persistence/models/Device.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java b/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java index 43828f7..26d0c84 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java +++ b/app/src/main/java/de/florianisme/wakeonlan/persistence/models/Device.java @@ -20,7 +20,7 @@ public class Device { public String sshAddress; - public int sshPort; + public Integer sshPort; public String sshUsername; @@ -29,7 +29,7 @@ public class Device { public String sshCommand; public Device(int id, String name, String macAddress, String broadcastAddress, int port, String statusIp, String secureOnPassword, - boolean remoteShutdownEnabled, String sshAddress, int sshPort, String sshUsername, String sshPassword, String sshCommand) { + boolean remoteShutdownEnabled, String sshAddress, Integer sshPort, String sshUsername, String sshPassword, String sshCommand) { this.id = id; this.name = name; this.macAddress = macAddress; From e11d5f6fba79fc9b0d4cbfc4660a5df777bfd50c Mon Sep 17 00:00:00 2001 From: Florianisme Date: Wed, 22 Mar 2023 22:05:54 +0100 Subject: [PATCH 12/72] #7 Restore and persist new fields correctly from Activities --- .../ui/modify/AddDeviceActivity.java | 17 ++++++- .../ui/modify/EditDeviceActivity.java | 32 ++++++++++++- .../ui/modify/ModifyDeviceActivity.java | 45 ++++++++++++++++++- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/AddDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/AddDeviceActivity.java index 429b81d..b31cc7f 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/AddDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/AddDeviceActivity.java @@ -1,5 +1,6 @@ package de.florianisme.wakeonlan.ui.modify; +import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -10,6 +11,12 @@ public class AddDeviceActivity extends ModifyDeviceActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + triggerRemoteShutdownLayoutVisibility(false); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.add_device_menu, menu); @@ -36,6 +43,12 @@ protected void persistDevice() { device.broadcastAddress = getDeviceBroadcastAddressText(); device.port = getPort(); device.secureOnPassword = getDeviceSecureOnPassword(); + device.remoteShutdownEnabled = getDeviceRemoteShutdownEnabled(); + device.sshAddress = getDeviceSshAddress(); + device.sshPort = getDeviceSshPort(); + device.sshUsername = getDeviceSshUsername(); + device.sshPassword = getDeviceSshPassword(); + device.sshCommand = getDeviceSshCommand(); deviceRepository.insertAll(device); } @@ -45,6 +58,8 @@ protected boolean inputsHaveNotChanged() { // There is no persisted device yet, so we check if any of our inputs are edited return getDeviceNameInputText().isEmpty() && getDeviceMacInputText().isEmpty() && getDeviceBroadcastAddressText().isEmpty() && getDeviceStatusIpText().isEmpty() - && getDeviceSecureOnPassword().isEmpty(); + && getDeviceSecureOnPassword().isEmpty() && !getDeviceRemoteShutdownEnabled() && + getDeviceSshAddress().isEmpty() && getDeviceSshPort() == -1 && getDeviceSshUsername().isEmpty() && + getDeviceSshPassword().isEmpty() && getDeviceSshCommand().isEmpty(); } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/EditDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/EditDeviceActivity.java index a8e78ef..56cdddf 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/EditDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/EditDeviceActivity.java @@ -10,6 +10,8 @@ import com.google.common.base.Strings; +import java.util.Objects; + import de.florianisme.wakeonlan.R; import de.florianisme.wakeonlan.persistence.models.Device; @@ -40,15 +42,28 @@ private void populateInputs() { deviceStatusIpInput.setText(device.statusIp); deviceMacInput.setText(device.macAddress); deviceBroadcastInput.setText(device.broadcastAddress); - deviceSecureOnPassword.setText(device.secureOnPassword); if (device.port == 9) { devicePorts.setText("9", false); } else { devicePorts.setText("7", false); } + deviceSecureOnPassword.setText(device.secureOnPassword); + + deviceEnableRemoteShutdown.setChecked(device.remoteShutdownEnabled); + triggerRemoteShutdownLayoutVisibility(device.remoteShutdownEnabled); + deviceSshAddressInput.setText(device.sshAddress); + deviceSshPortInput.setText(getSshPortFallback()); + deviceSshUsernameInput.setText(device.sshUsername); + deviceSshPasswordInput.setText(device.sshPassword); + deviceSshCommandInput.setText(device.sshCommand); } } + @NonNull + private String getSshPortFallback() { + return device.sshPort == null || device.sshPort < 0 ? "" : String.valueOf(device.sshPort); + } + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.edit_device_menu, menu); @@ -84,7 +99,14 @@ protected boolean inputsHaveNotChanged() { device.macAddress.equals(getDeviceMacInputText()) && device.statusIp.equals(getDeviceStatusIpText()) && device.port == getPort() && - Strings.nullToEmpty(device.secureOnPassword).equals(getDeviceSecureOnPassword()); + Strings.nullToEmpty(device.secureOnPassword).equals(getDeviceSecureOnPassword()) && + device.remoteShutdownEnabled == getDeviceRemoteShutdownEnabled() && + Strings.nullToEmpty(device.sshAddress).equals(getDeviceSshAddress()) && + Objects.equals(device.sshPort, getDeviceSshPort()) && + Strings.nullToEmpty(device.sshUsername).equals(getDeviceSshUsername()) && + Strings.nullToEmpty(device.sshPassword).equals(getDeviceSshPassword()) && + Strings.nullToEmpty(device.sshCommand).equals(getDeviceSshCommand()); + } @Override @@ -95,6 +117,12 @@ protected void persistDevice() { device.broadcastAddress = getDeviceBroadcastAddressText(); device.port = getPort(); device.secureOnPassword = getDeviceSecureOnPassword(); + device.remoteShutdownEnabled = getDeviceRemoteShutdownEnabled(); + device.sshAddress = getDeviceSshAddress(); + device.sshPort = getDeviceSshPort(); + device.sshUsername = getDeviceSshUsername(); + device.sshPassword = getDeviceSshPassword(); + device.sshCommand = getDeviceSshCommand(); deviceRepository.update(device); } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java index d73c341..c30675d 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java @@ -15,6 +15,7 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView; import com.google.android.material.textfield.TextInputEditText; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import java.io.IOException; @@ -102,8 +103,11 @@ public void onClick(View v) { } private void setRemoteDeviceShutdownSwitchListener() { - deviceEnableRemoteShutdown.setOnCheckedChangeListener((buttonView, isChecked) -> - deviceRemoteShutdownContainer.setVisibility(isChecked ? View.VISIBLE : View.GONE)); + deviceEnableRemoteShutdown.setOnCheckedChangeListener((buttonView, isChecked) -> triggerRemoteShutdownLayoutVisibility(isChecked)); + } + + protected void triggerRemoteShutdownLayoutVisibility(boolean isEnabled) { + deviceRemoteShutdownContainer.setVisibility(isEnabled ? View.VISIBLE : View.GONE); } private void addValidators() { @@ -210,6 +214,43 @@ protected String getDeviceSecureOnPassword() { return getInputText(deviceSecureOnPassword); } + protected boolean getDeviceRemoteShutdownEnabled() { + return deviceEnableRemoteShutdown.isChecked(); + } + + @NonNull + protected String getDeviceSshAddress() { + return getInputText(deviceSshAddressInput); + } + + @NonNull + protected Integer getDeviceSshPort() { + try { + String sshPort = getInputText(deviceSshPortInput); + if (Strings.nullToEmpty(sshPort).isEmpty()) { + return -1; + } + return Integer.parseInt(sshPort); + } catch (NumberFormatException e) { + return -1; + } + } + + @NonNull + protected String getDeviceSshUsername() { + return getInputText(deviceSshUsernameInput); + } + + @NonNull + protected String getDeviceSshPassword() { + return getInputText(deviceSshPasswordInput); + } + + @NonNull + protected String getDeviceSshCommand() { + return getInputText(deviceSshCommandInput); + } + @Override public boolean onSupportNavigateUp() { if (inputsHaveNotChanged()) { From 06b9dd6938fed2444f42b827c0dfb6f244e564a5 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Wed, 22 Mar 2023 22:06:25 +0100 Subject: [PATCH 13/72] #7 Call provisional ShutdownExecutor on button click --- .../florianisme/wakeonlan/ui/list/DeviceListAdapter.java | 1 + .../wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java index ba81818..2aaf42e 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java @@ -92,6 +92,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final deviceItemViewHolder.setDeviceMacAddress(device.macAddress); deviceItemViewHolder.setOnClickHandler(device); deviceItemViewHolder.setOnEditClickHandler(device); + deviceItemViewHolder.setOnShutdownClickHandler(device); deviceItemViewHolder.startDeviceStatusQuery(device); } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java index e3a418c..f4ecedb 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java @@ -18,6 +18,7 @@ import de.florianisme.wakeonlan.R; import de.florianisme.wakeonlan.persistence.models.Device; import de.florianisme.wakeonlan.persistence.models.DeviceStatus; +import de.florianisme.wakeonlan.shutdown.ShutdownExecutor; import de.florianisme.wakeonlan.ui.list.DeviceClickedCallback; import de.florianisme.wakeonlan.ui.list.status.DeviceStatusTester; import de.florianisme.wakeonlan.ui.list.status.PingDeviceStatusTester; @@ -32,6 +33,7 @@ public class DeviceItemViewHolder extends RecyclerView.ViewHolder { private final Button editButton; private final Button sendWolButton; + private final Button shutdownButton; private final DeviceClickedCallback deviceClickedCallback; private final DeviceStatusTester deviceStatusTester; @@ -43,6 +45,7 @@ public DeviceItemViewHolder(View view, DeviceClickedCallback deviceClickedCallba editButton = view.findViewById(R.id.edit); sendWolButton = view.findViewById(R.id.send_wol); + shutdownButton = view.findViewById(R.id.shutdown); this.deviceClickedCallback = deviceClickedCallback; this.deviceStatusTester = new PingDeviceStatusTester(); } @@ -74,6 +77,10 @@ public void setOnEditClickHandler(Device device) { }); } + public void setOnShutdownClickHandler(Device device) { + shutdownButton.setOnClickListener(v -> ShutdownExecutor.shutdownDevice(device)); + } + public void startDeviceStatusQuery(Device device) { deviceStatus.clearAnimation(); deviceStatus.setBackground(AppCompatResources.getDrawable(itemView.getContext(), R.drawable.device_status_unknown)); From 1e71e2321a26a2e64711d82c820866cf89080fa3 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Wed, 22 Mar 2023 22:13:42 +0100 Subject: [PATCH 14/72] #7 Move WoL port input besides MAC-Address input --- .../main/res/layout/content_modify_device.xml | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index f975908..a611bcb 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -88,13 +88,13 @@ @@ -110,6 +110,27 @@ android:singleLine="true" /> + + + + + - - - - - - + app:layout_constraintTop_toBottomOf="@id/device_broadcast_layout" /> Date: Thu, 23 Mar 2023 21:43:25 +0100 Subject: [PATCH 15/72] #7 Fix DiffCallback not detecting differences after device update --- .../wakeonlan/ui/list/DeviceDiffCallback.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceDiffCallback.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceDiffCallback.java index d5290b0..006c59f 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceDiffCallback.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceDiffCallback.java @@ -22,10 +22,23 @@ public boolean areContentsTheSame(@NonNull Device oldDevice, @NonNull Device new stringMatches(oldDevice.statusIp, newDevice.statusIp) && stringMatches(oldDevice.macAddress, newDevice.macAddress) && stringMatches(oldDevice.secureOnPassword, newDevice.secureOnPassword) && - oldDevice.port == newDevice.port; + oldDevice.port == newDevice.port && + oldDevice.remoteShutdownEnabled == newDevice.remoteShutdownEnabled && + bothNullOrEqual(oldDevice.sshPort, newDevice.sshPort) && + stringMatches(oldDevice.sshAddress, newDevice.sshAddress) && + stringMatches(oldDevice.sshUsername, newDevice.sshUsername) && + stringMatches(oldDevice.sshPassword, newDevice.sshPassword) && + stringMatches(oldDevice.sshCommand, newDevice.sshCommand); } private boolean stringMatches(@Nullable String oldString, @Nullable String newString) { return Strings.nullToEmpty(oldString).equals(newString); } + + private boolean bothNullOrEqual(@Nullable Integer oldInteger, @Nullable Integer newInteger) { + if (oldInteger == null && newInteger == null) { + return true; + } + return oldInteger != null && oldInteger.equals(newInteger); + } } From 3fac07e32d2372d90714a2f31af2fe4030452b46 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Thu, 23 Mar 2023 21:44:04 +0100 Subject: [PATCH 16/72] #7 Move SSH logic into separate Runnable --- .../wakeonlan/shutdown/ShutdownExecutor.java | 25 ++++----- .../wakeonlan/shutdown/ShutdownModel.java | 38 +++++++++++++ .../shutdown/ShutdownModelFactory.java | 53 +++++++++++++++++++ .../wakeonlan/shutdown/ShutdownRunnable.java | 31 +++++++++++ 4 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModel.java create mode 100644 app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModelFactory.java create mode 100644 app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownRunnable.java diff --git a/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java index 57a3bf5..d64d8ad 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java +++ b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownExecutor.java @@ -2,12 +2,10 @@ import android.util.Log; -import net.schmizz.sshj.SSHClient; -import net.schmizz.sshj.connection.channel.direct.Session; - import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -24,17 +22,16 @@ public class ShutdownExecutor { } public static void shutdownDevice(Device device) { - try (SSHClient sshClient = new SSHClient()) { - sshClient.addHostKeyVerifier((hostname, port, key) -> true); - sshClient.connect(device.sshAddress, device.sshPort); - sshClient.authPassword(device.sshUsername, device.sshPassword); - Session session = sshClient.startSession(); - - session.allocateDefaultPTY(); - Session.Command exec = session.exec(device.sshCommand); - exec.wait(2000); - } catch (Exception e) { - Log.e(ShutdownExecutor.class.getSimpleName(), "Error during SSH execution", e); + Optional optionalShutdownModel = ShutdownModelFactory.fromDevice(device); + + if (!optionalShutdownModel.isPresent()) { + Log.w(ShutdownExecutor.class.getSimpleName(), "Can not shutdown device. Not all required fields were set"); + return; } + + ShutdownModel shutdownModel = optionalShutdownModel.get(); + ShutdownRunnable shutdownRunnable = new ShutdownRunnable(shutdownModel); + + executor.execute(shutdownRunnable); } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModel.java b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModel.java new file mode 100644 index 0000000..fa2dc1f --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModel.java @@ -0,0 +1,38 @@ +package de.florianisme.wakeonlan.shutdown; + +public class ShutdownModel { + + private final String sshAddress; + private final int sshPort; + private final String username; + private final String password; + private final String command; + + public ShutdownModel(String sshAddress, int sshPort, String username, String password, String command) { + this.sshAddress = sshAddress; + this.sshPort = sshPort; + this.username = username; + this.password = password; + this.command = command; + } + + public String getSshAddress() { + return sshAddress; + } + + public int getSshPort() { + return sshPort; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getCommand() { + return command; + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModelFactory.java b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModelFactory.java new file mode 100644 index 0000000..1393941 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownModelFactory.java @@ -0,0 +1,53 @@ +package de.florianisme.wakeonlan.shutdown; + +import androidx.annotation.Nullable; + +import com.google.common.base.Strings; + +import java.util.Optional; + +import de.florianisme.wakeonlan.persistence.models.Device; + +public class ShutdownModelFactory { + + private static final int DEFAULT_SSH_PORT = 22; + + public static Optional fromDevice(Device device) { + boolean shutdownEnabled = device.remoteShutdownEnabled; + String address = getValueOrFallback(device.sshAddress, device.statusIp); + int port = getSshPortOrFallback(device.sshPort); + String username = getValueOrFallback(device.sshUsername, null); + String password = getValueOrFallback(device.sshPassword, null); + String command = getValueOrFallback(device.sshCommand, null); + + if (allRequiredFieldsSet(shutdownEnabled, address, username, password, command)) { + return Optional.of(new ShutdownModel(address, port, username, password, command)); + } + + return Optional.empty(); + } + + private static boolean allRequiredFieldsSet(boolean shutdownEnabled, String address, String username, String password, String command) { + return shutdownEnabled && address != null && username != null && password != null && command != null; + } + + @Nullable + private static String getValueOrFallback(@Nullable String value, @Nullable String fallback) { + if (!Strings.isNullOrEmpty(value)) { + return value; + } + if (!Strings.isNullOrEmpty(fallback)) { + return fallback; + } + return null; + } + + private static Integer getSshPortOrFallback(@Nullable Integer value) { + if (value != null && value > 0) { + return value; + } + return ShutdownModelFactory.DEFAULT_SSH_PORT; + } + + +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownRunnable.java b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownRunnable.java new file mode 100644 index 0000000..79bf68a --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/shutdown/ShutdownRunnable.java @@ -0,0 +1,31 @@ +package de.florianisme.wakeonlan.shutdown; + +import android.util.Log; + +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.connection.channel.direct.Session; + +public class ShutdownRunnable implements Runnable { + + private final ShutdownModel shutdownModel; + + public ShutdownRunnable(ShutdownModel shutdownModel) { + this.shutdownModel = shutdownModel; + } + + @Override + public void run() { + try (SSHClient sshClient = new SSHClient()) { + sshClient.addHostKeyVerifier((hostname, port, key) -> true); + sshClient.connect(shutdownModel.getSshAddress(), shutdownModel.getSshPort()); + sshClient.authPassword(shutdownModel.getUsername(), shutdownModel.getPassword()); + Session session = sshClient.startSession(); + + session.allocateDefaultPTY(); + Session.Command exec = session.exec(shutdownModel.getCommand()); + exec.wait(2000); + } catch (Exception e) { + Log.e(ShutdownRunnable.class.getSimpleName(), "Error during SSH execution", e); + } + } +} From 6a177b0823a7ea61d74bdf6102a86dfc8d93345e Mon Sep 17 00:00:00 2001 From: Florianisme Date: Thu, 23 Mar 2023 21:45:19 +0100 Subject: [PATCH 17/72] #7 Hide Shutdown button if configuration is invalid or disabled --- .../wakeonlan/ui/list/DeviceListAdapter.java | 2 +- .../ui/list/viewholder/DeviceItemViewHolder.java | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java index 2aaf42e..a80bccb 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListAdapter.java @@ -92,7 +92,7 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final deviceItemViewHolder.setDeviceMacAddress(device.macAddress); deviceItemViewHolder.setOnClickHandler(device); deviceItemViewHolder.setOnEditClickHandler(device); - deviceItemViewHolder.setOnShutdownClickHandler(device); + deviceItemViewHolder.setShutdownVisibilityAndClickHandler(device); deviceItemViewHolder.startDeviceStatusQuery(device); } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java index f4ecedb..5d69b38 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/viewholder/DeviceItemViewHolder.java @@ -11,6 +11,7 @@ import android.view.animation.Animation; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import androidx.appcompat.content.res.AppCompatResources; import androidx.recyclerview.widget.RecyclerView; @@ -19,6 +20,7 @@ import de.florianisme.wakeonlan.persistence.models.Device; import de.florianisme.wakeonlan.persistence.models.DeviceStatus; import de.florianisme.wakeonlan.shutdown.ShutdownExecutor; +import de.florianisme.wakeonlan.shutdown.ShutdownModelFactory; import de.florianisme.wakeonlan.ui.list.DeviceClickedCallback; import de.florianisme.wakeonlan.ui.list.status.DeviceStatusTester; import de.florianisme.wakeonlan.ui.list.status.PingDeviceStatusTester; @@ -77,8 +79,17 @@ public void setOnEditClickHandler(Device device) { }); } - public void setOnShutdownClickHandler(Device device) { - shutdownButton.setOnClickListener(v -> ShutdownExecutor.shutdownDevice(device)); + public void setShutdownVisibilityAndClickHandler(Device device) { + boolean shutdownConfigurationValid = ShutdownModelFactory.fromDevice(device).isPresent(); + + shutdownButton.setVisibility(shutdownConfigurationValid ? View.VISIBLE : View.GONE); + + if (shutdownConfigurationValid) { + shutdownButton.setOnClickListener(v -> { + ShutdownExecutor.shutdownDevice(device); + Toast.makeText(v.getContext(), v.getContext().getString(R.string.remote_shutdown_send_command, device.name), Toast.LENGTH_LONG).show(); + }); + } } public void startDeviceStatusQuery(Device device) { From 2bcb1d25f1db2375b2666d0b2a6471f09e1818e7 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Thu, 23 Mar 2023 21:45:45 +0100 Subject: [PATCH 18/72] #7 Fix input validation and error texts --- .../ui/modify/ModifyDeviceActivity.java | 18 +++++++++++------- .../ConditionalInputNotEmptyValidator.java | 13 +++---------- .../validator/InputNotEmptyValidator.java | 9 +++++---- app/src/main/res/values/strings.xml | 8 ++++++++ 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java index c30675d..77c487b 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/ModifyDeviceActivity.java @@ -114,18 +114,22 @@ private void addValidators() { deviceMacInput.addTextChangedListener(new MacValidator(deviceMacInput)); deviceMacInput.addTextChangedListener(new MacAddressAutocomplete()); - deviceNameInput.addTextChangedListener(new InputNotEmptyValidator(deviceNameInput)); - deviceBroadcastInput.addTextChangedListener(new InputNotEmptyValidator(deviceBroadcastInput)); + deviceNameInput.addTextChangedListener(new InputNotEmptyValidator(deviceNameInput, R.string.add_device_error_name_empty)); + deviceBroadcastInput.addTextChangedListener(new InputNotEmptyValidator(deviceBroadcastInput, R.string.add_device_error_broadcast_empty)); deviceSecureOnPassword.addTextChangedListener(new SecureOnPasswordValidator(deviceSecureOnPassword)); List> remoteShutdownEnabledSupplier = Collections.singletonList(() -> deviceEnableRemoteShutdown.isChecked()); List> statusIpFallbackAvailable = Lists.newArrayList(() -> deviceEnableRemoteShutdown.isChecked(), () -> isEmpty(deviceStatusIpInput)); - deviceSshAddressInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshAddressInput, statusIpFallbackAvailable)); - deviceSshUsernameInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshUsernameInput, remoteShutdownEnabledSupplier)); - deviceSshPasswordInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshPasswordInput, remoteShutdownEnabledSupplier)); - deviceSshCommandInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshCommandInput, remoteShutdownEnabledSupplier)); + deviceSshAddressInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshAddressInput, + R.string.add_device_error_ssh_address_empty, statusIpFallbackAvailable)); + deviceSshUsernameInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshUsernameInput, + R.string.add_device_error_ssh_username_empty, remoteShutdownEnabledSupplier)); + deviceSshPasswordInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshPasswordInput, + R.string.add_device_error_ssh_password_empty, remoteShutdownEnabledSupplier)); + deviceSshCommandInput.addTextChangedListener(new ConditionalInputNotEmptyValidator(deviceSshCommandInput, + R.string.add_device_error_ssh_command_empty, remoteShutdownEnabledSupplier)); } protected boolean assertInputsNotEmptyAndValid() { @@ -156,11 +160,11 @@ private void addDevicePortsAdapter() { } protected void checkAndPersistDevice() { + triggerValidators(); if (assertInputsNotEmptyAndValid()) { persistDevice(); finish(); } else { - triggerValidators(); Toast.makeText(this, R.string.add_device_error_save_clicked, Toast.LENGTH_LONG).show(); } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java index 260931b..9ffb922 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/ConditionalInputNotEmptyValidator.java @@ -5,14 +5,12 @@ import java.util.List; import java.util.function.Supplier; -import de.florianisme.wakeonlan.R; - -public class ConditionalInputNotEmptyValidator extends Validator { +public class ConditionalInputNotEmptyValidator extends InputNotEmptyValidator { private final List> conditionalSupplierList; - public ConditionalInputNotEmptyValidator(EditText editTextView, List> conditionalSupplierList) { - super(editTextView); + public ConditionalInputNotEmptyValidator(EditText editTextView, int errorMessageStringId, List> conditionalSupplierList) { + super(editTextView, errorMessageStringId); this.conditionalSupplierList = conditionalSupplierList; } @@ -29,9 +27,4 @@ private boolean inputShouldBeValidated() { return conditionalSupplierList.stream().allMatch(Supplier::get); } - @Override - int getErrorMessageStringId() { - return R.string.add_device_error_name_empty; - } - } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/InputNotEmptyValidator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/InputNotEmptyValidator.java index c727c35..4999089 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/InputNotEmptyValidator.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/InputNotEmptyValidator.java @@ -2,12 +2,13 @@ import android.widget.EditText; -import de.florianisme.wakeonlan.R; - public class InputNotEmptyValidator extends Validator { - public InputNotEmptyValidator(EditText editTextView) { + private final int errorMessageStringId; + + public InputNotEmptyValidator(EditText editTextView, int errorMessageStringId) { super(editTextView); + this.errorMessageStringId = errorMessageStringId; } @Override @@ -17,7 +18,7 @@ public ValidationResult validate(String text) { @Override int getErrorMessageStringId() { - return R.string.add_device_error_name_empty; + return errorMessageStringId; } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7875ae..a81c590 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,8 +35,13 @@ Invalid MAC Address Name must not be empty + Broadcast Address must not be empty Invalid IP Address Password must be either empty, a valid MAC or IP Address + SSH Address must not be empty when no Status IP set + Username must not be empty + Password must not be empty + Shutdown Command must not be empty Please correct your inputs first @@ -96,4 +101,7 @@ Could not wake device + + + Sending shutdown command to %1$s \ No newline at end of file From 5460883e3f01b1ac1af7a83dcf29fcd8e240b9d5 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Thu, 23 Mar 2023 21:58:10 +0100 Subject: [PATCH 19/72] #7 Add helper text to shutdown command input --- app/src/main/res/layout/content_modify_device.xml | 3 ++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index a611bcb..710a712 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -304,10 +304,11 @@ android:layout_marginTop="8dp" android:layout_marginEnd="24dp" android:hint="@string/add_device_shutdown_command" + app:helperText="@string/add_device_shutdown_command_helper" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/device_remote_shutdown_password" - app:placeholderText="/bin/shutdown -h now"> + app:placeholderText="/usr/sbin/shutdown -h now"> Username Password Shutdown Command + Command must be executable without triggering sudo-password-prompt Save Delete From afc9e026220bbcbf8f7d2e73d1b256da8a964288 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Thu, 23 Mar 2023 22:22:07 +0100 Subject: [PATCH 20/72] #7 Adjust wording --- app/src/main/res/layout/content_modify_device.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index 710a712..406403f 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -308,7 +308,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/device_remote_shutdown_password" - app:placeholderText="/usr/sbin/shutdown -h now"> + app:placeholderText="sudo /usr/sbin/shutdown -h now"> Username Password Shutdown Command - Command must be executable without triggering sudo-password-prompt + Command must be executable without triggering Interactive authentication (sudo password-prompt) Save Delete From d62c129d7d4858ce556831c5e541eaeb2f336169 Mon Sep 17 00:00:00 2001 From: Florianisme Date: Thu, 11 May 2023 23:12:00 +0200 Subject: [PATCH 21/72] #7 Add button to test shutdown configuration --- app/src/main/res/layout/content_modify_device.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index 406403f..6f19ea0 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -321,8 +321,19 @@ android:singleLine="true" /> +