diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index 48309b20..329f3715 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -88,13 +88,16 @@ interface IMeshService { void setChannels(in byte []payload); /// Send Shutdown admin packet to nodeNum - void requestShutdown(in String nodeId); + void requestShutdown(in int idNum); /// Send Reboot admin packet to nodeNum - void requestReboot(in String nodeId); + void requestReboot(in int idNum); /// Send FactoryReset admin packet to nodeNum - void requestFactoryReset(in String nodeId); + void requestFactoryReset(in int idNum); + + /// Send NodedbReset admin packet to nodeNum + void requestNodedbReset(in int idNum); /** Is the packet radio currently connected to the phone? Returns a ConnectionState string. diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 9837b697..bb8094e5 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -234,11 +234,6 @@ class UIViewModel @Inject constructor( // We consider hasWifi = ESP32 fun isESP32() = myNodeInfo.value?.hasWifi == true - fun hasAXP(): Boolean { - val hasAXP = listOf(4, 7, 9) // mesh.proto 'HardwareModel' enums with AXP192 chip - return hasAXP.contains(nodeDB.ourNodeInfo?.user?.hwModel?.number) - } - /// hardware info about our local device (can be null) private val _myNodeInfo = MutableLiveData() val myNodeInfo: LiveData get() = _myNodeInfo @@ -357,16 +352,36 @@ class UIViewModel @Inject constructor( } } - fun requestShutdown() { - meshService?.requestShutdown(DataPacket.ID_LOCAL) + fun requestShutdown(idNum: Int) { + try { + meshService?.requestShutdown(idNum) + } catch (ex: RemoteException) { + errormsg("RemoteException: ${ex.message}") + } } - fun requestReboot() { - meshService?.requestReboot(DataPacket.ID_LOCAL) + fun requestReboot(idNum: Int) { + try { + meshService?.requestReboot(idNum) + } catch (ex: RemoteException) { + errormsg("RemoteException: ${ex.message}") + } } - fun requestFactoryReset() { - meshService?.requestFactoryReset(DataPacket.ID_LOCAL) + fun requestFactoryReset(idNum: Int) { + try { + meshService?.requestFactoryReset(idNum) + } catch (ex: RemoteException) { + errormsg("RemoteException: ${ex.message}") + } + } + + fun requestNodedbReset(idNum: Int) { + try { + meshService?.requestNodedbReset(idNum) + } catch (ex: RemoteException) { + errormsg("RemoteException: ${ex.message}") + } } /** diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 2ad6b954..3f3ab92e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1341,7 +1341,7 @@ class MeshService : Service(), Logging { else { discardNodeDB() debug("Installing new node DB") - myNodeInfo = newMyNodeInfo// Install myNodeInfo as current + myNodeInfo = newMyNodeInfo // Install myNodeInfo as current newNodes.forEach(::installNodeInfo) newNodes.clear() // Just to save RAM ;-) @@ -1383,24 +1383,30 @@ class MeshService : Service(), Logging { }) } - private fun requestShutdown(nodeId: String) { - sendToRadio(newMeshPacketTo(toNodeNum(nodeId)).buildAdminPacket { + private fun requestShutdown(idNum: Int) { + sendToRadio(newMeshPacketTo(idNum).buildAdminPacket { shutdownSeconds = 5 }) } - private fun requestReboot(nodeId: String) { - sendToRadio(newMeshPacketTo(toNodeNum(nodeId)).buildAdminPacket { + private fun requestReboot(idNum: Int) { + sendToRadio(newMeshPacketTo(idNum).buildAdminPacket { rebootSeconds = 5 }) } - private fun requestFactoryReset(nodeId: String) { - sendToRadio(newMeshPacketTo(toNodeNum(nodeId)).buildAdminPacket { + private fun requestFactoryReset(idNum: Int) { + sendToRadio(newMeshPacketTo(idNum).buildAdminPacket { factoryReset = 1 }) } + private fun requestNodedbReset(idNum: Int) { + sendToRadio(newMeshPacketTo(idNum).buildAdminPacket { + nodedbReset = 1 + }) + } + /** * Start the modern (REV2) API configuration flow */ @@ -1721,16 +1727,20 @@ class MeshService : Service(), Logging { stopLocationRequests() } - override fun requestShutdown(nodeId: String) = toRemoteExceptions { - this@MeshService.requestShutdown(nodeId) + override fun requestShutdown(idNum: Int) = toRemoteExceptions { + this@MeshService.requestShutdown(idNum) } - override fun requestReboot(nodeId: String) = toRemoteExceptions { - this@MeshService.requestReboot(nodeId) + override fun requestReboot(idNum: Int) = toRemoteExceptions { + this@MeshService.requestReboot(idNum) } - override fun requestFactoryReset(nodeId: String) = toRemoteExceptions { - this@MeshService.requestFactoryReset(nodeId) + override fun requestFactoryReset(idNum: Int) = toRemoteExceptions { + this@MeshService.requestFactoryReset(idNum) + } + + override fun requestNodedbReset(idNum: Int) = toRemoteExceptions { + this@MeshService.requestNodedbReset(idNum) } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt index 8e4b2435..fdb81614 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt @@ -55,9 +55,6 @@ class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging { binding.lsSleepView.isEnabled = connected && model.config.power.isPowerSaving binding.positionBroadcastSwitch.isEnabled = connected binding.lsSleepSwitch.isEnabled = connected && model.isESP32() - binding.shutdownButton.isEnabled = connected && model.hasAXP() - binding.rebootButton.isEnabled = connected - binding.factoryResetButton.isEnabled = connected } binding.positionBroadcastPeriodEditText.on(EditorInfo.IME_ACTION_DONE) { @@ -111,41 +108,5 @@ class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging { debug("User changed isPowerSaving to $isChecked") } } - - binding.shutdownButton.setOnClickListener { - MaterialAlertDialogBuilder(requireContext()) - .setMessage("${getString(R.string.shutdown)}?") - .setNeutralButton(R.string.cancel) { _, _ -> - } - .setPositiveButton(getString(R.string.okay)) { _, _ -> - debug("User clicked requestShutdown") - model.requestShutdown() - } - .show() - } - - binding.rebootButton.setOnClickListener { - MaterialAlertDialogBuilder(requireContext()) - .setMessage("${getString(R.string.reboot)}?") - .setNeutralButton(R.string.cancel) { _, _ -> - } - .setPositiveButton(getString(R.string.okay)) { _, _ -> - debug("User clicked requestReboot") - model.requestReboot() - } - .show() - } - - binding.factoryResetButton.setOnClickListener { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.are_you_sure_factory_reset) - .setMessage(R.string.factory_reset_description) - .setNeutralButton(R.string.cancel) { _, _ -> - } - .setPositiveButton(R.string.okay) { _, _ -> - model.requestFactoryReset() - } - .show() - } } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index a56dbd4a..edbccdfb 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -3,8 +3,10 @@ package com.geeksville.mesh.ui import android.os.Bundle import android.text.method.LinkMovementMethod import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.text.HtmlCompat @@ -12,13 +14,14 @@ import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResult import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.geeksville.mesh.android.Logging import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R +import com.geeksville.mesh.android.Logging import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding import com.geeksville.mesh.databinding.NodelistFragmentBinding import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.util.formatAgo +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import java.net.URLEncoder @@ -48,6 +51,90 @@ class UsersFragment : ScreenFragment("Users"), Logging { private val nodesAdapter = object : RecyclerView.Adapter() { + private var nodes = arrayOf() + + private fun popup(view: View, position: Int) { + val node = nodes[position] + val user = node.user + val showAdmin = position == 0 // TODO add admin channel check + val popup = PopupMenu(requireContext(), view) + popup.inflate(R.menu.menu_nodes) + popup.menu.findItem(R.id.direct_message).isVisible = position > 0 + popup.menu.setGroupVisible(R.id.group_admin, showAdmin) + popup.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.direct_message -> { + if (position > 0 && user != null) { + debug("calling MessagesFragment filter: 0${user.id}") + setFragmentResult( + "requestKey", + bundleOf( + "contactKey" to "0${user.id}", + "contactName" to user.longName + ) + ) + parentFragmentManager.beginTransaction() + .replace(R.id.mainActivityLayout, MessagesFragment()) + .addToBackStack(null) + .commit() + } + } + R.id.reboot -> { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("${getString(R.string.reboot)}\n${user?.longName}?") + .setIcon(R.drawable.ic_twotone_warning_24) + .setNeutralButton(R.string.cancel) { _, _ -> + } + .setPositiveButton(getString(R.string.okay)) { _, _ -> + debug("User clicked requestReboot") + model.requestReboot(node.num) + } + .show() + } + R.id.shutdown -> { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("${getString(R.string.shutdown)}\n${user?.longName}?") + .setIcon(R.drawable.ic_twotone_warning_24) + .setNeutralButton(R.string.cancel) { _, _ -> + } + .setPositiveButton(getString(R.string.okay)) { _, _ -> + debug("User clicked requestShutdown") + model.requestShutdown(node.num) + } + .show() + } + R.id.factory_reset -> { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("${getString(R.string.factory_reset)}\n${user?.longName}?") + .setIcon(R.drawable.ic_twotone_warning_24) + .setMessage(R.string.factory_reset_description) + .setNeutralButton(R.string.cancel) { _, _ -> + } + .setPositiveButton(R.string.okay) { _, _ -> + debug("User clicked requestFactoryReset") + model.requestFactoryReset(node.num) + } + .show() + } + R.id.nodedb_reset -> { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("${getString(R.string.nodedb_reset)}\n${user?.longName}?") + .setIcon(R.drawable.ic_twotone_warning_24) + .setMessage(R.string.nodedb_reset_description) + .setNeutralButton(R.string.cancel) { _, _ -> + } + .setPositiveButton(getString(R.string.okay)) { _, _ -> + debug("User clicked requestNodedbReset") + model.requestNodedbReset(node.num) + } + .show() + } + } + true + } + popup.show() + } + /** * Called when RecyclerView needs a new [ViewHolder] of the given type to represent * an item. @@ -169,26 +256,14 @@ class UsersFragment : ScreenFragment("Users"), Logging { } } holder.chipNode.setOnClickListener { - if (position > 0 && user != null) { - debug("calling MessagesFragment filter:${user.id}") - setFragmentResult( - "requestKey", - bundleOf("contactKey" to "0${user.id}", "contactName" to name) - ) - parentFragmentManager.beginTransaction() - .replace(R.id.mainActivityLayout, MessagesFragment()) - .addToBackStack(null) - .commit() - } + popup(it, position) } holder.itemView.setOnLongClickListener { - // do something else + popup(it, position) true } } - private var nodes = arrayOf() - /// Called when our node DB changes fun onNodesChanged(nodesIn: Array) { if (nodesIn.size > 1) diff --git a/app/src/main/res/drawable/ic_twotone_warning_24.xml b/app/src/main/res/drawable/ic_twotone_warning_24.xml new file mode 100644 index 00000000..e75aa8b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_twotone_warning_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/layout/advanced_settings.xml b/app/src/main/res/layout/advanced_settings.xml index 4d2ce438..b8ffe2be 100644 --- a/app/src/main/res/layout/advanced_settings.xml +++ b/app/src/main/res/layout/advanced_settings.xml @@ -66,38 +66,4 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/lsSleepView" /> - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_nodes.xml b/app/src/main/res/menu/menu_nodes.xml new file mode 100644 index 00000000..ee6b0d9e --- /dev/null +++ b/app/src/main/res/menu/menu_nodes.xml @@ -0,0 +1,26 @@ + + + + + + + + + + \ 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 de6e03f4..b5af03e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -151,8 +151,10 @@ Instantly send Empty channel names use the default encryption key (any device on %s can read your messages). Factory reset - Are you sure you want to factory reset? This will clear all device configuration you have done. Bluetooth disabled. Meshtastic needs Nearby devices permission to find and connect to devices via Bluetooth. You can turn it off when not in use. + Direct Message + NodeDB reset + This will clear all nodes from this list.