Skip to content
8 changes: 7 additions & 1 deletion AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,13 @@
android:label="@string/p__location"
android:parentActivityName=".PrivacyPrefs"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|screenSize" />
android:configChanges="orientation|keyboardHidden|screenSize" />

<activity android:name=".DeviceIdPrefs"
android:label="@string/p__device_id"
android:parentActivityName=".PrefsAct"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|screenSize" />

<activity android:name=".GoogleMapAct" android:label="@string/app_map"
android:launchMode="singleTop"
Expand Down
16 changes: 14 additions & 2 deletions res/layout/stationview.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,19 @@
android:typeface="monospace"
android:visibility="gone"/> <!-- Set visibility based on condition -->

<!-- List message below Speed and Course (should always be visible) -->
<!-- Device name (from tocalls.yaml lookup), hidden if unknown -->
<TextView
android:id="@+id/station_device"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/station_course"
android:layout_marginTop="0sp"
android:textColor="#b0b080"
android:textSize="14sp"
android:typeface="monospace"
android:visibility="gone"/>

<!-- List message below device line -->
<TextView
android:id="@+id/listmessage"
android:layout_width="fill_parent"
Expand All @@ -81,7 +93,7 @@
android:textSize="17sp"
android:typeface="monospace"
android:focusable="false"
android:layout_below="@id/station_course"
android:layout_below="@id/station_device"
android:layout_marginTop="5sp"
android:visibility="gone" />

Expand Down
16 changes: 16 additions & 0 deletions res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -607,4 +607,20 @@
<string name="p_ptt_tail_summary">Extends PTT to ensure full packet TX</string>
<string name="p_ptt_tail_entry">Enter the tail time [ms]</string>


<!-- Device Identification -->
<string name="p__device_id">Device Identification</string>
<string name="p_device_id_summary">Manage APRS device database</string>
<string name="p_device_id_auto_update">Automatically update on startup</string>
<string name="p_device_id_url">Database URL</string>
<string name="p_device_id_url_summary">URL to fetch tocalls.yaml from</string>
<string name="p_device_id_reset_url">Reset to defaults</string>
<string name="p_device_id_reset_url_summary">Restore the database URL to the official source</string>
<string name="p_device_id_update_now">Update now</string>
<string name="p_device_id_update_now_summary">Download the latest device database</string>
<string name="device_id_default_url">https://raw.githubusercontent.com/aprsorg/aprs-deviceid/main/tocalls.yaml</string>
<string name="device_id_update_success">Device database updated</string>
<string name="device_id_update_failed">Update failed: %s</string>
<string name="device_id_url_reset">URL reset to default</string>

</resources>
27 changes: 27 additions & 0 deletions res/xml/device_id_prefs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

<CheckBoxPreference
android:key="device_id_auto_update"
android:title="@string/p_device_id_auto_update"
android:defaultValue="true" />

<EditTextPreference
android:key="device_id_url"
android:title="@string/p_device_id_url"
android:summary="@string/p_device_id_url_summary"
android:defaultValue="@string/device_id_default_url"
android:inputType="textUri"
android:singleLine="true" />

<Preference
android:key="device_id_reset_url"
android:title="@string/p_device_id_reset_url"
android:summary="@string/p_device_id_reset_url_summary" />

<Preference
android:key="device_id_update_now"
android:title="@string/p_device_id_update_now"
android:summary="@string/p_device_id_update_now_summary" />

</PreferenceScreen>
17 changes: 16 additions & 1 deletion res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,22 @@
</PreferenceScreen>
</PreferenceCategory>

<PreferenceCategory
<PreferenceCategory
android:title="@string/p__device_id">

<PreferenceScreen
android:key="p_device_id"
android:title="@string/p__device_id"
android:summary="@string/p_device_id_summary">

<intent android:action="android.intent.action.MAIN"
android:targetPackage="org.aprsdroid.app"
android:targetClass="org.aprsdroid.app.DeviceIdPrefs" />

</PreferenceScreen>
</PreferenceCategory>

<PreferenceCategory
android:title="@string/freq_control">

