aprsdroid/src/AprsService.scala

408 wiersze
12 KiB
Scala

package org.aprsdroid.app
import _root_.android.app.Service
import _root_.android.content.{Context, Intent, IntentFilter}
import _root_.android.location._
import _root_.android.os.{Bundle, IBinder, Handler}
import _root_.android.preference.PreferenceManager
import _root_.android.util.Log
import _root_.android.widget.Toast
import _root_.net.ab0oo.aprs.parser._
object AprsService {
val PACKAGE = "org.aprsdroid.app"
// intent actions
val SERVICE = PACKAGE + ".SERVICE"
val SERVICE_ONCE = PACKAGE + ".ONCE"
// broadcast actions
val UPDATE = PACKAGE + ".UPDATE" // something added to the log
val MESSAGE = PACKAGE + ".MESSAGE" // we received a message/ack
val MESSAGETX = PACKAGE + ".MESSAGETX" // we created a message for TX
// broadcast intent extras
val LOCATION = PACKAGE + ".LOCATION"
val STATUS = PACKAGE + ".STATUS"
val PACKET = PACKAGE + ".PACKET"
val FAST_LANE_ACT = 30000
def intent(ctx : Context, action : String) : Intent = {
new Intent(action, null, ctx, classOf[AprsService])
}
var running = false
implicit def block2runnable(block: => Unit) =
new Runnable() {
def run() { block }
}
}
class AprsService extends Service with LocationListener {
import AprsService._
val TAG = "APRSdroid.Service"
lazy val prefs = new PrefsWrapper(this)
lazy val locMan = getSystemService(Context.LOCATION_SERVICE).asInstanceOf[LocationManager]
val handler = new Handler()
lazy val db = StorageDatabase.open(this)
lazy val msgService = new MessageService(this)
lazy val msgNotifier = msgService.createMessageNotifier()
var poster : AprsIsUploader = null
var singleShot = false
var lastLoc : Location = null
var fastLaneLoc : Location = null
override def onStart(i : Intent, startId : Int) {
Log.d(TAG, "onStart: " + i + ", " + startId);
super.onStart(i, startId)
handleStart(i)
}
override def onStartCommand(i : Intent, flags : Int, startId : Int) : Int = {
Log.d(TAG, "onStartCommand: " + i + ", " + flags + ", " + startId);
handleStart(i)
Service.START_REDELIVER_INTENT
}
def requestLocations(stay_on : Boolean) {
// get update interval and distance
val upd_int = prefs.getStringInt("interval", 10)
val upd_dist = prefs.getStringInt("distance", 10)
val gps_act = prefs.getString("gps_activation", "med")
if (stay_on || (gps_act == "always")) {
locMan.requestLocationUpdates(LocationManager.GPS_PROVIDER,
0, 0, this)
} else {
// for GPS precision == medium, we use getGpsInterval()
locMan.requestLocationUpdates(LocationManager.GPS_PROVIDER,
upd_int * 60000 - getGpsInterval(), upd_dist * 1000, this)
}
if (prefs.getBoolean("netloc", false)) {
locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
upd_int * 60000, upd_dist * 1000, this)
}
}
def handleStart(i : Intent) {
// get update interval and distance
val upd_int = prefs.getStringInt("interval", 10)
val upd_dist = prefs.getStringInt("distance", 10)
// display notification (even though we are not actually started yet,
// but we need this to prevent error message reordering)
fastLaneLoc = null
if (i.getAction() == SERVICE_ONCE) {
// if already running, we want to send immediately and continue;
// otherwise, we finish after a single position report
lastLoc = null
// set to true if not yet running or already running singleShot
singleShot = !running || singleShot
if (singleShot)
showToast(getString(R.string.service_once))
} else
showToast(getString(R.string.service_start).format(upd_int, upd_dist))
// the poster needs to be running before location updates come in
if (!running) {
running = true
startPoster()
// register for outgoing message notifications
registerReceiver(msgNotifier, new IntentFilter(AprsService.MESSAGETX))
}
// continuous GPS tracking for single shot mode
requestLocations(singleShot)
val callssid = prefs.getCallSsid()
val message = "%s: %d min, %d km".format(callssid, upd_int, upd_dist)
ServiceNotifier.instance.start(this, message)
}
def startPoster() {
if (poster != null)
poster.stop()
poster = AprsIsUploader.instanciateUploader(this, prefs)
poster.start()
}
override def onBind(i : Intent) : IBinder = null
override def onUnbind(i : Intent) : Boolean = false
def showToast(msg : String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
addPost(StorageDatabase.Post.TYPE_INFO, null, msg)
}
override def onDestroy() {
running = false
// catch FC when service is killed from outside
if (poster != null) {
poster.stop()
showToast(getString(R.string.service_stop))
}
msgService.stop()
locMan.removeUpdates(this);
unregisterReceiver(msgNotifier)
ServiceNotifier.instance.stop(this)
}
def getGpsInterval() : Int = {
val gps_act = prefs.getString("gps_activation", "med")
if (gps_act == "med") FAST_LANE_ACT
else 0
}
def startFastLane() {
Log.d(TAG, "switching to fast lane");
// request fast update rate
locMan.removeUpdates(this);
requestLocations(true)
handler.postDelayed({ stopFastLane(true) }, FAST_LANE_ACT)
}
def stopFastLane(post : Boolean) {
if (!running)
return;
Log.d(TAG, "switching to slow lane");
if (post && fastLaneLoc != null) {
Log.d(TAG, "stopFastLane: posting " + fastLaneLoc);
postLocation(fastLaneLoc)
}
fastLaneLoc = null
// reset update speed
locMan.removeUpdates(this);
requestLocations(false)
}
def goingFastLane(location : Location) : Boolean = {
if (fastLaneLoc == null) {
// need to set fastLaneLoc before re-requesting locations
fastLaneLoc = location
startFastLane()
} else
fastLaneLoc = location
return true
}
def smartBeaconSpeedRate(speed : Float) : Int = {
val SB_FAST_SPEED = 28 // [m/s] = ~100km/h
val SB_FAST_RATE = 60
val SB_SLOW_SPEED = 1 // [m/s] = 3.6km/h
val SB_SLOW_RATE = 1200
if (speed <= SB_SLOW_SPEED)
SB_SLOW_RATE
else if (speed >= SB_FAST_SPEED)
SB_FAST_RATE
else
((SB_SLOW_RATE - SB_FAST_RATE) * (SB_FAST_SPEED - speed) / (SB_FAST_SPEED-SB_SLOW_SPEED)).toInt
}
// returns the angle between two bearings
def getBearingAngle(alpha : Float, beta : Float) : Float = {
val delta = math.abs(alpha-beta)%360
if (delta <= 180) delta else (360-delta)
}
// obtain max speed in [m/s] from moved distance, last and current location
def getSpeed(location : Location) : Float = {
val dist = location.distanceTo(lastLoc)
val t_diff = location.getTime - lastLoc.getTime
math.max(math.max(dist*1000/t_diff, location.getSpeed), lastLoc.getSpeed)
}
def smartBeaconCornerPeg(location : Location) : Boolean = {
val SB_TURN_TIME = 15
val SB_TURN_MIN = 10
val SB_TURN_SLOPE = 240.0
val speed = getSpeed(location)
val t_diff = location.getTime - lastLoc.getTime
val turn = getBearingAngle(location.getBearing, lastLoc.getBearing)
// no bearing / stillstand -> no corner pegging
if (!location.hasBearing || speed == 0)
return false
// if last bearing unknown, deploy turn_time
if (!lastLoc.hasBearing)
return (t_diff/1000 >= SB_TURN_TIME)
// threshold depends on slope/speed [mph]
val threshold = SB_TURN_MIN + SB_TURN_SLOPE/(speed*2.23693629)
Log.d(TAG, "smartBeaconCornerPeg: %1.0f < %1.0f %d/%d".format(turn, threshold,
t_diff/1000, SB_TURN_TIME))
// need to corner peg if turn time reached and turn > threshold
(t_diff/1000 >= SB_TURN_TIME && turn > threshold)
}
// return true if current position is "new enough" vs. lastLoc
def smartBeaconCheck(location : Location) : Boolean = {
if (lastLoc == null)
return true
if (smartBeaconCornerPeg(location))
return true
val dist = location.distanceTo(lastLoc)
val t_diff = location.getTime - lastLoc.getTime
val speed = getSpeed(location)
//if (location.hasSpeed && location.hasBearing)
val speed_rate = smartBeaconSpeedRate(speed)
Log.d(TAG, "smartBeaconCheck: %1.0fm, %1.2fm/s -> %d/%ds - %s".format(dist, speed,
t_diff/1000, speed_rate, (t_diff/1000 >= speed_rate).toString))
if (t_diff/1000 >= speed_rate)
true
else
false
}
// LocationListener interface
override def onLocationChanged(location : Location) {
val upd_int = prefs.getStringInt("interval", 10) * 60000
val upd_dist = prefs.getStringInt("distance", 10) * 1000
if (prefs.getBoolean("smartbeaconing", true)) {
if (!smartBeaconCheck(location))
return
} else /* no smartbeaconing */
if (lastLoc != null &&
(location.getTime - lastLoc.getTime < (upd_int - getGpsInterval()) ||
location.distanceTo(lastLoc) < upd_dist)) {
//Log.d(TAG, "onLocationChanged: ignoring premature location")
return
}
// check if we need to go fast lane
val gps_act = prefs.getString("gps_activation", "med")
if (gps_act == "med" && location.getProvider == LocationManager.GPS_PROVIDER) {
if (goingFastLane(location))
return
}
postLocation(location)
}
def appVersion() : String = {
val pi = getPackageManager().getPackageInfo(getPackageName(), 0)
"APDR%s".format(pi.versionName filter (_.isDigit) take 2)
}
def postLocation(location : Location) {
lastLoc = location
val i = new Intent(UPDATE)
i.putExtra(LOCATION, location)
val callssid = prefs.getCallSsid()
var symbol = prefs.getString("symbol", "")
if (symbol.length != 2)
symbol = getString(R.string.default_symbol)
val status = prefs.getString("status", getString(R.string.default_status))
val packet = AprsPacket.formatLoc(callssid, appVersion(), symbol, status, location)
Log.d(TAG, "packet: " + packet)
val result = try {
val status = poster.update(packet)
i.putExtra(STATUS, status)
i.putExtra(PACKET, packet.toString)
val prec_status = "%s (±%dm)".format(status, location.getAccuracy.asInstanceOf[Int])
addPost(StorageDatabase.Post.TYPE_POST, prec_status, packet.toString)
prec_status
} catch {
case e : Exception =>
i.putExtra(PACKET, e.getMessage())
addPost(StorageDatabase.Post.TYPE_ERROR, "Error", e.getMessage())
e.printStackTrace()
e.getMessage()
}
if (singleShot) {
singleShot = false
stopSelf()
} else {
val message = "%s: %s".format(callssid, result)
ServiceNotifier.instance.start(this, message)
}
}
override def onProviderDisabled(provider : String) {
Log.d(TAG, "onProviderDisabled: " + provider)
val netloc_available = locMan.getProviders(true).contains(LocationManager.NETWORK_PROVIDER)
val netloc_usable = netloc_available && prefs.getBoolean("netloc", false)
if (provider == LocationManager.GPS_PROVIDER &&
netloc_usable == false) {
// GPS was our last data source, we have to complain!
Toast.makeText(this, R.string.service_no_location, Toast.LENGTH_LONG).show()
}
}
override def onProviderEnabled(provider : String) {
Log.d(TAG, "onProviderEnabled: " + provider)
}
override def onStatusChanged(provider : String, st: Int, extras : Bundle) {
Log.d(TAG, "onStatusChanged: " + provider)
}
def parsePacket(ts : Long, message : String) {
try {
val fap = new Parser().parse(message)
if (fap.getAprsInformation() == null) {
Log.d(TAG, "parsePacket() misses payload: " + message)
return
}
if (fap.hasFault())
throw new Exception("FAP fault")
fap.getAprsInformation() match {
case pp : PositionPacket => db.addPosition(ts, fap, pp.getPosition(), null)
case op : ObjectPacket => db.addPosition(ts, fap, op.getPosition(), op.getObjectName())
case msg : MessagePacket => msgService.handleMessage(ts, fap, msg)
}
} catch {
case e : Exception =>
Log.d(TAG, "parsePacket() unsupported packet: " + message)
e.printStackTrace()
}
}
def addPost(t : Int, status : String, message : String) {
val ts = System.currentTimeMillis()
db.addPost(ts, t, status, message)
if (t == StorageDatabase.Post.TYPE_POST || t == StorageDatabase.Post.TYPE_INCMG) {
parsePacket(ts, message)
} else {
// only log status messages
Log.d(TAG, "addPost: " + status + " - " + message)
}
sendBroadcast(new Intent(UPDATE).putExtra(STATUS, message))
}
// support for translated IDs
def addPost(t : Int, status_id : Int, message : String) {
addPost(t, getString(status_id), message)
}
def postAddPost(t : Int, status_id : Int, message : String) {
// only log "info" if enabled in prefs
if (t == StorageDatabase.Post.TYPE_INFO && prefs.getBoolean("conn_log", false) == false)
return
handler.post {
addPost(t, status_id, message)
if (t == StorageDatabase.Post.TYPE_INCMG)
msgService.sendPendingMessages()
else if (t == StorageDatabase.Post.TYPE_ERROR)
stopSelf()
}
}
def postSubmit(post : String) {
postAddPost(StorageDatabase.Post.TYPE_INCMG, R.string.post_incmg, post)
}
def postAbort(post : String) {
postAddPost(StorageDatabase.Post.TYPE_ERROR, R.string.post_error, post)
}
}