diff --git a/app/build.gradle b/app/build.gradle index 7763931..5180c23 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,7 @@ apply from: "$rootProject.projectDir/shared-build.gradle" android { defaultConfig { - versionCode 28 + versionCode 33 wearAppUnbundled true } buildFeatures { @@ -19,6 +19,7 @@ dependencies { implementation 'androidx.navigation:navigation-fragment:2.4.1' implementation 'androidx.navigation:navigation-ui:2.4.1' implementation 'com.google.guava:guava:31.0.1-jre' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' def room_version = "2.4.1" implementation "androidx.room:room-runtime:$room_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a85f0de..a648d3b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + @@ -22,13 +23,17 @@ + optionalMachine = getMachineAtIndex(machineAtIndex()); + Tile tile = super.getQsTile(); + if (optionalMachine.isPresent()) { Device device = optionalMachine.get(); this.device = device; - super.getQsTile().setLabel(device.name); + tile.setLabel(device.name); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - super.getQsTile().setSubtitle(getString(R.string.tile_subtitle)); + tile.setSubtitle(getString(R.string.tile_subtitle)); } - super.getQsTile().setState(Tile.STATE_ACTIVE); + tile.setState(Tile.STATE_ACTIVE); } else { - super.getQsTile().setLabel(getString(R.string.tile_no_device_found)); - super.getQsTile().setState(Tile.STATE_UNAVAILABLE); + tile.setLabel(getString(R.string.tile_no_device_found)); + tile.setState(Tile.STATE_UNAVAILABLE); } - super.getQsTile().updateTile(); + tile.updateTile(); } private Optional getMachineAtIndex(int index) { 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 0498a8a..e77b558 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/MainActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/MainActivity.java @@ -54,7 +54,7 @@ private void initializeNavController() { } private Set getMenuIds() { - return Sets.newHashSet(R.id.deviceListFragment, R.id.backupFragment); + return Sets.newHashSet(R.id.deviceListFragment, R.id.backupFragment, R.id.networkScanFragment); } @Override diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/AddDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/AddDeviceActivity.java similarity index 96% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/AddDeviceActivity.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/AddDeviceActivity.java index d2c1c8c..6243362 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/AddDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/AddDeviceActivity.java @@ -1,4 +1,4 @@ -package de.florianisme.wakeonlan.ui.home.modify; +package de.florianisme.wakeonlan.ui.home.details; import android.view.Menu; import android.view.MenuItem; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/AddNetworkScanDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/AddNetworkScanDeviceActivity.java new file mode 100644 index 0000000..7742736 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/AddNetworkScanDeviceActivity.java @@ -0,0 +1,27 @@ +package de.florianisme.wakeonlan.ui.home.details; + +import android.os.Bundle; + +public class AddNetworkScanDeviceActivity extends AddDeviceActivity { + + public static final String MACHINE_IP_KEY = "deviceIp"; + public static final String MACHINE_NAME_KEY = "deviceName"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + prepopulateInputs(); + } + + private void prepopulateInputs() { + Bundle extras = getIntent().getExtras(); + if (extras != null) { + String machineName = extras.getString(MACHINE_NAME_KEY, null); + String machineIp = extras.getString(MACHINE_IP_KEY, null); + + deviceNameInput.setText(machineName); + deviceStatusIpInput.setText(machineIp); + broadcastAutofill.callOnClick(); + } + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/EditDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/EditDeviceActivity.java similarity index 98% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/EditDeviceActivity.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/EditDeviceActivity.java index 4dd8c5f..d971d7e 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/EditDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/EditDeviceActivity.java @@ -1,4 +1,4 @@ -package de.florianisme.wakeonlan.ui.home.modify; +package de.florianisme.wakeonlan.ui.home.details; import android.os.Bundle; import android.view.Menu; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/ModifyDeviceActivity.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/ModifyDeviceActivity.java similarity index 89% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/ModifyDeviceActivity.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/ModifyDeviceActivity.java index 9da0e18..6b0e438 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/ModifyDeviceActivity.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/ModifyDeviceActivity.java @@ -1,9 +1,10 @@ -package de.florianisme.wakeonlan.ui.home.modify; +package de.florianisme.wakeonlan.ui.home.details; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; +import android.widget.ImageButton; import android.widget.Toast; import androidx.annotation.NonNull; @@ -21,10 +22,10 @@ import de.florianisme.wakeonlan.databinding.ActivityModifyDeviceBinding; import de.florianisme.wakeonlan.persistence.AppDatabase; import de.florianisme.wakeonlan.persistence.DatabaseInstanceManager; -import de.florianisme.wakeonlan.ui.home.modify.watcher.autocomplete.MacAddressAutocomplete; -import de.florianisme.wakeonlan.ui.home.modify.watcher.validator.IpAddressValidator; -import de.florianisme.wakeonlan.ui.home.modify.watcher.validator.MacValidator; -import de.florianisme.wakeonlan.ui.home.modify.watcher.validator.NameValidator; +import de.florianisme.wakeonlan.ui.home.details.watcher.autocomplete.MacAddressAutocomplete; +import de.florianisme.wakeonlan.ui.home.details.watcher.validator.IpAddressValidator; +import de.florianisme.wakeonlan.ui.home.details.watcher.validator.MacValidator; +import de.florianisme.wakeonlan.ui.home.details.watcher.validator.NameValidator; import de.florianisme.wakeonlan.util.BroadcastHelper; public abstract class ModifyDeviceActivity extends AppCompatActivity { @@ -36,6 +37,7 @@ public abstract class ModifyDeviceActivity extends AppCompatActivity { protected TextInputEditText deviceNameInput; protected TextInputEditText deviceStatusIpInput; protected TextInputEditText deviceBroadcastInput; + protected ImageButton broadcastAutofill; protected MaterialAutoCompleteTextView devicePorts; @Override @@ -50,6 +52,7 @@ protected void onCreate(Bundle savedInstanceState) { deviceNameInput = binding.device.deviceName; deviceStatusIpInput = binding.device.deviceStatusIp; deviceBroadcastInput = binding.device.deviceBroadcast; + broadcastAutofill = binding.device.broadcastAutofill; setSupportActionBar(binding.toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); @@ -62,12 +65,12 @@ protected void onCreate(Bundle savedInstanceState) { } private void addAutofillClickHandler() { - binding.device.broadcastAutofill.setOnClickListener(new View.OnClickListener() { + broadcastAutofill.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { Optional broadcastAddress = BroadcastHelper.getBroadcastAddress(); - broadcastAddress.ifPresent(inetAddress -> binding.device.deviceBroadcast.setText(inetAddress.getHostAddress())); + broadcastAddress.ifPresent(inetAddress -> deviceBroadcastInput.setText(inetAddress.getHostAddress())); } catch (IOException e) { Log.e(this.getClass().getName(), "Can not retrieve Broadcast Address", e); } diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/autocomplete/MacAddressAutocomplete.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/autocomplete/MacAddressAutocomplete.java similarity index 96% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/autocomplete/MacAddressAutocomplete.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/autocomplete/MacAddressAutocomplete.java index 086e5a0..d8cd506 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/autocomplete/MacAddressAutocomplete.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/autocomplete/MacAddressAutocomplete.java @@ -1,4 +1,4 @@ -package de.florianisme.wakeonlan.ui.home.modify.watcher.autocomplete; +package de.florianisme.wakeonlan.ui.home.details.watcher.autocomplete; import android.text.Editable; import android.text.TextWatcher; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/IpAddressValidator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/IpAddressValidator.java similarity index 94% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/IpAddressValidator.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/IpAddressValidator.java index 6b08513..e7e3448 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/IpAddressValidator.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/IpAddressValidator.java @@ -1,4 +1,4 @@ -package de.florianisme.wakeonlan.ui.home.modify.watcher.validator; +package de.florianisme.wakeonlan.ui.home.details.watcher.validator; import android.widget.EditText; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/MacValidator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/MacValidator.java similarity index 91% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/MacValidator.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/MacValidator.java index 3d73ce2..4a2a67a 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/MacValidator.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/MacValidator.java @@ -1,4 +1,4 @@ -package de.florianisme.wakeonlan.ui.home.modify.watcher.validator; +package de.florianisme.wakeonlan.ui.home.details.watcher.validator; import android.widget.EditText; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/NameValidator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/NameValidator.java similarity index 87% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/NameValidator.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/NameValidator.java index 4999104..7ebe344 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/NameValidator.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/NameValidator.java @@ -1,4 +1,4 @@ -package de.florianisme.wakeonlan.ui.home.modify.watcher.validator; +package de.florianisme.wakeonlan.ui.home.details.watcher.validator; import android.widget.EditText; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/ValidationResult.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/ValidationResult.java new file mode 100644 index 0000000..efed941 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/ValidationResult.java @@ -0,0 +1,5 @@ +package de.florianisme.wakeonlan.ui.home.details.watcher.validator; + +public enum ValidationResult { + VALID, INVALID +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/Validator.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/Validator.java similarity index 94% rename from app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/Validator.java rename to app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/Validator.java index d7b3553..13e88a9 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/Validator.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/details/watcher/validator/Validator.java @@ -1,4 +1,4 @@ -package de.florianisme.wakeonlan.ui.home.modify.watcher.validator; +package de.florianisme.wakeonlan.ui.home.details.watcher.validator; import android.text.Editable; import android.text.TextWatcher; diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/list/viewholder/DeviceItemViewHolder.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/list/viewholder/DeviceItemViewHolder.java index 31f287e..f1e4210 100644 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/list/viewholder/DeviceItemViewHolder.java +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/list/viewholder/DeviceItemViewHolder.java @@ -18,8 +18,8 @@ import de.florianisme.wakeonlan.persistence.models.DeviceStatus; import de.florianisme.wakeonlan.status.DeviceStatusTester; import de.florianisme.wakeonlan.status.PingDeviceStatusTester; +import de.florianisme.wakeonlan.ui.home.details.EditDeviceActivity; import de.florianisme.wakeonlan.ui.home.list.DeviceClickedCallback; -import de.florianisme.wakeonlan.ui.home.modify.EditDeviceActivity; import de.florianisme.wakeonlan.wol.WolSender; public class DeviceItemViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/ValidationResult.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/ValidationResult.java deleted file mode 100644 index 06e3c8b..0000000 --- a/app/src/main/java/de/florianisme/wakeonlan/ui/home/modify/watcher/validator/ValidationResult.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.florianisme.wakeonlan.ui.home.modify.watcher.validator; - -public enum ValidationResult { - VALID, INVALID -} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanAdapter.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanAdapter.java new file mode 100644 index 0000000..b639414 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanAdapter.java @@ -0,0 +1,106 @@ +package de.florianisme.wakeonlan.ui.home.scan; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.AsyncListDiffer; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.common.base.Strings; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import de.florianisme.wakeonlan.R; +import de.florianisme.wakeonlan.ui.home.scan.model.NetworkScanDevice; +import de.florianisme.wakeonlan.ui.home.scan.viewholder.EmptyViewHolder; +import de.florianisme.wakeonlan.ui.home.scan.viewholder.ListViewType; +import de.florianisme.wakeonlan.ui.home.scan.viewholder.ScanResultViewHolder; + +public class NetworkScanAdapter extends RecyclerView.Adapter { + + private final AsyncListDiffer listDiffer = new AsyncListDiffer<>(this, new NetworkScanDiffCallback()); + + public void clearDataset() { + listDiffer.submitList(new ArrayList<>()); + } + + public void updateList(List updatedList) { + List sortedList = updatedList.stream() + .distinct() + .sorted(getScanDeviceComparator()) + .collect(Collectors.toList()); + + listDiffer.submitList(sortedList); + } + + private Comparator getScanDeviceComparator() { + Comparator networkScanDeviceComparator = + Comparator.comparing(device -> + Strings.nullToEmpty(device.getIpAddress()) + .substring(0, device.getIpAddress() + .lastIndexOf(".") + 1)); + return networkScanDeviceComparator.reversed(); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view; + RecyclerView.ViewHolder viewHolder; + + if (ListViewType.EMPTY.ordinal() == viewType) { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.network_list_empty, parent, false); + viewHolder = new EmptyViewHolder(view); + } else { + view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.network_list_item, parent, false); + viewHolder = new ScanResultViewHolder(view); + } + + return viewHolder; + } + + @Override + public int getItemViewType(int position) { + if (listDiffer.getCurrentList().isEmpty()) { + return ListViewType.EMPTY.ordinal(); + } else { + return ListViewType.SCAN_DEVICE.ordinal(); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (getItemViewType(position) == ListViewType.SCAN_DEVICE.ordinal()) { + ScanResultViewHolder scanResultViewHolder = (ScanResultViewHolder) viewHolder; + + NetworkScanDevice networkScanDevice = listDiffer.getCurrentList().get(position); + + scanResultViewHolder.setNameIfPresent(networkScanDevice.getName()); + scanResultViewHolder.setIpAddress(networkScanDevice.getIpAddress()); + scanResultViewHolder.setOnAddClickListener(networkScanDevice); + } + } + + @Override + public long getItemId(int position) { + if (listDiffer.getCurrentList().isEmpty()) { + return RecyclerView.NO_ID; + } + return listDiffer.getCurrentList().get(position).hashCode(); + } + + @Override + public int getItemCount() { + if (listDiffer.getCurrentList().isEmpty()) { + return 1; // "Empty" item + } + return listDiffer.getCurrentList().size(); + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanDiffCallback.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanDiffCallback.java new file mode 100644 index 0000000..d27a866 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanDiffCallback.java @@ -0,0 +1,22 @@ +package de.florianisme.wakeonlan.ui.home.scan; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import com.google.common.base.Strings; + +import de.florianisme.wakeonlan.ui.home.scan.model.NetworkScanDevice; + +public class NetworkScanDiffCallback extends DiffUtil.ItemCallback { + + @Override + public boolean areItemsTheSame(@NonNull NetworkScanDevice oldItem, @NonNull NetworkScanDevice newItem) { + return areContentsTheSame(oldItem, newItem); + } + + @Override + public boolean areContentsTheSame(@NonNull NetworkScanDevice oldItem, @NonNull NetworkScanDevice newItem) { + return Strings.nullToEmpty(oldItem.getIpAddress()).equals(newItem.getIpAddress()) + && (oldItem.getName().isPresent() && newItem.getName().isPresent() && oldItem.getName().get().equals(newItem.getName().get())); + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanFragment.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanFragment.java new file mode 100644 index 0000000..6e3aba1 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanFragment.java @@ -0,0 +1,107 @@ +package de.florianisme.wakeonlan.ui.home.scan; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import de.florianisme.wakeonlan.databinding.FragmentNetworkScanBinding; +import de.florianisme.wakeonlan.ui.home.list.LinearLayoutManagerWrapper; +import de.florianisme.wakeonlan.ui.home.scan.callbacks.ScanCallback; +import de.florianisme.wakeonlan.ui.home.scan.model.NetworkScanDevice; + +public class NetworkScanFragment extends Fragment { + + private FragmentNetworkScanBinding binding; + private NetworkScanAdapter networkScanAdapter; + private NetworkScanTask networkScanTask; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + binding = FragmentNetworkScanBinding.inflate(inflater, container, false); + + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + setupRecyclerView(); + setupSwipeToRefresh(); + + startNetworkScan(); + } + + private void startNetworkScan() { + binding.swipeRefresh.setRefreshing(true); + networkScanAdapter.clearDataset(); + + if (networkScanTask != null) { + networkScanTask.cancel(); + } + + networkScanTask = new NetworkScanTask(getContext(), getScanCallback()); + Thread networkScanThread = new Thread(networkScanTask); + + networkScanThread.start(); + } + + private void setupSwipeToRefresh() { + binding.swipeRefresh.setOnRefreshListener(this::startNetworkScan); + } + + @Override + public void onPause() { + super.onPause(); + if (networkScanTask != null) { + networkScanTask.cancel(); + } + } + + private ScanCallback getScanCallback() { + List resultList = new ArrayList<>(15); + + return new ScanCallback() { + @Override + public void onError(int errorStringReference) { + getActivity().runOnUiThread(() -> Toast.makeText(getContext(), errorStringReference, Toast.LENGTH_SHORT).show()); + } + + @Override + public void onDeviceFound(String ip, String hostName) { + NetworkScanDevice networkScanDevice = new NetworkScanDevice(); + networkScanDevice.setIpAddress(ip); + if (hostName != null && !ip.equals(hostName) && !hostName.isEmpty()) { + networkScanDevice.setName(hostName); + } + + resultList.add(networkScanDevice); + networkScanAdapter.updateList(resultList); + } + + @Override + public void onTaskEnd() { + binding.swipeRefresh.setRefreshing(false); + } + }; + } + + private void setupRecyclerView() { + RecyclerView recyclerView = binding.networkList; + + networkScanAdapter = new NetworkScanAdapter(); + recyclerView.setAdapter(networkScanAdapter); + recyclerView.setLayoutManager(new LinearLayoutManagerWrapper(getContext())); + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanTask.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanTask.java new file mode 100644 index 0000000..6825e2e --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/NetworkScanTask.java @@ -0,0 +1,72 @@ +package de.florianisme.wakeonlan.ui.home.scan; + +import android.content.Context; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.text.format.Formatter; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.net.InetAddress; + +import de.florianisme.wakeonlan.R; +import de.florianisme.wakeonlan.ui.home.scan.callbacks.ScanCallback; + +public class NetworkScanTask implements Runnable { + + private final WeakReference contextWeakReference; + private final ScanCallback scanCallback; + private boolean canceled = false; + + public NetworkScanTask(Context context, ScanCallback scanCallback) { + contextWeakReference = new WeakReference<>(context); + this.scanCallback = scanCallback; + } + + @Override + public void run() { + try { + Context context = contextWeakReference.get(); + + if (context != null) { + WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + + WifiInfo connectionInfo = wm.getConnectionInfo(); + int ipAddress = connectionInfo.getIpAddress(); + String ipString = Formatter.formatIpAddress(ipAddress); + + if (ipString == null) { + scanCallback.onError(R.string.network_scan_error_ip); + return; + } + + String prefix = ipString.substring(0, ipString.lastIndexOf(".") + 1); + + for (int i = 0; i < 255; i++) { + String testIp = prefix + i; + + InetAddress address = InetAddress.getByName(testIp); + boolean reachable = address.isReachable(50); + + if (reachable) { + scanCallback.onDeviceFound(address.getHostAddress(), address.getHostName()); + } + + if (this.canceled) { + return; + } + } + } + } catch (Exception e) { + scanCallback.onError(R.string.network_scan_error_general); + scanCallback.onTaskEnd(); + Log.e(getClass().getSimpleName(), "Error while scanning network", e); + } + + scanCallback.onTaskEnd(); + } + + public void cancel() { + this.canceled = true; + } +} \ No newline at end of file diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/callbacks/ScanCallback.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/callbacks/ScanCallback.java new file mode 100644 index 0000000..b507fe5 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/callbacks/ScanCallback.java @@ -0,0 +1,11 @@ +package de.florianisme.wakeonlan.ui.home.scan.callbacks; + +public interface ScanCallback { + + void onError(int errorStringReference); + + void onDeviceFound(String ip, String hostName); + + void onTaskEnd(); + +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/model/NetworkScanDevice.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/model/NetworkScanDevice.java new file mode 100644 index 0000000..24834d5 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/model/NetworkScanDevice.java @@ -0,0 +1,43 @@ +package de.florianisme.wakeonlan.ui.home.scan.model; + +import java.util.Optional; + +public class NetworkScanDevice { + + private Optional name = Optional.empty(); + private String ipAddress; + + public String getIpAddress() { + return ipAddress; + } + + public void setName(String name) { + this.name = Optional.of(name); + } + + public Optional getName() { + return name; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NetworkScanDevice that = (NetworkScanDevice) o; + + if (name.isPresent() ? !name.equals(that.name) : that.name.isPresent()) return false; + return ipAddress.equals(that.ipAddress); + } + + @Override + public int hashCode() { + int result = name.isPresent() ? name.hashCode() : 0; + result = 31 * result + ipAddress.hashCode(); + return result; + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/EmptyViewHolder.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/EmptyViewHolder.java new file mode 100644 index 0000000..ebfe1c7 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/EmptyViewHolder.java @@ -0,0 +1,11 @@ +package de.florianisme.wakeonlan.ui.home.scan.viewholder; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +public class EmptyViewHolder extends RecyclerView.ViewHolder { + public EmptyViewHolder(View view) { + super(view); + } +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/ListViewType.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/ListViewType.java new file mode 100644 index 0000000..acb2798 --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/ListViewType.java @@ -0,0 +1,5 @@ +package de.florianisme.wakeonlan.ui.home.scan.viewholder; + +public enum ListViewType { + EMPTY, SCAN_DEVICE +} diff --git a/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/ScanResultViewHolder.java b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/ScanResultViewHolder.java new file mode 100644 index 0000000..fad29fe --- /dev/null +++ b/app/src/main/java/de/florianisme/wakeonlan/ui/home/scan/viewholder/ScanResultViewHolder.java @@ -0,0 +1,64 @@ +package de.florianisme.wakeonlan.ui.home.scan.viewholder; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.Optional; + +import de.florianisme.wakeonlan.R; +import de.florianisme.wakeonlan.ui.home.details.AddNetworkScanDeviceActivity; +import de.florianisme.wakeonlan.ui.home.scan.model.NetworkScanDevice; + +public class ScanResultViewHolder extends RecyclerView.ViewHolder { + + private final TextView deviceName; + private final TextView deviceIp; + + private final Button addDevice; + + public ScanResultViewHolder(@NonNull View itemView) { + super(itemView); + + deviceName = itemView.findViewById(R.id.scan_device_name); + deviceIp = itemView.findViewById(R.id.scan_device_ip); + addDevice = itemView.findViewById(R.id.scan_device_add); + } + + public void setIpAddress(String ipAddress) { + deviceIp.setText(ipAddress); + } + + public void setNameIfPresent(Optional name) { + if (name.isPresent()) { + deviceName.setVisibility(View.VISIBLE); + deviceName.setText(name.get()); + deviceIp.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body2); + } else { + deviceName.setVisibility(View.GONE); + deviceIp.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Headline6); + } + } + + public void setOnAddClickListener(NetworkScanDevice scanDevice) { + addDevice.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Context context = view.getContext(); + + Intent intent = new Intent(context, AddNetworkScanDeviceActivity.class); + Bundle bundle = new Bundle(); + bundle.putString(AddNetworkScanDeviceActivity.MACHINE_NAME_KEY, scanDevice.getName().orElse(null)); + bundle.putString(AddNetworkScanDeviceActivity.MACHINE_IP_KEY, scanDevice.getIpAddress()); + intent.putExtras(bundle); + context.startActivity(intent); + } + }); + } +} diff --git a/app/src/main/res/drawable/ic_search_network.xml b/app/src/main/res/drawable/ic_search_network.xml new file mode 100644 index 0000000..bffed68 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_network.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_modify_device.xml b/app/src/main/res/layout/activity_modify_device.xml index 7a9d9ae..480bc6f 100644 --- a/app/src/main/res/layout/activity_modify_device.xml +++ b/app/src/main/res/layout/activity_modify_device.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.home.modify.ModifyDeviceActivity"> + tools:context=".ui.home.details.ModifyDeviceActivity"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/network_list_empty.xml b/app/src/main/res/layout/network_list_empty.xml new file mode 100644 index 0000000..93352da --- /dev/null +++ b/app/src/main/res/layout/network_list_empty.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/network_list_item.xml b/app/src/main/res/layout/network_list_item.xml new file mode 100644 index 0000000..5edac36 --- /dev/null +++ b/app/src/main/res/layout/network_list_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index b0436f6..c0d8053 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -5,6 +5,10 @@ android:id="@+id/deviceListFragment" android:icon="@drawable/ic_device_list" android:title="@string/drawer_menu_item_device_list" /> + @@ -27,6 +27,12 @@ android:label="@string/title_fragment_device_list" tools:layout="@layout/fragment_list_devices" /> + + Fehler beim Importieren der Geräte %1$d Geräte erfolgreich exportiert Fehler beim Exportieren der Geräte + Netzwerk Scan + Gerät hinzufügen + Keine Geräte gefunden + Sind Sie mit einem WLAN Netzwerk verbunden? + IP Addresse kann nicht abgefragt werden + Fehler beim Scannen des Netzwerks \ 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 932149b..e45ccb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ Add Device Edit Device Devices + Network Scan Export/Import Data @@ -64,7 +65,15 @@ Successfully exported %1$d devices Error while exporting devices + + Add Device + No devices found + Are you connected to a WiFI Network? + Can not obtain IP Address + Error while scanning network + @string/title_fragment_device_list + @string/title_fragment_network_scan @string/title_fragment_backup \ No newline at end of file diff --git a/shared-build.gradle b/shared-build.gradle index e7ca733..99274c4 100644 --- a/shared-build.gradle +++ b/shared-build.gradle @@ -19,7 +19,7 @@ android { minSdk 24 targetSdk 31 versionCode 4 - versionName "1.3.1" + versionName "1.4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/wear/build.gradle b/wear/build.gradle index 2a7ee7d..3156598 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -3,7 +3,7 @@ apply from: "$rootProject.projectDir/shared-build.gradle" android { defaultConfig { - versionCode 27 + versionCode 32 } }