<CheckBoxPreference
Expand Down
5 changes: 4 additions & 1 deletion src/APRSdroid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ class APRSdroid extends Activity {
if (UsbTnc.checkDeviceHandle(prefs, getIntent.getParcelableExtra("device")) && prefs.getBoolean("service_running", false))
startService(AprsService.intent(this, AprsService.SERVICE))

val mapmode = MapModes.defaultMapMode(this, new PrefsWrapper(this))
val prefsWrapper = new PrefsWrapper(this)
DeviceDbUpdater.updateIfAllowed(this, prefsWrapper)

val mapmode = MapModes.defaultMapMode(this, prefsWrapper)
prefs.getString("activity", "log") match {
case "hub" => replaceAct(classOf[HubActivity])
case "map" => replaceAct(mapmode.viewClass)
Expand Down
40 changes: 24 additions & 16 deletions src/AprsPacket.scala
Original file line number Diff line number Diff line change
Expand Up @@ -339,32 +339,40 @@ object AprsPacket {
}
}

def micEDeviceInfo(comment: String): Option[Map[String, String]] = {
if (comment == null || comment.length < 2) None
else COMMENT_DATA.get(comment.takeRight(2))
}

def kenwoodDeviceInfo(comment: String): Option[Map[String, String]] = {
if (comment == null || comment.isEmpty) None
else KENWOOD_COMMENT_DATA.get(comment.takeRight(1))
}

def packetKenwoodDeviceInfo(packet: String): Option[Map[String, String]] = {
val colonIndex = packet.indexOf(':')
if (colonIndex == -1 || colonIndex + 10 >= packet.length) {
None
} else if (packet(colonIndex + 1) == '\'') {
KENWOOD_COMMENT_DATA.get(packet(colonIndex + 10).toString)
} else {
None
}
}

// Function to check if the last 2 characters of the comment match anything in COMMENT_DATA
def micetocall(comment: String): Option[String] = {
val lastTwoChars = comment.takeRight(2) // Get the last 2 characters of the comment
COMMENT_DATA.get(lastTwoChars).flatMap(_.get("model"))
micEDeviceInfo(comment).flatMap(_.get("model"))
}

// Function to check if the last character of the comment matches anything in KENWOOD_COMMENT_DATA
def kenwoodtocall(comment: String): Option[String] = {
val lastChar = comment.takeRight(1) // Get the last character of the comment
KENWOOD_COMMENT_DATA.get(lastChar).flatMap(_.get("model"))
kenwoodDeviceInfo(comment).flatMap(_.get("model"))
}

// Function to check old Kenwood calls
def oldkenwoodtocall(packet: String): Option[String] = {
val colonIndex = packet.indexOf(':')

if (colonIndex == -1 || colonIndex + 10 >= packet.length) {
None
} else {
if (packet(colonIndex + 1) == '\'') {
val keyChar = packet(colonIndex + 10).toString
KENWOOD_COMMENT_DATA.get(keyChar).flatMap(_.get("model"))
} else {
None
}
}
packetKenwoodDeviceInfo(packet).flatMap(_.get("model"))
}

def parseComment(comment: String): String = {
Expand Down
91 changes: 91 additions & 0 deletions src/DeviceDbUpdater.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.aprsdroid.app

import _root_.android.content.Context
import _root_.android.util.Log
import _root_.android.widget.Toast

import java.io.{BufferedInputStream, FileOutputStream}
import java.net.URL

// Downloads tocalls.yaml from a configurable URL and saves it to internal storage.
// All network I/O runs on a background thread.
object DeviceDbUpdater {
val TAG = "APRSdroid.DeviceDbUpdater"

val DEFAULT_TOCALLS_URL =
"https://raw.githubusercontent.com/aprsorg/aprs-deviceid/main/tocalls.yaml"

// Returns the URL from preferences, falling back to the default.
def getUrl(prefs: PrefsWrapper): String = {
val url = prefs.getString("device_id_url", "").trim
if (url.isEmpty) DEFAULT_TOCALLS_URL else url
}

// Downloads tocalls.yaml if the preferences allow it right now.
def updateIfAllowed(context: Context, prefs: PrefsWrapper): Unit = {
val autoUpdate = prefs.getBoolean("device_id_auto_update", true)
if (!autoUpdate) return
update(context, prefs, silent = true)
}

// Downloads tocalls.yaml unconditionally and saves it to internal storage.
// Pass silent=false to show a Toast when done (for manual "Update now" taps).
def update(context: Context, prefs: PrefsWrapper, silent: Boolean = false): Unit = {
val appContext = context.getApplicationContext
val urlString = getUrl(prefs)

new Thread(new Runnable {
override def run(): Unit = {
try {
Log.i(TAG, "Downloading tocalls.yaml from " + urlString)
val url = new URL(urlString)
val connection = url.openConnection()
connection.setConnectTimeout(15000)
connection.setReadTimeout(30000)
connection.connect()

val outFile = DeviceIdentifier.tocallsFile(appContext)
val tmpFile = new java.io.File(outFile.getParent, "tocalls.yaml.tmp")

val input = new BufferedInputStream(connection.getInputStream)
val output = new FileOutputStream(tmpFile)
try {
val buf = new Array[Byte](8192)
var len = input.read(buf)
while (len != -1) {
output.write(buf, 0, len)
len = input.read(buf)
}
} finally {
output.close()
input.close()
}

// Atomic replace: rename tmp → final
tmpFile.renameTo(outFile)
DeviceIdentifier.invalidate()
Log.i(TAG, "tocalls.yaml updated successfully")

if (!silent) {
new android.os.Handler(appContext.getMainLooper).post(new Runnable {
override def run(): Unit =
Toast.makeText(appContext, R.string.device_id_update_success,
Toast.LENGTH_SHORT).show()
})
}
} catch {
case e: Exception =>
Log.e(TAG, "Failed to download tocalls.yaml", e)
if (!silent) {
new android.os.Handler(appContext.getMainLooper).post(new Runnable {
override def run(): Unit =
Toast.makeText(appContext,
appContext.getString(R.string.device_id_update_failed, e.getMessage),
Toast.LENGTH_LONG).show()
})
}
}
}
}).start()
}
}
61 changes: 61 additions & 0 deletions src/DeviceIdPrefs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.aprsdroid.app

import android.content.SharedPreferences
import android.os.Bundle
import android.preference.{EditTextPreference, PreferenceActivity}
import android.widget.Toast

class DeviceIdPrefs extends PreferenceActivity with SharedPreferences.OnSharedPreferenceChangeListener {

lazy val prefs = new PrefsWrapper(this)

override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.device_id_prefs)
getPreferenceScreen().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this)
updateUrlSummary()

// "Update now" — download tocalls.yaml immediately
findPreference("device_id_update_now").setOnPreferenceClickListener(
new android.preference.Preference.OnPreferenceClickListener {
override def onPreferenceClick(pref: android.preference.Preference): Boolean = {
DeviceDbUpdater.update(DeviceIdPrefs.this, prefs, silent = false)
true
}
}
)

// "Reset to defaults" — restore the URL to the official source
findPreference("device_id_reset_url").setOnPreferenceClickListener(
new android.preference.Preference.OnPreferenceClickListener {
override def onPreferenceClick(pref: android.preference.Preference): Boolean = {
val editor = getPreferenceScreen().getSharedPreferences().edit()
editor.putString("device_id_url", DeviceDbUpdater.DEFAULT_TOCALLS_URL)
editor.apply()
updateUrlSummary()
Toast.makeText(DeviceIdPrefs.this, R.string.device_id_url_reset,
Toast.LENGTH_SHORT).show()
true
}
}
)
}

override def onDestroy() {
super.onDestroy()
getPreferenceScreen().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this)
}

override def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String): Unit = {
if (key == "device_id_url") updateUrlSummary()
}

// Show the current URL as the summary so the user can see what's set.
private def updateUrlSummary(): Unit = {
val urlPref = findPreference("device_id_url").asInstanceOf[EditTextPreference]
val current = prefs.getString("device_id_url", DeviceDbUpdater.DEFAULT_TOCALLS_URL)
urlPref.setSummary(if (current.isEmpty) DeviceDbUpdater.DEFAULT_TOCALLS_URL else current)
}
}
Loading