Convert sample project to Kotlin (#3111)

pull/3112/head
Phil Oliver 2025-09-15 15:50:36 -04:00 zatwierdzone przez GitHub
rodzic be6e9ad5ec
commit 58344c1c0f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
11 zmienionych plików z 300 dodań i 264 usunięć

Wyświetl plik

@ -1,26 +0,0 @@
package com.meshtastic.android.meshserviceexample;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.meshtastic.android.meshserviceexample", appContext.getPackageName());
}
}

Wyświetl plik

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.meshtastic.android.meshserviceexample
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Assert.assertEquals("com.meshtastic.android.meshserviceexample", appContext.packageName)
}
}

Wyświetl plik

@ -1,221 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.meshtastic.android.meshserviceexample;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.geeksville.mesh.DataPacket;
import com.geeksville.mesh.IMeshService;
import com.geeksville.mesh.MessageStatus;
import com.geeksville.mesh.NodeInfo;
import com.geeksville.mesh.Portnums;
import java.util.Objects;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MeshServiceExample";
private IMeshService meshService;
private ServiceConnection serviceConnection;
private boolean isMeshServiceBound = false;
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
TextView mainTextView = findViewById(R.id.mainTextView);
ImageView statusImageView = findViewById(R.id.statusImageView);
Button sendButton = findViewById(R.id.sendBtn);
sendButton.setOnClickListener(v -> {
if (meshService != null) {
try {
byte[] bytes = "Hello from MeshServiceExample".getBytes();
DataPacket dataPacket = new DataPacket(DataPacket.ID_BROADCAST, bytes, Portnums.PortNum.TEXT_MESSAGE_APP_VALUE, DataPacket.ID_LOCAL, System.currentTimeMillis(), 0, MessageStatus.UNKNOWN, 3, 0, true);
meshService.send(dataPacket);
Log.d(TAG, "Message sent successfully");
} catch (Exception e) {
Log.e(TAG, "Failed to send message", e);
}
} else {
Log.w(TAG, "MeshService is not bound, cannot send message");
}
});
// Now you can call methods on meshService
serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
meshService = IMeshService.Stub.asInterface(service);
Log.i(TAG, "Connected to MeshService");
isMeshServiceBound = true;
statusImageView.setImageResource(android.R.color.holo_green_light);
}
@Override
public void onServiceDisconnected(ComponentName name) {
meshService = null;
isMeshServiceBound = false;
}
};
// Handle the received broadcast
// handle node changed
// handle position app data
BroadcastReceiver meshtasticReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null || intent.getAction() == null) {
Log.w(TAG, "Received null intent or action");
return;
}
// Handle the received broadcast
String action = intent.getAction();
Log.d(TAG, "Received broadcast: " + action);
switch (Objects.requireNonNull(action)) {
case "com.geeksville.mesh.NODE_CHANGE":
// handle node changed
try {
NodeInfo ni = intent.getParcelableExtra("com.geeksville.mesh.NodeInfo");
Log.d(TAG, "NodeInfo: " + ni);
mainTextView.setText("NodeInfo: " + ni);
} catch (Exception e) {
Log.e(TAG, "onReceive: " + e.getMessage());
return;
}
break;
case "com.geeksville.mesh.MESSAGE_STATUS":
int id = intent.getIntExtra("com.geeksville.mesh.PacketId", 0);
MessageStatus status = intent.getParcelableExtra("com.geeksville.mesh.Status");
Log.d(TAG, "Message Status ID: " + id + " Status: " + status);
break;
case "com.geeksville.mesh.MESH_CONNECTED": {
String extraConnected = intent.getStringExtra("com.geeksville.mesh.Connected");
boolean connected = extraConnected.equalsIgnoreCase("connected");
Log.d(TAG, "Received ACTION_MESH_CONNECTED: " + extraConnected);
if (connected) {
statusImageView.setImageResource(android.R.color.holo_green_light);
}
break;
}
case "com.geeksville.mesh.MESH_DISCONNECTED": {
String extraConnected = intent.getStringExtra("com.geeksville.mesh.Disconnected");
boolean disconnected = extraConnected.equalsIgnoreCase("disconnected");
Log.d(TAG, "Received ACTION_MESH_DISTCONNECTED: " + extraConnected);
if (disconnected) {
statusImageView.setImageResource(android.R.color.holo_red_light);
}
break;
}
case "com.geeksville.mesh.RECEIVED.POSITION_APP": {
// handle position app data
try {
NodeInfo ni = intent.getParcelableExtra("com.geeksville.mesh.NodeInfo");
Log.d(TAG, "Position App NodeInfo: " + ni);
mainTextView.setText("Position App NodeInfo: " + ni);
} catch (Exception e) {
e.printStackTrace();
return;
}
break;
}
default:
Log.w(TAG, "Unknown action: " + action);
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction("com.geeksville.mesh.NODE_CHANGE");
filter.addAction("com.geeksville.mesh.RECEIVED.NODEINFO_APP");
filter.addAction("com.geeksville.mesh.RECEIVED.POSITION_APP");
filter.addAction("com.geeksville.mesh.MESH_CONNECTED");
filter.addAction("com.geeksville.mesh.MESH_DISCONNECTED");
registerReceiver(meshtasticReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
Log.d(TAG, "Registered meshtasticPacketReceiver");
while (!bindMeshService()) {
try {
// Wait for the service to bind
Thread.sleep(1000);
} catch (InterruptedException e) {
Log.e(TAG, "Binding interrupted", e);
break;
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindMeshService();
}
private boolean bindMeshService() {
try {
Log.i(TAG, "Attempting to bind to Mesh Service...");
Intent intent = new Intent("com.geeksville.mesh.Service");
intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService");
return bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
} catch (Exception e) {
Log.e(TAG, "Failed to bind", e);
}
return false;
}
private void unbindMeshService() {
if (isMeshServiceBound) {
try {
unbindService(serviceConnection);
} catch (IllegalArgumentException e) {
Log.w(TAG, "MeshService not registered or already unbound: " + e.getMessage());
}
isMeshServiceBound = false;
meshService = null;
}
}
}

Wyświetl plik

@ -0,0 +1,228 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.meshtastic.android.meshserviceexample
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.ServiceConnection
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.MessageStatus
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Portnums
private const val TAG: String = "MeshServiceExample"
class MainActivity : AppCompatActivity() {
private var meshService: IMeshService? = null
private var serviceConnection: ServiceConnection? = null
private var isMeshServiceBound = false
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
val mainTextView = findViewById<TextView>(R.id.mainTextView)
val statusImageView = findViewById<ImageView>(R.id.statusImageView)
findViewById<Button>(R.id.sendBtn).setOnClickListener { _ ->
meshService?.let {
try {
it.send(
DataPacket(
to = DataPacket.ID_BROADCAST,
bytes = "Hello from MeshServiceExample".toByteArray(),
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
from = DataPacket.ID_LOCAL,
time = System.currentTimeMillis(),
id = 0,
status = MessageStatus.UNKNOWN,
hopLimit = 3,
channel = 0,
wantAck = true,
),
)
Log.d(TAG, "Message sent successfully")
} catch (e: Exception) {
Log.e(TAG, "Failed to send message", e)
}
} ?: Log.w(TAG, "MeshService is not bound, cannot send message")
}
// Now you can call methods on meshService
serviceConnection =
object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
meshService = IMeshService.Stub.asInterface(service)
Log.i(TAG, "Connected to MeshService")
isMeshServiceBound = true
statusImageView.setImageResource(android.R.color.holo_green_light)
}
override fun onServiceDisconnected(name: ComponentName?) {
meshService = null
isMeshServiceBound = false
}
}
// Handle the received broadcast
// handle node changed
// handle position app data
val meshtasticReceiver: BroadcastReceiver =
object : BroadcastReceiver() {
@SuppressLint("SetTextI18n")
override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null) {
Log.w(TAG, "Received null intent")
return
}
val action = intent.action
if (intent.action == null) {
Log.w(TAG, "Received null action")
return
}
Log.d(TAG, "Received broadcast: $action")
when (action) {
"com.geeksville.mesh.NODE_CHANGE" ->
try {
val ni = intent.getParcelableExtra("com.geeksville.mesh.NodeInfo", NodeInfo::class.java)
Log.d(TAG, "NodeInfo: $ni")
mainTextView.text = "NodeInfo: $ni"
} catch (e: Exception) {
Log.e(TAG, "onReceive: ${e.message}")
return
}
"com.geeksville.mesh.MESSAGE_STATUS" -> {
val id = intent.getIntExtra("com.geeksville.mesh.PacketId", 0)
val status =
intent.getParcelableExtra("com.geeksville.mesh.Status", MessageStatus::class.java)
Log.d(TAG, "Message Status ID: $id Status: $status")
}
"com.geeksville.mesh.MESH_CONNECTED" -> {
val extraConnected = intent.getStringExtra("com.geeksville.mesh.Connected")
val connected = extraConnected.equals("connected", ignoreCase = true)
Log.d(TAG, "Received ACTION_MESH_CONNECTED: $extraConnected")
if (connected) {
statusImageView.setImageResource(android.R.color.holo_green_light)
}
}
"com.geeksville.mesh.MESH_DISCONNECTED" -> {
val extraConnected = intent.getStringExtra("com.geeksville.mesh.Disconnected")
val disconnected = extraConnected.equals("disconnected", ignoreCase = true)
Log.d(TAG, "Received ACTION_MESH_DISCONNECTED: $extraConnected")
if (disconnected) {
statusImageView.setImageResource(android.R.color.holo_red_light)
}
}
"com.geeksville.mesh.RECEIVED.POSITION_APP" -> {
try {
val ni = intent.getParcelableExtra("com.geeksville.mesh.NodeInfo", NodeInfo::class.java)
Log.d(TAG, "Position App NodeInfo: $ni")
mainTextView.text = "Position App NodeInfo: $ni"
} catch (e: Exception) {
e.printStackTrace()
return
}
}
else -> Log.w(TAG, "Unknown action: $action")
}
}
}
val filter =
IntentFilter().apply {
addAction("com.geeksville.mesh.NODE_CHANGE")
addAction("com.geeksville.mesh.RECEIVED.NODEINFO_APP")
addAction("com.geeksville.mesh.RECEIVED.POSITION_APP")
addAction("com.geeksville.mesh.MESH_CONNECTED")
addAction("com.geeksville.mesh.MESH_DISCONNECTED")
}
registerReceiver(meshtasticReceiver, filter, RECEIVER_NOT_EXPORTED)
Log.d(TAG, "Registered meshtasticPacketReceiver")
while (!bindMeshService()) {
try {
Thread.sleep(1000)
} catch (e: InterruptedException) {
Log.e(TAG, "Binding interrupted", e)
break
}
}
}
override fun onDestroy() {
super.onDestroy()
unbindMeshService()
}
private fun bindMeshService(): Boolean {
try {
Log.i(TAG, "Attempting to bind to Mesh Service...")
val intent = Intent("com.geeksville.mesh.Service")
intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.service.MeshService")
return bindService(intent, serviceConnection!!, BIND_AUTO_CREATE)
} catch (e: java.lang.Exception) {
Log.e(TAG, "Failed to bind", e)
}
return false
}
private fun unbindMeshService() {
if (isMeshServiceBound) {
try {
unbindService(serviceConnection!!)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "MeshService not registered or already unbound: " + e.message)
}
isMeshServiceBound = false
meshService = null
}
}
}

Wyświetl plik

@ -1,17 +0,0 @@
package com.meshtastic.android.meshserviceexample;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

Wyświetl plik

@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.meshtastic.android.meshserviceexample
import org.junit.Assert
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
Assert.assertEquals(4, (2 + 2).toLong())
}
}