diff --git a/app/build.gradle b/app/build.gradle index 98963f4..2d693d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,7 @@ apply from: "$rootProject.projectDir/shared-build.gradle" android { defaultConfig { - versionCode 97 + versionCode 107 wearAppUnbundled true } buildFeatures { @@ -13,19 +13,19 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.navigation:navigation-fragment:2.7.3' - implementation 'androidx.navigation:navigation-ui:2.7.3' + implementation 'androidx.navigation:navigation-fragment:2.7.5' + implementation 'androidx.navigation:navigation-ui:2.7.5' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'org.reactivestreams:reactive-streams:1.0.3' - implementation 'io.reactivex.rxjava2:rxjava:2.2.9' + implementation 'org.reactivestreams:reactive-streams:1.0.4' + implementation 'io.reactivex.rxjava2:rxjava:2.2.21' implementation 'android.arch.lifecycle:livedata:1.1.1' implementation "androidx.core:core:1.12.0" implementation 'androidx.core:core-google-shortcuts:1.1.0' - def room_version = "2.5.2" + def room_version = "2.6.0" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" @@ -34,8 +34,8 @@ dependencies { implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' - implementation files('libs/android-ping.aar') implementation project(path: ':shared-models') + implementation project(path: ':ping') // Fix Duplicate class implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) diff --git a/app/libs/android-ping.aar b/app/libs/android-ping.aar deleted file mode 100644 index bcbdd9f..0000000 Binary files a/app/libs/android-ping.aar and /dev/null differ 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 26d0c84..4308747 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 @@ -1,6 +1,11 @@ package de.florianisme.wakeonlan.persistence.models; -public class Device { +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +public class Device implements Parcelable { public int id; @@ -48,4 +53,53 @@ public Device(int id, String name, String macAddress, String broadcastAddress, i public Device() { } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator<>() { + public Device createFromParcel(Parcel in) { + return new Device(in); + } + + public Device[] newArray(int size) { + return new Device[size]; + } + }; + + private Device(Parcel in) { + this.id = in.readInt(); + this.name = in.readString(); + this.macAddress = in.readString(); + this.broadcastAddress = in.readString(); + this.port = in.readInt(); + this.statusIp = in.readString(); + this.secureOnPassword = in.readString(); + this.remoteShutdownEnabled = in.readInt() >= 1; + this.sshAddress = in.readString(); + this.sshPort = in.readInt(); + this.sshUsername = in.readString(); + this.sshPassword = in.readString(); + this.sshCommand = in.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(id); + dest.writeString(name); + dest.writeString(macAddress); + dest.writeString(broadcastAddress); + dest.writeInt(port); + dest.writeString(statusIp); + dest.writeString(secureOnPassword); + dest.writeInt(remoteShutdownEnabled ? 1 : 0); + dest.writeString(sshAddress); + dest.writeInt(sshPort == null ? -1 : sshPort); + dest.writeString(sshUsername); + dest.writeString(sshPassword); + dest.writeString(sshCommand); + } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/quickaccess/QuickAccessProviderService.java b/app/src/main/java/de/florianisme/wakeonlan/quickaccess/QuickAccessProviderService.java index 85eef85..5acd22a 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/quickaccess/QuickAccessProviderService.java +++ b/app/src/main/java/de/florianisme/wakeonlan/quickaccess/QuickAccessProviderService.java @@ -46,7 +46,7 @@ public Flow.Publisher createPublisherFor(@NonNull List controlI @Override public void onDestroy() { - StatefulControlService.unscheduleStatusTester(); + StatefulControlService.stopAllStatusTesters(); super.onDestroy(); } diff --git a/app/src/main/java/de/florianisme/wakeonlan/quickaccess/StatefulControlService.java b/app/src/main/java/de/florianisme/wakeonlan/quickaccess/StatefulControlService.java index 7f0476a..04a5afe 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/quickaccess/StatefulControlService.java +++ b/app/src/main/java/de/florianisme/wakeonlan/quickaccess/StatefulControlService.java @@ -12,28 +12,22 @@ import androidx.annotation.RequiresApi; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import de.florianisme.wakeonlan.R; import de.florianisme.wakeonlan.persistence.models.Device; import de.florianisme.wakeonlan.persistence.models.DeviceStatus; import de.florianisme.wakeonlan.persistence.repository.DeviceRepository; -import de.florianisme.wakeonlan.ui.list.status.DeviceStatusTester; -import de.florianisme.wakeonlan.ui.list.status.PingDeviceStatusTester; +import de.florianisme.wakeonlan.ui.list.status.pool.PingStatusTesterPool; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTestType; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTesterPool; import io.reactivex.processors.ReplayProcessor; @RequiresApi(api = Build.VERSION_CODES.R) public class StatefulControlService { - private static final Map statusTesterMap = new HashMap<>(); - - static void unscheduleStatusTester() { - statusTesterMap.forEach((id, tester) -> tester.stopDeviceStatusPings()); - statusTesterMap.clear(); - } + private static final StatusTesterPool STATUS_TESTER_POOL = PingStatusTesterPool.getInstance(); static void createAndUpdateStatefulControls(List deviceIds, ReplayProcessor processor, Context context) { DeviceRepository deviceRepository = DeviceRepository.getInstance(context); @@ -42,12 +36,10 @@ static void createAndUpdateStatefulControls(List deviceIds, ReplayProces .collect(Collectors.toList()); for (Device device : filteredDevices) { - DeviceStatusTester deviceStatusTester = statusTesterMap.getOrDefault(device.id, new PingDeviceStatusTester()); - statusTesterMap.putIfAbsent(device.id, deviceStatusTester); - deviceStatusTester.scheduleDeviceStatusPings(device, deviceStatus -> { + STATUS_TESTER_POOL.scheduleStatusTest(device, deviceStatus -> { Control control = mapDeviceToStatefulControl(device, deviceStatus == DeviceStatus.ONLINE, context); processor.onNext(control); - }); + }, StatusTestType.QUICK_ACCESS); } } @@ -67,4 +59,7 @@ private static Control mapDeviceToStatefulControl(Device device, boolean toggleO .build(); } + public static void stopAllStatusTesters() { + STATUS_TESTER_POOL.stopAllStatusTesters(StatusTestType.QUICK_ACCESS); + } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/quicksettings/DeviceTileService.java b/app/src/main/java/de/florianisme/wakeonlan/quicksettings/DeviceTileService.java index a05230b..2e6c662 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/quicksettings/DeviceTileService.java +++ b/app/src/main/java/de/florianisme/wakeonlan/quicksettings/DeviceTileService.java @@ -3,7 +3,6 @@ import android.os.Build; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; -import android.util.Log; import android.widget.Toast; import java.util.List; @@ -13,18 +12,22 @@ import de.florianisme.wakeonlan.persistence.models.Device; import de.florianisme.wakeonlan.persistence.models.DeviceStatus; import de.florianisme.wakeonlan.persistence.repository.DeviceRepository; +import de.florianisme.wakeonlan.shutdown.ShutdownExecutor; import de.florianisme.wakeonlan.ui.list.status.DeviceStatusListener; -import de.florianisme.wakeonlan.ui.list.status.DeviceStatusTester; -import de.florianisme.wakeonlan.ui.list.status.PingDeviceStatusTester; +import de.florianisme.wakeonlan.ui.list.status.pool.PingStatusTesterPool; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTestType; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTesterPool; import de.florianisme.wakeonlan.wol.WolSender; public abstract class DeviceTileService extends TileService implements DeviceStatusListener { - private final DeviceStatusTester deviceStatusTester = new PingDeviceStatusTester(); + private final StatusTesterPool statusTesterPool = PingStatusTesterPool.getInstance(); private DeviceRepository deviceRepository; private Device device; + private DeviceStatus lastDeviceStatus = DeviceStatus.UNKNOWN; + @Override public void onTileAdded() { @@ -41,7 +44,7 @@ private void updateTileState() { if (optionalMachine.isPresent()) { Device device = optionalMachine.get(); this.device = device; - deviceStatusTester.scheduleDeviceStatusPings(device, this); + statusTesterPool.scheduleStatusTest(device, this, StatusTestType.TILE); tile.setLabel(device.name); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -67,15 +70,20 @@ public void onStatusAvailable(DeviceStatus deviceStatus) { Tile tile = getQsTile(); tile.setState(deviceStatus == DeviceStatus.ONLINE ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); tile.updateTile(); + + lastDeviceStatus = deviceStatus; } @Override public void onClick() { - try { + if (lastDeviceStatus == DeviceStatus.ONLINE) { + if (device.remoteShutdownEnabled) { + ShutdownExecutor.shutdownDevice(device); + Toast.makeText(this, getString(R.string.remote_shutdown_send_command, device.name), Toast.LENGTH_LONG).show(); + } + } else { WolSender.sendWolPacket(device); Toast.makeText(this, getString(R.string.wol_toast_sending_packet, device.name), Toast.LENGTH_LONG).show(); - } catch (Exception e) { - Log.e(this.getClass().getName(), "Error while sending WOL Packet", e); } } @@ -88,7 +96,7 @@ public void onStartListening() { @Override public void onStopListening() { super.onStopListening(); - deviceStatusTester.stopDeviceStatusPings(); + statusTesterPool.stopStatusTest(device, StatusTestType.TILE); } abstract int machineAtIndex(); diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/MainActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/MainActivity.java index 4350a23..b880e59 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/MainActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/MainActivity.java @@ -1,6 +1,10 @@ package de.florianisme.wakeonlan.ui; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; +import android.view.View; +import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.GravityCompat; @@ -14,6 +18,7 @@ import java.util.Set; +import de.florianisme.wakeonlan.BuildConfig; import de.florianisme.wakeonlan.R; import de.florianisme.wakeonlan.databinding.ActivityMainBinding; import de.florianisme.wakeonlan.persistence.repository.DeviceRepository; @@ -34,6 +39,8 @@ protected void onCreate(Bundle savedInstanceState) { binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + setVersionInformation(); + setSupportActionBar(binding.toolbar); initializeNavController(); @@ -41,6 +48,24 @@ protected void onCreate(Bundle savedInstanceState) { initializeShortcuts(); } + private void setVersionInformation() { + View headerView = binding.navigationView.getHeaderView(0); + + TextView versionView = headerView.findViewById(R.id.navigation_header_version); + TextView headerTitleView = headerView.findViewById(R.id.navigation_header_title); + + headerTitleView.setOnApplyWindowInsetsListener((v, insets) -> { + int calculatedTopPadding = Math.round(insets.getSystemWindowInsetTop() + + getResources().getDimension(R.dimen.navigation_header_top_padding)); + + headerTitleView.setPadding(versionView.getPaddingStart(), calculatedTopPadding, + versionView.getPaddingEnd(), 0); + return insets; + }); + + versionView.setText(getString(R.string.drawer_menu_header_version, BuildConfig.VERSION_NAME)); + } + private void initializeWearClient() { wearClient = new WearClient(this); DeviceRepository.getInstance(this) @@ -53,6 +78,17 @@ private void initializeNavController() { appBarConfiguration = new AppBarConfiguration.Builder(getMenuIds()).setOpenableLayout(binding.drawerLayout).build(); NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); NavigationUI.setupWithNavController(binding.navigationView, navController); + + setGithubShortcut(); + } + + private void setGithubShortcut() { + binding.navigationView.getMenu().findItem(R.id.githubShortcut).setOnMenuItemClickListener(item -> { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Florianisme/WakeOnLan")); + startActivity(browserIntent); + + return false; + }); } private void initializeShortcuts() { diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataExporter.java b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataExporter.java index 388d5fc..16dd922 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataExporter.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataExporter.java @@ -9,10 +9,11 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.fragment.app.Fragment; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; import java.io.OutputStream; import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -49,7 +50,7 @@ public void onActivityResult(Uri uri) { .stream().map(DeviceBackupModel::new) .collect(Collectors.toList()); - byte[] content = new ObjectMapper().writeValueAsBytes(devices); + byte[] content = new Gson().toJson(devices).getBytes(StandardCharsets.UTF_8); writeDevicesToFile(uri, content, context); Toast.makeText(context, context.getString(R.string.backup_message_export_success, devices.size()), Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataImporter.java b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataImporter.java index b9e3585..e383ef7 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataImporter.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/backup/DataImporter.java @@ -9,11 +9,13 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.fragment.app.Fragment; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.ByteStreams; +import com.google.gson.Gson; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.util.Arrays; @@ -46,7 +48,8 @@ public void onActivityResult(Uri uri) { try { byte[] bytes = readContentFromFile(uri, context); - Device[] devices = Arrays.stream(new ObjectMapper().readValue(bytes, DeviceBackupModel[].class)) + InputStreamReader inputStreamReader = new InputStreamReader(new ByteArrayInputStream(bytes)); + Device[] devices = Arrays.stream(new Gson().fromJson(inputStreamReader, DeviceBackupModel[].class)) .map(DeviceBackupModel::toModel) .toArray(Device[]::new); 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 ef29968..a74323f 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 @@ -1,12 +1,9 @@ package de.florianisme.wakeonlan.ui.backup.model; -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.gson.annotations.SerializedName; import de.florianisme.wakeonlan.persistence.models.Device; -@JsonIgnoreProperties(ignoreUnknown = true) public class DeviceBackupModel { /* @@ -15,50 +12,43 @@ public class DeviceBackupModel { to JSON with its obfuscated name ("a", "b", and so on). */ - @JsonProperty("id") - @JsonAlias("a") + @SerializedName(value = "id", alternate = "a") public int id; - @JsonProperty("name") - @JsonAlias("b") + @SerializedName(value = "name", alternate = "b") public String name; - @JsonProperty("mac_address") - @JsonAlias("c") + @SerializedName(value = "mac_address", alternate = "c") public String macAddress; - @JsonProperty("broadcast_address") - @JsonAlias("d") + @SerializedName(value = "broadcast_address", alternate = "d") public String broadcastAddress; - @JsonProperty("port") - @JsonAlias("e") + @SerializedName(value = "port", alternate = "e") public int port; - @JsonProperty("status_ip") - @JsonAlias("f") + @SerializedName(value = "status_ip", alternate = "f") public String statusIp; - @JsonProperty("secure_on_password") - @JsonAlias("g") + @SerializedName(value = "secure_on_password", alternate = "g") public String secureOnPassword; - @JsonProperty("remote_shutdown_enabled") + @SerializedName(value = "remote_shutdown_enabled") public boolean remoteShutdownEnabled; - @JsonProperty("ssh_address") + @SerializedName(value = "ssh_address") public String sshAddress; - @JsonProperty("ssh_port") + @SerializedName(value = "ssh_port") public Integer sshPort; - @JsonProperty("ssh_username") + @SerializedName(value = "ssh_username") public String sshUsername; - @JsonProperty("ssh_password") + @SerializedName(value = "ssh_password") public String sshPassword; - @JsonProperty("ssh_command") + @SerializedName(value = "ssh_command") public String sshCommand; public DeviceBackupModel(Device device) { 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 a80bccb..4d1554c 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 @@ -12,6 +12,7 @@ import de.florianisme.wakeonlan.R; import de.florianisme.wakeonlan.persistence.models.Device; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTesterPool; import de.florianisme.wakeonlan.ui.list.viewholder.DeviceItemViewHolder; import de.florianisme.wakeonlan.ui.list.viewholder.EmptyViewHolder; import de.florianisme.wakeonlan.ui.list.viewholder.ListViewType; @@ -20,10 +21,13 @@ public class DeviceListAdapter extends RecyclerView.Adapter listDiffer = new AsyncListDiffer<>(this, new DeviceDiffCallback()); private final DeviceClickedCallback deviceClickedCallback; + private final StatusTesterPool statusTesterPool; - public DeviceListAdapter(List initialDataset, DeviceClickedCallback deviceClickedCallback) { + public DeviceListAdapter(List initialDataset, DeviceClickedCallback deviceClickedCallback, StatusTesterPool statusTesterPool) { this.deviceClickedCallback = deviceClickedCallback; - updateDataset(initialDataset); + this.statusTesterPool = statusTesterPool; + + listDiffer.submitList(initialDataset); } public void updateDataset(List updatedDevices) { @@ -43,7 +47,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, } else { view = LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.device_list_item, viewGroup, false); - viewHolder = new DeviceItemViewHolder(view, deviceClickedCallback); + viewHolder = new DeviceItemViewHolder(view, deviceClickedCallback, statusTesterPool); } return viewHolder; @@ -58,13 +62,6 @@ public int getItemViewType(int position) { } } - @Override - public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { - if (holder instanceof DeviceItemViewHolder) { - ((DeviceItemViewHolder) holder).cancelStatusUpdates(); - } - } - @Override public long getItemId(int position) { if (listDiffer.getCurrentList().isEmpty()) { @@ -85,15 +82,11 @@ public int getItemCount() { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int position) { if (getItemViewType(position) == ListViewType.DEVICE.ordinal()) { DeviceItemViewHolder deviceItemViewHolder = (DeviceItemViewHolder) viewHolder; + deviceItemViewHolder.cancelStatusUpdates(); - Device device = listDiffer.getCurrentList().get(position); - deviceItemViewHolder.setDeviceName(device.name); - deviceItemViewHolder.setDeviceMacAddress(device.macAddress); - deviceItemViewHolder.setOnClickHandler(device); - deviceItemViewHolder.setOnEditClickHandler(device); - deviceItemViewHolder.setShutdownVisibilityAndClickHandler(device); - deviceItemViewHolder.startDeviceStatusQuery(device); + Device device = listDiffer.getCurrentList().get(position); + deviceItemViewHolder.fromDevice(device); } } } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListFragment.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListFragment.java index 6cd9fd8..c5c4e68 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListFragment.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/DeviceListFragment.java @@ -20,13 +20,19 @@ import de.florianisme.wakeonlan.persistence.repository.DeviceRepository; import de.florianisme.wakeonlan.ui.list.layoutmanager.GridLayoutManagerWrapper; import de.florianisme.wakeonlan.ui.list.layoutmanager.LinearLayoutManagerWrapper; +import de.florianisme.wakeonlan.ui.list.status.pool.PingStatusTesterPool; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTestType; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTesterPool; public class DeviceListFragment extends Fragment { + private DeviceRepository deviceRepository; private FragmentListDevicesBinding binding; private DeviceListAdapter deviceListAdapter; + private static final StatusTesterPool STATUS_TESTER_POOL = PingStatusTesterPool.getInstance(); + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -36,23 +42,36 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, return binding.getRoot(); } + @Override + public void onPause() { + super.onPause(); + PingStatusTesterPool.getInstance().pauseAllForType(StatusTestType.LIST); + } + + @Override + public void onResume() { + super.onResume(); + PingStatusTesterPool.getInstance().resumeAll(); + } + @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + deviceRepository = DeviceRepository.getInstance(getContext()); + instantiateRecyclerView(); registerLiveDataObserver(); } private void registerLiveDataObserver() { - DeviceRepository.getInstance(getContext()) - .getAllAsObservable() + deviceRepository.getAllAsObservable() .observe(getViewLifecycleOwner(), devices -> deviceListAdapter.updateDataset(devices)); } private void instantiateRecyclerView() { List initialDataset = DeviceRepository.getInstance(getContext()).getAll(); - deviceListAdapter = new DeviceListAdapter(initialDataset, buildDeviceClickedCallback()); + deviceListAdapter = new DeviceListAdapter(initialDataset, buildDeviceClickedCallback(), STATUS_TESTER_POOL); deviceListAdapter.setHasStableIds(true); RecyclerView devicesRecyclerView = binding.machineList; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/DeviceStatusTester.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/DeviceStatusTester.java deleted file mode 100644 index 24afc55..0000000 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/DeviceStatusTester.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.florianisme.wakeonlan.ui.list.status; - -import de.florianisme.wakeonlan.persistence.models.Device; - -public interface DeviceStatusTester { - - void scheduleDeviceStatusPings(Device device, DeviceStatusListener deviceStatusListener); - - void stopDeviceStatusPings(); -} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/DeviceStatusTesterBuilder.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/DeviceStatusTesterBuilder.java new file mode 100644 index 0000000..33447ae --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/DeviceStatusTesterBuilder.java @@ -0,0 +1,9 @@ +package de.florianisme.wakeonlan.ui.list.status; + +import de.florianisme.wakeonlan.persistence.models.Device; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTestItem; + +public interface DeviceStatusTesterBuilder { + + Runnable buildStatusTestCallable(Device device, StatusTestItem statusTestItem); +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingDeviceStatusTester.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingDeviceStatusTester.java deleted file mode 100644 index b92ab03..0000000 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingDeviceStatusTester.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.florianisme.wakeonlan.ui.list.status; - -import android.util.Log; - -import com.spectrum.android.ping.Ping; - -import java.net.InetAddress; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import de.florianisme.wakeonlan.persistence.models.Device; -import de.florianisme.wakeonlan.persistence.models.DeviceStatus; - -public class PingDeviceStatusTester implements DeviceStatusTester { - - private ScheduledExecutorService pingExecutor; - - @Override - public void scheduleDeviceStatusPings(Device device, DeviceStatusListener deviceStatusListener) { - stopDeviceStatusPings(); - - pingExecutor = Executors.newSingleThreadScheduledExecutor(); - pingExecutor.scheduleWithFixedDelay(() -> { - if (device.statusIp == null || device.statusIp.isEmpty()) { - deviceStatusListener.onStatusAvailable(DeviceStatus.UNKNOWN); - return; - } - - try { - final InetAddress dest = InetAddress.getByName(device.statusIp); - final Ping ping = new Ping(dest, new Ping.PingListener() { - @Override - public void onPing(final long timeMs, final int count) { - if (timeMs == -1L) { - Log.w(getClass().getSimpleName(), String.format("Ping timed out for IP %s", device.statusIp)); - deviceStatusListener.onStatusAvailable(DeviceStatus.OFFLINE); - return; - } - deviceStatusListener.onStatusAvailable(DeviceStatus.ONLINE); - } - - @Override - public void onPingException(final Exception e, final int count) { - Log.w(getClass().getSimpleName(), String.format("Error while pinging device with IP %s", device.statusIp), e); - deviceStatusListener.onStatusAvailable(DeviceStatus.OFFLINE); - } - }); - ping.setCount(1); - ping.setTimeoutMs(1000); - ping.run(); - } catch (Exception e) { - Log.w(getClass().getSimpleName(), String.format("Error while pinging device with IP %s", device.statusIp), e); - deviceStatusListener.onStatusAvailable(DeviceStatus.UNKNOWN); - } - }, 0, 4000, TimeUnit.MILLISECONDS); - } - - @Override - public void stopDeviceStatusPings() { - if (pingExecutor != null && !pingExecutor.isShutdown()) { - pingExecutor.shutdown(); - } - } -} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingDeviceStatusTesterBuilder.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingDeviceStatusTesterBuilder.java new file mode 100644 index 0000000..b2982df --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingDeviceStatusTesterBuilder.java @@ -0,0 +1,12 @@ +package de.florianisme.wakeonlan.ui.list.status; + +import de.florianisme.wakeonlan.persistence.models.Device; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTestItem; + +public class PingDeviceStatusTesterBuilder implements DeviceStatusTesterBuilder { + + public Runnable buildStatusTestCallable(Device device, StatusTestItem statusTestItem) { + return new PingRunnable(device, statusTestItem); + } + +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingRunnable.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingRunnable.java new file mode 100644 index 0000000..30c8fb5 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/PingRunnable.java @@ -0,0 +1,68 @@ +package de.florianisme.wakeonlan.ui.list.status; + +import android.util.Log; + +import java.net.InetAddress; + +import de.florianisme.wakeonlan.persistence.models.Device; +import de.florianisme.wakeonlan.persistence.models.DeviceStatus; +import de.florianisme.wakeonlan.ping.Ping; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTestItem; + +public class PingRunnable implements Runnable { + + private final Device device; + private final StatusTestItem statusTestItem; + + private boolean skipRunningExecutionResults = false; + + public PingRunnable(Device device, StatusTestItem statusTestItem) { + this.device = device; + this.statusTestItem = statusTestItem; + } + + public void cancelStatusUpdates() { + skipRunningExecutionResults = true; + } + + @Override + public void run() { + if (device.statusIp == null || device.statusIp.isEmpty()) { + notifyDeviceStautsListeners(DeviceStatus.UNKNOWN); + return; + } + + try { + final InetAddress dest = InetAddress.getByName(device.statusIp); + final Ping ping = new Ping(dest, new Ping.PingListener() { + @Override + public void onPing(final long timeMs) { + if (timeMs == -1L) { + Log.w(getClass().getSimpleName(), String.format("Ping timed out for IP %s", device.statusIp)); + notifyDeviceStautsListeners(DeviceStatus.OFFLINE); + return; + } + notifyDeviceStautsListeners(DeviceStatus.ONLINE); + } + + @Override + public void onPingException(final Exception e) { + Log.w(getClass().getSimpleName(), String.format("Error while pinging device with IP %s", device.statusIp), e); + notifyDeviceStautsListeners(DeviceStatus.OFFLINE); + } + }); + ping.setTimeoutMs(1000); + ping.run(); + } catch (Exception e) { + Log.w(getClass().getSimpleName(), String.format("Error while pinging device with IP %s", device.statusIp), e); + notifyDeviceStautsListeners(DeviceStatus.UNKNOWN); + } + } + + private void notifyDeviceStautsListeners(DeviceStatus offline) { + if (!skipRunningExecutionResults) { + statusTestItem.forAllListeners(deviceStatusListener -> deviceStatusListener.onStatusAvailable(offline)); + } + } + +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/PingStatusTesterPool.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/PingStatusTesterPool.java new file mode 100644 index 0000000..8b56846 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/PingStatusTesterPool.java @@ -0,0 +1,130 @@ +package de.florianisme.wakeonlan.ui.list.status.pool; + +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import de.florianisme.wakeonlan.persistence.models.Device; +import de.florianisme.wakeonlan.ui.list.status.DeviceStatusListener; +import de.florianisme.wakeonlan.ui.list.status.DeviceStatusTesterBuilder; +import de.florianisme.wakeonlan.ui.list.status.PingDeviceStatusTesterBuilder; + +public class PingStatusTesterPool implements StatusTesterPool { + + private static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(15); + private static final DeviceStatusTesterBuilder DEVICE_STATUS_TESTER = new PingDeviceStatusTesterBuilder(); + + private static final Object STATUS_LOCK = new Object(); + + private static Map statusCheckMap = new HashMap<>(15); + + private static StatusTesterPool INSTANCE; + + private PingStatusTesterPool() { + // No instantiation + } + + public static synchronized StatusTesterPool getInstance() { + if (INSTANCE == null) { + INSTANCE = new PingStatusTesterPool(); + } + + return INSTANCE; + } + + @Override + public void scheduleStatusTest(Device device, DeviceStatusListener deviceStatusListener, StatusTestType statusTestType) { + synchronized (STATUS_LOCK) { + StatusTestItem statusTestItem; + + if (statusCheckMap.containsKey(device.id) && statusCheckMap.get(device.id) != null) { + Log.w(getClass().getSimpleName(), "Another status check already running for device " + device.name); + + statusTestItem = statusCheckMap.get(device.id); + + if (statusTestItem == null) { + throw new IllegalStateException("Item can not be null at this point"); + } + + statusTestItem.addOrReplaceStatusListener(statusTestType, deviceStatusListener); + } else { + statusTestItem = new StatusTestItem(device); + statusTestItem.addOrReplaceStatusListener(statusTestType, deviceStatusListener); + } + + ScheduledFuture scheduledFuture = + EXECUTOR.scheduleWithFixedDelay(DEVICE_STATUS_TESTER.buildStatusTestCallable(device, statusTestItem), + 0, 2, TimeUnit.SECONDS); + statusTestItem.setOrUpdateRunnable(scheduledFuture); + + statusCheckMap.put(device.id, statusTestItem); + Log.d(getClass().getSimpleName(), "Successfully scheduled new status check for device " + device.name + " of type " + statusTestType); + Log.d(getClass().getSimpleName(), "Total of " + statusCheckMap.size() + " status checks currently running"); + } + } + + @Override + public void stopStatusTest(Device device, StatusTestType testType) { + Log.d(getClass().getSimpleName(), "Stopping status checks for device " + device.name + " of type " + testType); + + synchronized (STATUS_LOCK) { + StatusTestItem statusTestItem = statusCheckMap.get(device.id); + + if (statusTestItem == null) { + return; + } + + if (statusTestItem.removeListenerAndCancelIfApplicable(testType)) { + statusCheckMap.remove(device.id); + } + } + } + + @Override + public void stopAllStatusTesters(StatusTestType testType) { + Log.d(getClass().getSimpleName(), "Stopping all status checks of type " + testType); + + Map updatedList = new HashMap<>(8); + + synchronized (STATUS_LOCK) { + statusCheckMap.values().forEach(statusTestItem -> { + if (!statusTestItem.removeListenerAndCancelIfApplicable(testType)) { + updatedList.put(statusTestItem.getDevice().id, statusTestItem); + } + }); + + statusCheckMap = updatedList; + } + } + + @Override + public void pauseAllForType(StatusTestType testType) { + Log.i(getClass().getSimpleName(), "Pausing all pings of type " + testType); + + synchronized (STATUS_LOCK) { + statusCheckMap.values().forEach(item -> item.pausePingRunnable(testType)); + } + } + + @Override + public void resumeAll() { + Log.i(getClass().getSimpleName(), "Resuming all previously scheduled pings"); + + synchronized (STATUS_LOCK) { + statusCheckMap.values().forEach(item -> { + Device device = item.getDevice(); + + ScheduledFuture scheduledFuture = + EXECUTOR.scheduleWithFixedDelay(DEVICE_STATUS_TESTER.buildStatusTestCallable(device, item), + 0, 2, TimeUnit.SECONDS); + item.setOrUpdateRunnable(scheduledFuture); + }); + } + } + +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTestItem.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTestItem.java new file mode 100644 index 0000000..4c18c1a --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTestItem.java @@ -0,0 +1,72 @@ +package de.florianisme.wakeonlan.ui.list.status.pool; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.function.Consumer; + +import de.florianisme.wakeonlan.persistence.models.Device; +import de.florianisme.wakeonlan.ui.list.status.DeviceStatusListener; + +public class StatusTestItem { + + private final Device device; + private ScheduledFuture runnable; + + private final Map listeners = new HashMap<>(3); + + public StatusTestItem(Device device) { + this.device = device; + } + + void addOrReplaceStatusListener(StatusTestType testType, DeviceStatusListener deviceStatusListener) { + synchronized (listeners) { + listeners.put(testType, deviceStatusListener); + } + } + + boolean removeListenerAndCancelIfApplicable(StatusTestType statusTestType) { + synchronized (listeners) { + listeners.remove(statusTestType); + + if (listeners.isEmpty()) { + cancelRunnable(); + return true; + } + return false; + } + } + + void setOrUpdateRunnable(ScheduledFuture scheduledFuture) { + if (this.runnable != null) { + cancelRunnable(); + } + this.runnable = scheduledFuture; + } + + private void cancelRunnable() { + this.runnable.cancel(true); + } + + public synchronized void forAllListeners(Consumer consumer) { + synchronized (listeners) { + listeners.values().forEach(consumer); + } + } + + public Device getDevice() { + return device; + } + + public void pausePingRunnable(StatusTestType testType) { + synchronized (listeners) { + if (runnableOnlyRunningForType(testType)) { + cancelRunnable(); + } + } + } + + private boolean runnableOnlyRunningForType(StatusTestType testType) { + return listeners.keySet().size() == 1 && listeners.containsKey(testType); + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTestType.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTestType.java new file mode 100644 index 0000000..8e3327a --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTestType.java @@ -0,0 +1,5 @@ +package de.florianisme.wakeonlan.ui.list.status.pool; + +public enum StatusTestType { + LIST, TILE, QUICK_ACCESS +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTesterPool.java b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTesterPool.java new file mode 100644 index 0000000..ae794b7 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/list/status/pool/StatusTesterPool.java @@ -0,0 +1,17 @@ +package de.florianisme.wakeonlan.ui.list.status.pool; + +import de.florianisme.wakeonlan.persistence.models.Device; +import de.florianisme.wakeonlan.ui.list.status.DeviceStatusListener; + +public interface StatusTesterPool { + + void scheduleStatusTest(Device device, DeviceStatusListener deviceStatusListener, StatusTestType testType); + + void stopStatusTest(Device device, StatusTestType testType); + + void stopAllStatusTesters(StatusTestType testType); + + void pauseAllForType(StatusTestType testType); + + void resumeAll(); +} 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 5d69b38..74b6cc5 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 @@ -22,8 +22,8 @@ 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; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTestType; +import de.florianisme.wakeonlan.ui.list.status.pool.StatusTesterPool; import de.florianisme.wakeonlan.ui.modify.EditDeviceActivity; import de.florianisme.wakeonlan.wol.WolSender; @@ -37,9 +37,11 @@ public class DeviceItemViewHolder extends RecyclerView.ViewHolder { private final Button sendWolButton; private final Button shutdownButton; private final DeviceClickedCallback deviceClickedCallback; - private final DeviceStatusTester deviceStatusTester; + private final StatusTesterPool statusTesterPool; - public DeviceItemViewHolder(View view, DeviceClickedCallback deviceClickedCallback) { + private Device device; + + public DeviceItemViewHolder(View view, DeviceClickedCallback deviceClickedCallback, StatusTesterPool statusTesterPool) { super(view); deviceStatus = view.findViewById(R.id.device_status); deviceName = view.findViewById(R.id.device_name); @@ -49,15 +51,19 @@ public DeviceItemViewHolder(View view, DeviceClickedCallback deviceClickedCallba sendWolButton = view.findViewById(R.id.send_wol); shutdownButton = view.findViewById(R.id.shutdown); this.deviceClickedCallback = deviceClickedCallback; - this.deviceStatusTester = new PingDeviceStatusTester(); + this.statusTesterPool = statusTesterPool; } - public void setDeviceName(String name) { - deviceName.setText(name); - } + public synchronized void fromDevice(Device device) { + this.device = device; - public void setDeviceMacAddress(String mac) { - deviceMacAddress.setText(mac); + deviceName.setText(device.name); + deviceMacAddress.setText(device.macAddress); + + setOnClickHandler(device); + setOnEditClickHandler(device); + setShutdownVisibilityAndClickHandler(device); + startDeviceStatusQuery(device); } public void setOnClickHandler(Device device) { @@ -73,7 +79,7 @@ public void setOnEditClickHandler(Device device) { Intent intent = new Intent(context, EditDeviceActivity.class); Bundle bundle = new Bundle(); - bundle.putInt(EditDeviceActivity.MACHINE_ID_KEY, device.id); + bundle.putParcelable(EditDeviceActivity.DEVICE_PARCELABLE_KEY, device); intent.putExtras(bundle); context.startActivity(intent); }); @@ -96,7 +102,7 @@ public void startDeviceStatusQuery(Device device) { deviceStatus.clearAnimation(); deviceStatus.setBackground(AppCompatResources.getDrawable(itemView.getContext(), R.drawable.device_status_unknown)); - deviceStatusTester.scheduleDeviceStatusPings(device, status -> { + statusTesterPool.scheduleStatusTest(device, status -> { if (status == DeviceStatus.ONLINE) { setAlphaAnimationIfNotSet(); setStatusDrawable(R.drawable.device_status_online); @@ -107,7 +113,7 @@ public void startDeviceStatusQuery(Device device) { deviceStatus.clearAnimation(); deviceStatus.setBackground(AppCompatResources.getDrawable(itemView.getContext(), R.drawable.device_status_unknown)); } - }); + }, StatusTestType.LIST); } private void setStatusDrawable(int statusDrawable) { @@ -133,8 +139,8 @@ private void setAlphaAnimationIfNotSet() { } public void cancelStatusUpdates() { - if (deviceStatusTester != null) { - deviceStatusTester.stopDeviceStatusPings(); + if (statusTesterPool != null && device != null) { + statusTesterPool.stopStatusTest(device, StatusTestType.LIST); } } } \ No newline at end of file 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 829a747..21e5724 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 @@ -62,6 +62,7 @@ protected Device buildDeviceFromInputs() { protected boolean inputsHaveNotChanged() { // There is no persisted device yet, so we check if any of our inputs are edited return getDeviceNameInputText().isEmpty() && getDeviceMacInputText().isEmpty() + && getPort() == 9 && getDeviceBroadcastAddressText().isEmpty() && getDeviceStatusIpText().isEmpty() && getDeviceSecureOnPassword().isEmpty() && !getDeviceRemoteShutdownEnabled() && getDeviceSshAddress().isEmpty() && getDeviceSshPort() == -1 && getDeviceSshUsername().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 d3f043f..c3d274b 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 @@ -17,7 +17,7 @@ public class EditDeviceActivity extends ModifyDeviceActivity { - public static final String MACHINE_ID_KEY = "machineId"; + public static final String DEVICE_PARCELABLE_KEY = "machine"; private Device device; @Override @@ -29,24 +29,18 @@ protected void onCreate(Bundle savedInstanceState) { private void populateInputs() { Bundle extras = getIntent().getExtras(); if (extras != null) { - int machineId = extras.getInt(MACHINE_ID_KEY, -1); - if (machineId == -1) { + device = extras.getParcelable(DEVICE_PARCELABLE_KEY); + if (device == null) { Toast.makeText(this, R.string.edit_machine_error_loading, Toast.LENGTH_SHORT).show(); finish(); return; } - device = deviceRepository.getById(machineId); - deviceNameInput.setText(device.name); deviceStatusIpInput.setText(device.statusIp); deviceMacInput.setText(device.macAddress); deviceBroadcastInput.setText(device.broadcastAddress); - if (device.port == 9) { - devicePorts.setText("9", false); - } else { - devicePorts.setText("7", false); - } + devicePorts.setText(String.valueOf(device.port)); deviceSecureOnPassword.setText(device.secureOnPassword); deviceEnableRemoteShutdown.setChecked(device.remoteShutdownEnabled); 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 5cc3610..bfec233 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 @@ -5,7 +5,6 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; -import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; @@ -20,7 +19,6 @@ import androidx.constraintlayout.widget.ConstraintLayout; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.textfield.MaterialAutoCompleteTextView; import com.google.android.material.textfield.TextInputEditText; import com.google.common.base.Strings; import com.google.common.base.Throwables; @@ -50,6 +48,7 @@ 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.PortValidator; import de.florianisme.wakeonlan.ui.modify.watcher.validator.SecureOnPasswordValidator; public abstract class ModifyDeviceActivity extends AppCompatActivity { @@ -63,7 +62,7 @@ public abstract class ModifyDeviceActivity extends AppCompatActivity { protected TextInputEditText deviceBroadcastInput; protected TextInputEditText deviceSecureOnPassword; protected ImageButton broadcastAutofill; - protected MaterialAutoCompleteTextView devicePorts; + protected TextInputEditText devicePorts; protected ConstraintLayout deviceRemoteShutdownContainer; protected SwitchCompat deviceEnableRemoteShutdown; protected TextInputEditText deviceSshAddressInput; @@ -106,7 +105,6 @@ protected void onCreate(Bundle savedInstanceState) { addValidators(); addAutofillClickHandler(); setRemoteDeviceShutdownSwitchListener(); - addDevicePortsAdapter(); setOnTestSshShutdownListenerClickedListener(); } @@ -129,6 +127,8 @@ private void addValidators() { deviceMacInput.addTextChangedListener(new MacValidator(deviceMacInput)); deviceMacInput.addTextChangedListener(new MacAddressAutocomplete()); + devicePorts.addTextChangedListener(new PortValidator(devicePorts)); + deviceNameInput.addTextChangedListener(new InputNotEmptyValidator(deviceNameInput, R.string.add_device_error_name_empty)); deviceSecureOnPassword.addTextChangedListener(new SecureOnPasswordValidator(deviceSecureOnPassword)); @@ -148,6 +148,7 @@ private void addValidators() { protected boolean assertInputsNotEmptyAndValid() { return deviceMacInput.getError() == null && isNotEmpty(deviceMacInput) && + devicePorts.getError() == null && deviceNameInput.getError() == null && isNotEmpty(deviceNameInput) && deviceStatusIpInput.getError() == null && deviceSecureOnPassword.getError() == null && @@ -165,13 +166,6 @@ private boolean isEmpty(TextInputEditText inputEditText) { return inputEditText.getText() == null || inputEditText.getText().length() == 0; } - private void addDevicePortsAdapter() { - ArrayAdapter stringArrayAdapter = new ArrayAdapter<>(this, R.layout.modify_device_port_dropdown, - getResources().getStringArray(R.array.ports_selection)); - devicePorts.setAdapter(stringArrayAdapter); - devicePorts.setText("9", false); - } - protected void checkAndPersistDevice() { triggerValidators(); if (assertInputsNotEmptyAndValid()) { @@ -186,6 +180,7 @@ private void triggerValidators() { deviceNameInput.setText(deviceNameInput.getText()); deviceBroadcastInput.setText(deviceBroadcastInput.getText()); deviceMacInput.setText(deviceMacInput.getText()); + devicePorts.setText(devicePorts.getText()); deviceSecureOnPassword.setText(deviceSecureOnPassword.getText()); deviceSshAddressInput.setText(deviceSshAddressInput.getText()); deviceSshUsernameInput.setText(deviceSshUsernameInput.getText()); @@ -334,7 +329,15 @@ private TextView getResultMessageView(LinearLayout layout) { abstract protected boolean inputsHaveNotChanged(); protected int getPort() { - return "7".equals(binding.device.devicePorts.getText().toString()) ? 7 : 9; + try { + String wakePort = getInputText(devicePorts); + if (Strings.nullToEmpty(wakePort).isEmpty()) { + return 9; + } + return Integer.parseInt(wakePort); + } catch (NumberFormatException e) { + return 9; + } } @NonNull diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/PortValidator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/PortValidator.java new file mode 100644 index 0000000..052fb9b --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/modify/watcher/validator/PortValidator.java @@ -0,0 +1,34 @@ +package de.florianisme.wakeonlan.ui.modify.watcher.validator; + +import android.widget.EditText; + +import com.google.common.base.Strings; + +import de.florianisme.wakeonlan.R; + +public class PortValidator extends Validator { + + public PortValidator(EditText editTextView) { + super(editTextView); + } + + @Override + public ValidationResult validate(String wakePort) { + try { + if (Strings.nullToEmpty(wakePort).isEmpty()) { + return ValidationResult.VALID; + } + int parsedPort = Integer.parseInt(wakePort); + + return parsedPort > 0 && parsedPort <= 65535 ? ValidationResult.VALID : ValidationResult.INVALID; + } catch (NumberFormatException e) { + return ValidationResult.INVALID; + } + } + + @Override + int getErrorMessageStringId() { + return R.string.add_device_error_port_invalid; + } + +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/wear/WearClient.java b/app/src/main/java/de/florianisme/wakeonlan/wear/WearClient.java index 1f67ced..9f04b75 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/wear/WearClient.java +++ b/app/src/main/java/de/florianisme/wakeonlan/wear/WearClient.java @@ -3,13 +3,14 @@ import android.content.Context; import android.util.Log; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.android.gms.wearable.DataClient; import com.google.android.gms.wearable.PutDataMapRequest; import com.google.android.gms.wearable.PutDataRequest; import com.google.android.gms.wearable.Wearable; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -38,8 +39,8 @@ private byte[] buildDevicesListByteArray(List devices) { List deviceDtos = devices.stream() .map(device -> new DeviceDto(device.id, device.name)) .collect(Collectors.toList()); - return new ObjectMapper().writeValueAsBytes(deviceDtos); - } catch (JsonProcessingException e) { + return new Gson().toJson(deviceDtos).getBytes(StandardCharsets.UTF_8); + } catch (JsonParseException e) { Log.e(getClass().getSimpleName(), "Could not transform list of devices to byte array", e); return new byte[0]; } diff --git a/app/src/main/res/drawable/ic_github_logo.xml b/app/src/main/res/drawable/ic_github_logo.xml new file mode 100644 index 0000000..f0f4c70 --- /dev/null +++ b/app/src/main/res/drawable/ic_github_logo.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0650348..3d83d1a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -37,6 +37,7 @@ android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" + app:headerLayout="@layout/header_navigation_drawer" app:itemIconTint="@drawable/drawer_item_color" app:itemTextColor="@drawable/drawer_item_color" app:menu="@menu/activity_main_drawer" /> diff --git a/app/src/main/res/layout/content_modify_device.xml b/app/src/main/res/layout/content_modify_device.xml index 0f0d3c5..51bd4e5 100644 --- a/app/src/main/res/layout/content_modify_device.xml +++ b/app/src/main/res/layout/content_modify_device.xml @@ -72,37 +72,40 @@ app:layout_constraintTop_toBottomOf="@id/device_title_connectivity" app:placeholderText="AB:12:CD:34:EF:56"> - - + + - - - - + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:hint="@string/add_device_port" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_title_connectivity"> + + + - - - - - + + - + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:hint="@string/add_device_status_ip" + app:helperText="@string/add_device_status_ip_helper" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/device_title_status" + app:placeholderText="192.168.0.100"> + + + @@ -345,13 +348,13 @@