Merge branch 'messaging'

redir
Georg Lukas 2011-07-14 23:25:33 +02:00
commit 077cf8881b
21 zmienionych plików z 831 dodań i 68 usunięć

Wyświetl plik

@ -30,9 +30,17 @@
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden"
/>
<activity android:name=".ConversationsActivity" android:label="@string/app_messages"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden"
/>
<activity android:name=".StationActivity" android:label="@string/app_sta"
android:configChanges="orientation|keyboardHidden"
/>
<activity android:name=".MessageActivity" android:label="@string/app_sta"
android:configChanges="orientation|keyboardHidden"
android:alwaysRetainTaskState="true"
/>
<activity android:name=".PrefsAct" android:label="@string/app_prefs" />
<activity android:name=".BackendPrefs" android:label="@string/app_prefs" />
<activity android:name=".MapAct" android:label="@string/app_map"

Plik binarny nie jest wyświetlany.

Wyświetl plik

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
>
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="100sp"
android:layout_weight="1"
android:focusable="true"
android:focusableInTouchMode="true"
android:descendantFocusability="blocksDescendants"
/>
<TextView
android:id="@+id/android:empty"
android:layout_width="fill_parent"
android:layout_height="100sp"
android:layout_weight="2"
android:textSize="18sp"
android:text="@string/msg_empty_list"/>
<Button
android:id="@+id/new_conversation"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/msg_send_new"
/>
</LinearLayout>

Wyświetl plik

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/call"
android:layout_width="20dp"
android:layout_weight="0.4"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:textColor="#b0b080"
android:textSize="24dp"
android:ellipsize="marquee"
android:typeface="monospace"
/>
<LinearLayout
android:layout_width="30dp"
android:layout_weight="0.6"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/ts"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#b0b0b0"
android:textSize="16dp"
/>
<TextView
android:id="@+id/message"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#8080b0"
android:textSize="16dp"
android:typeface="monospace"
android:focusable="false"
android:ellipsize="marquee"
android:singleLine="true"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

Wyświetl plik

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/message_act"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
>
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:descendantFocusability="blocksDescendants"
android:stackFromBottom="true"
android:transcriptMode="alwaysScroll" />
/>
<LinearLayout
android:id="@+id/buttonlayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
xmlns:android="http://schemas.android.com/apk/res/android"
>
<EditText android:id="@+id/msginput"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:maxLength="67"
android:layout_weight="1"
android:inputType="textShortMessage"
/>
<Button android:id="@+id/msgsend"
android:layout_width="60sp"
android:layout_height="wrap_content"
android:text="@android:string/ok"
android:enabled="false"
/>
</LinearLayout>
</LinearLayout>

Wyświetl plik

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="fill_parent" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<EditText
android:id="@+id/callsign"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
android:layout_marginTop="5sp"
android:layout_marginBottom="5sp"
android:layout_marginRight="10sp"
android:singleLine="true"
android:inputType="textCapCharacters"
android:hint="@string/p_callsign"
/>
<EditText
android:id="@+id/message"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10sp"
android:layout_marginTop="5sp"
android:layout_marginBottom="5sp"
android:layout_marginRight="10sp"
android:singleLine="true"
android:maxLength="67"
android:hint="@string/msg_message_text"
/>
</LinearLayout>
</ScrollView>

Wyświetl plik

@ -1,5 +1,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/details" android:title="@string/app_sta" />
<item android:id="@+id/message" android:title="@string/app_messages" />
<item android:id="@+id/mapbutton" android:title="@string/sta_map" />
<item android:id="@+id/aprsfibutton" android:title="@string/sta_aprsfi" />
<item android:id="@+id/qrzcombutton" android:title="@string/sta_qrzcom" />

Wyświetl plik

@ -11,6 +11,10 @@
android:title="@string/show_log"
android:alphabeticShortcut="l"
android:icon="@android:drawable/ic_menu_recent_history" />
<item android:id="@+id/conversations"
android:title="@string/app_messages"
android:alphabeticShortcut="c"
android:icon="@android:drawable/ic_menu_send" />
<item android:id="@+id/age"
android:title="@string/age"
android:alphabeticShortcut="a"

Wyświetl plik

@ -6,6 +6,7 @@
<string name="app_map">APRSdroid Map</string>
<string name="app_hub">APRSdroid Hub</string>
<string name="app_sta">Station Info</string>
<string name="app_messages">Messages</string>
<!-- APRSdroid activity -->
<string name="firstrun">You need to configure APRSdroid with your callsign and passcode!</string>
<string name="wrongpasscode">Your passcode does not match your callsign!</string>
@ -32,6 +33,11 @@
<string name="sta_aprsfi">aprs.fi</string>
<string name="sta_qrzcom">QRZ.com</string>
<!-- Conversations activity -->
<string name="msg_send_new">Send message to...</string>
<string name="msg_message_text">Message text</string>
<string name="msg_empty_list">There are no stored conversations.</string>
<!-- AprsService -->
<string name="aprsservice">APRSdroid Service</string>
<string name="service_once">APRS Service single shot.</string>

Wyświetl plik

@ -59,7 +59,13 @@ object AprsPacket {
new Position(location.getLatitude, location.getLongitude, 0,
symbol(0), symbol(1)),
formatCourseSpeed(location) + formatAltitude(location) +
" " + status))
" " + status, /* messaging = */ true))
}
def formatMessage(callssid : String, toCall : String, dest : String,
message : String, msgid : String) = {
new APRSPacket(callssid, toCall, null, new MessagePacket(dest,
message, msgid))
}
def formatLogin(callsign : String, ssid : String, passcode : String, version : String) : String = {

Wyświetl plik

@ -1,24 +1,32 @@
package org.aprsdroid.app
import _root_.android.app.Service
import _root_.android.content.{Context, Intent}
import _root_.android.content.{BroadcastReceiver, ContentValues, 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"
val MESSAGE = PACKAGE + ".MESSAGE"
// broadcast intent extras
val LOCATION = PACKAGE + ".LOCATION"
val STATUS = PACKAGE + ".STATUS"
val PACKET = PACKAGE + ".PACKET"
val FAST_LANE_ACT = 30000
val NUM_OF_RETRIES = 7
def intent(ctx : Context, action : String) : Intent = {
new Intent(action, null, ctx, classOf[AprsService])
}
@ -44,6 +52,12 @@ class AprsService extends Service with LocationListener {
lazy val db = StorageDatabase.open(this)
lazy val msgNotifier = new BroadcastReceiver() {
override def onReceive(ctx : Context, i : Intent) {
sendPendingMessages()
}
}
var poster : AprsIsUploader = null
var singleShot = false
@ -109,6 +123,10 @@ class AprsService extends Service with LocationListener {
// continuous GPS tracking for single shot mode
requestLocations(singleShot)
// register for outgoing message notifications
registerReceiver(msgNotifier, new IntentFilter(AprsService.MESSAGE))
val callssid = prefs.getCallSsid()
val message = "%s: %d min, %d km".format(callssid, upd_int, upd_dist)
ServiceNotifier.instance.start(this, message)
@ -137,6 +155,7 @@ class AprsService extends Service with LocationListener {
poster.stop()
showToast(getString(R.string.service_stop))
}
unregisterReceiver(msgNotifier)
ServiceNotifier.instance.stop(this)
running = false
}
@ -220,6 +239,7 @@ class AprsService extends Service with LocationListener {
Log.d(TAG, "packet: " + packet)
val result = try {
sendPendingMessages()
val status = poster.update(packet)
i.putExtra(STATUS, status)
i.putExtra(PACKET, packet.toString)
@ -252,13 +272,67 @@ class AprsService extends Service with LocationListener {
Log.d(TAG, "onStatusChanged: " + provider)
}
def handleMessage(ts : Long, ap : APRSPacket, msg : MessagePacket) {
val callssid = prefs.getCallSsid()
if (msg.getTargetCallsign() == callssid) {
if (msg.isAck() || msg.isRej()) {
val new_type = if (msg.isAck())
StorageDatabase.Message.TYPE_OUT_ACKED
else
StorageDatabase.Message.TYPE_OUT_REJECTED
db.updateMessageAcked(ap.getSourceCall(), msg.getMessageNumber(), new_type)
} else {
db.addMessage(ts, ap.getSourceCall(), msg)
if (msg.getMessageNumber() != "") {
// we need to send an ack
val ack = AprsPacket.formatMessage(callssid, appVersion(), ap.getSourceCall(), "ack", msg.getMessageNumber())
val status = poster.update(ack)
addPost(StorageDatabase.Post.TYPE_POST, status, ack.toString)
}
ServiceNotifier.instance.notifyMessage(this, ap.getSourceCall(), msg.getMessageBody())
}
sendBroadcast(new Intent(AprsService.MESSAGE).putExtra(STATUS, ap.toString))
}
}
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 => 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) {
db.addPost(System.currentTimeMillis(), t, status, message)
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))
}
def postSubmit(post : String) {
handler.post { addPost(StorageDatabase.Post.TYPE_INCMG, "incoming", post) }
handler.post {
addPost(StorageDatabase.Post.TYPE_INCMG, "incoming", post)
sendPendingMessages()
}
}
def postAbort(post : String) {
@ -267,5 +341,47 @@ class AprsService extends Service with LocationListener {
stopSelf()
}
}
def canSendMsg(ts : Long, retrycnt : Int) : Boolean = {
if (retrycnt == 0)
true
else {
//val delta = 30000*scala.math.pow(2, retrycnt-1).toLong
val delta = 30000 * (1 << (retrycnt - 1))
(ts + delta < System.currentTimeMillis)
}
}
def sendPendingMessages() {
import StorageDatabase.Message._
val callssid = prefs.getCallSsid()
val c = db.getPendingMessages(NUM_OF_RETRIES)
//Log.d(TAG, "sendPendingMessages")
c.moveToFirst()
while (!c.isAfterLast()) {
val ts = c.getLong(COLUMN_TS)
val retrycnt = c.getInt(COLUMN_RETRYCNT)
val call = c.getString(COLUMN_CALL)
val msgid = c.getString(COLUMN_MSGID)
val msgtype = c.getInt(COLUMN_TYPE)
val text = c.getString(COLUMN_TEXT)
Log.d(TAG, "pending message: %d/%d ->%s '%s'".format(retrycnt, NUM_OF_RETRIES, call, text))
if (retrycnt < NUM_OF_RETRIES && canSendMsg(ts, retrycnt)) {
val msg = AprsPacket.formatMessage(callssid, appVersion(), call, text, msgid)
val status = poster.update(msg)
addPost(StorageDatabase.Post.TYPE_POST, status, msg.toString)
val cv = new ContentValues()
cv.put(RETRYCNT, (retrycnt + 1).asInstanceOf[java.lang.Integer])
cv.put(TS, System.currentTimeMillis.asInstanceOf[java.lang.Long])
// XXX: do not ack until acked
db.updateMessage(c.getLong(/* COLUMN_ID */ 0), cv)
sendBroadcast(new Intent(AprsService.MESSAGE).putExtra(STATUS, msg.toString))
}
c.moveToNext()
}
c.close()
}
}

Wyświetl plik

@ -0,0 +1,67 @@
package org.aprsdroid.app
import _root_.android.app.Activity
import _root_.android.content._
import _root_.android.database.Cursor
import _root_.android.os.{AsyncTask, Bundle, Handler}
import _root_.android.text.format.DateUtils
import _root_.android.util.Log
import _root_.android.view.View
import _root_.android.widget.{SimpleCursorAdapter, TextView}
object ConversationListAdapter {
import StorageDatabase.Message._
val LIST_FROM = Array(CALL, TEXT)
val LIST_TO = Array(R.id.call, R.id.message)
// null, incoming, out-new, out-acked, out-rejected
val COLORS = Array(0, 0xff8080b0, 0xff80a080, 0xff30b030, 0xffb03030)
}
class ConversationListAdapter(context : Context, prefs : PrefsWrapper)
extends SimpleCursorAdapter(context, R.layout.conversationview, null,
ConversationListAdapter.LIST_FROM, ConversationListAdapter.LIST_TO) {
lazy val storage = StorageDatabase.open(context)
reload()
lazy val locReceiver = new LocationReceiver2(load_cursor,
replace_cursor, cancel_cursor)
context.registerReceiver(locReceiver, new IntentFilter(AprsService.MESSAGE))
override def bindView(view : View, context : Context, cursor : Cursor) {
import StorageDatabase.Message._
val ts = cursor.getLong(COLUMN_TS)
val msgtype = cursor.getInt(COLUMN_TYPE)
view.findViewById(R.id.message).asInstanceOf[TextView]
.setTextColor(MessageListAdapter.COLORS(msgtype))
val age = DateUtils.getRelativeTimeSpanString(context, ts)
view.findViewById(R.id.ts).asInstanceOf[TextView].setText(age)
super.bindView(view, context, cursor)
}
def load_cursor(i : Intent) = {
val c = storage.getConversations()
c.getCount()
c
}
def replace_cursor(c : Cursor) {
changeCursor(c)
context.asInstanceOf[LoadingIndicator].onStopLoading()
}
def cancel_cursor(c : Cursor) {
c.close()
}
def reload() {
locReceiver.startTask(null)
}
def onDestroy() {
context.unregisterReceiver(locReceiver)
changeCursor(null)
}
}

Wyświetl plik

@ -0,0 +1,85 @@
package org.aprsdroid.app
import _root_.android.app.AlertDialog
import _root_.android.app.ListActivity
import _root_.android.content._
import _root_.android.database.Cursor
import _root_.android.os.{Bundle, Handler}
import _root_.android.util.Log
import _root_.android.view.{ContextMenu, LayoutInflater, Menu, MenuItem, View}
import _root_.android.view.View.OnClickListener
import _root_.android.widget.{Button, EditText, ListView}
class ConversationsActivity extends LoadingListActivity
with OnClickListener {
val TAG = "APRSdroid.Conversations"
menu_id = R.id.conversations
lazy val mycall = prefs.getCallSsid()
lazy val pla = new ConversationListAdapter(this, prefs)
lazy val newConversationBtn = findViewById(R.id.new_conversation).asInstanceOf[Button]
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.conversations)
registerForContextMenu(getListView())
newConversationBtn.setOnClickListener(this);
getListView().setOnCreateContextMenuListener(this);
onStartLoading()
setListAdapter(pla)
getListView().setTextFilterEnabled(true)
}
override def onDestroy() {
super.onDestroy()
pla.onDestroy()
}
override def onCreateOptionsMenu(menu : Menu) : Boolean = {
getMenuInflater().inflate(R.menu.options, menu);
true
}
override def onListItemClick(l : ListView, v : View, position : Int, id : Long) {
//super.onListItemClick(l, v, position, id)
val c = getListView().getItemAtPosition(position).asInstanceOf[Cursor]
val call = c.getString(StorageDatabase.Message.COLUMN_CALL)
openMessaging(call)
}
override def onClick(view : View) {
view.getId match {
case R.id.new_conversation =>
newConversation()
}
}
def newConversation() {
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE)
.asInstanceOf[LayoutInflater]
val nm_view = inflater.inflate(R.layout.new_message_view, null, false)
val nm_call = nm_view.findViewById(R.id.callsign).asInstanceOf[EditText]
val nm_text = nm_view.findViewById(R.id.message).asInstanceOf[EditText]
new AlertDialog.Builder(this).setTitle(getString(R.string.msg_send_new))
.setView(nm_view)
//.setIcon(android.R.drawable.ic_dialog_info)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
override def onClick(d : DialogInterface, which : Int) {
which match {
case DialogInterface.BUTTON_POSITIVE =>
openMessageSend(nm_call.getText().toString(),
nm_text.getText().toString())
case _ =>
finish()
}
}})
.setNegativeButton(android.R.string.cancel, null)
.create.show
}
}

Wyświetl plik

@ -83,8 +83,8 @@ class MapAct extends MapActivity with UIHelper {
def getTargetCall() : String = {
val i = getIntent()
if (i != null && i.getStringExtra("call") != null) {
i.getStringExtra("call")
if (i != null && i.getDataString() != null) {
i.getDataString()
} else ""
}

Wyświetl plik

@ -0,0 +1,115 @@
package org.aprsdroid.app
import _root_.android.app.ListActivity
import _root_.android.content._
import _root_.android.database.Cursor
import _root_.android.net.Uri
import _root_.android.os.{Bundle, Handler}
import _root_.android.text.{Editable, TextWatcher}
import _root_.android.util.Log
import _root_.android.view.{KeyEvent, Menu, MenuItem, View, Window}
import _root_.android.view.View.{OnClickListener, OnKeyListener}
import _root_.android.widget.{Button, EditText, ListView}
class MessageActivity extends LoadingListActivity
with OnClickListener with OnKeyListener with TextWatcher {
val TAG = "APRSdroid.Message"
lazy val targetcall = getIntent().getDataString()
lazy val storage = StorageDatabase.open(this)
lazy val mycall = prefs.getCallSsid()
lazy val pla = new MessageListAdapter(this, prefs, mycall, targetcall)
lazy val msginput = findView[EditText](R.id.msginput)
lazy val msgsend = findView[Button](R.id.msgsend)
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.message_act)
//getListView().setOnCreateContextMenuListener(this);
onStartLoading()
setListAdapter(pla)
msginput.addTextChangedListener(this)
msginput.setOnKeyListener(this)
msgsend.setOnClickListener(this)
setTitle(getString(R.string.app_messages) + ": " + targetcall)
val message = getIntent().getStringExtra("message")
if (message != null) {
Log.d(TAG, "sending message to %s: %s".format(targetcall, message))
sendMessage(message)
}
}
override def onResume() {
super.onResume()
ServiceNotifier.instance.cancelMessage(this, targetcall)
}
override def onDestroy() {
super.onDestroy()
pla.onDestroy()
}
override def onCreateOptionsMenu(menu : Menu) : Boolean = {
getMenuInflater().inflate(R.menu.options, menu);
true
}
// TextWatcher for msginput
override def afterTextChanged(s : Editable) {
msgsend.setEnabled(msginput.getText().length() > 0)
}
override def beforeTextChanged(s : CharSequence, start : Int, before : Int, count : Int) {
}
override def onTextChanged(s : CharSequence, start : Int, before : Int, count : Int) {
}
// react on "Return" key
def onKey(v : View, kc : Int, ev : KeyEvent) = {
if (ev.getAction() == KeyEvent.ACTION_DOWN && kc == KeyEvent.KEYCODE_ENTER) {
sendMessage()
true
} else false
}
def sendMessage() {
sendMessage(msginput.getText().toString())
}
def sendMessage(msg : String) {
import StorageDatabase.Message._
if (msg.length() == 0)
return
Log.d("MessageActivity", "sending " + msg)
msginput.setText(null)
val cv = new ContentValues()
cv.put(TS, System.currentTimeMillis().asInstanceOf[java.lang.Long])
cv.put(RETRYCNT, 0.asInstanceOf[java.lang.Integer])
cv.put(CALL, targetcall)
cv.put(MSGID, storage.createMsgId(targetcall).asInstanceOf[java.lang.Integer])
cv.put(TYPE, TYPE_OUT_NEW.asInstanceOf[java.lang.Integer])
cv.put(TEXT, msg)
storage.addMessage(cv)
// notify backend
sendBroadcast(new Intent(AprsService.MESSAGE))
}
// button actions
override def onClick(view : View) {
Log.d(TAG, "onClick: " + view.getId)
view.getId match {
case R.id.msgsend =>
sendMessage()
true
case _ => false
}
}
}

Wyświetl plik

@ -0,0 +1,79 @@
package org.aprsdroid.app
import _root_.android.app.Activity
import _root_.android.content._
import _root_.android.database.Cursor
import _root_.android.os.{AsyncTask, Bundle, Handler}
import _root_.android.text.format.DateUtils
import _root_.android.util.Log
import _root_.android.view.View
import _root_.android.widget.{SimpleCursorAdapter, TextView}
object MessageListAdapter {
import StorageDatabase.Message._
val LIST_FROM = Array("TSS", CALL, TEXT)
val LIST_TO = Array(R.id.listts, R.id.liststatus, R.id.listmessage)
val NUM_OF_RETRIES = 7
// null, incoming, out-new, out-acked, out-rejected
val COLORS = Array(0, 0xff8080b0, 0xff80a080, 0xff30b030, 0xffb03030)
}
class MessageListAdapter(context : Context, prefs : PrefsWrapper,
mycall : String, targetcall : String)
extends SimpleCursorAdapter(context, R.layout.listitem, null, MessageListAdapter.LIST_FROM, MessageListAdapter.LIST_TO) {
lazy val storage = StorageDatabase.open(context)
reload()
lazy val locReceiver = new LocationReceiver2(load_cursor,
replace_cursor, cancel_cursor)
context.registerReceiver(locReceiver, new IntentFilter(AprsService.MESSAGE))
override def bindView(view : View, context : Context, cursor : Cursor) {
import StorageDatabase.Message._
val msgtype = cursor.getInt(COLUMN_TYPE)
val retrycnt = cursor.getInt(COLUMN_RETRYCNT)
view.findViewById(R.id.listmessage).asInstanceOf[TextView]
.setTextColor(MessageListAdapter.COLORS(msgtype))
val statusview = view.findViewById(R.id.liststatus).asInstanceOf[TextView]
statusview.setTextColor(MessageListAdapter.COLORS(msgtype))
super.bindView(view, context, cursor)
msgtype match {
case TYPE_INCOMING =>
statusview.setText(targetcall)
case TYPE_OUT_NEW =>
statusview.setText("%s %d/%d".format(mycall, retrycnt, MessageListAdapter.NUM_OF_RETRIES))
case TYPE_OUT_ACKED =>
//statusview.setText("%s ack #%d".format(mycall, retrycnt))
statusview.setText(mycall)
case TYPE_OUT_REJECTED =>
statusview.setText("%s rej #%d".format(mycall, retrycnt))
}
}
def load_cursor(i : Intent) = {
val c = storage.getMessages(targetcall)
c.getCount()
c
}
def replace_cursor(c : Cursor) {
changeCursor(c)
context.asInstanceOf[LoadingIndicator].onStopLoading()
}
def cancel_cursor(c : Cursor) {
c.close()
}
def reload() {
locReceiver.startTask(null)
}
def onDestroy() {
context.unregisterReceiver(locReceiver)
changeCursor(null)
}
}

Wyświetl plik

@ -39,10 +39,10 @@ class PositionListAdapter(context : Context, prefs : PrefsWrapper,
context.registerReceiver(locReceiver, new IntentFilter(AprsService.UPDATE))
private val DARK = Array(0xff, 0x60, 0x60, 0x40)
private val BRIGHT = Array(0xff, 0xff, 0xff, 0xc0)
private val MAX = 30*60*1000
def getAgeColor(ts : Long) : Int = {
val DARK = Array(0xff, 0x60, 0x60, 0x40)
val BRIGHT = Array(0xff, 0xff, 0xff, 0xc0)
val MAX = 30*60*1000
val delta = (System.currentTimeMillis - ts).toInt
val factor = if (delta < MAX) delta else MAX
val mix = DARK zip BRIGHT map (t => { t._2 - (t._2 - t._1)*factor/MAX } )
@ -98,7 +98,6 @@ class PositionListAdapter(context : Context, prefs : PrefsWrapper,
def load_cursor(i : Intent) = {
import PositionListAdapter._
Benchmark("get my position") {
val cursor = storage.getStaPosition(mycall)
if (cursor.getCount() > 0) {
cursor.moveToFirst()
@ -106,14 +105,13 @@ class PositionListAdapter(context : Context, prefs : PrefsWrapper,
my_lon = cursor.getInt(StorageDatabase.Position.COLUMN_LON)
}
cursor.close()
}
val c = mode match {
case SINGLE => storage.getStaPosition(targetcall)
case NEIGHBORS => storage.getNeighbors(mycall, my_lat, my_lon,
System.currentTimeMillis - prefs.getShowAge(), "50")
case SSIDS => storage.getAllSsids(targetcall)
}
Benchmark("getCount") { c.getCount() }
c.getCount()
c
}

Wyświetl plik

@ -2,6 +2,7 @@ package org.aprsdroid.app
import _root_.android.app.{Notification, NotificationManager, PendingIntent, Service}
import _root_.android.content.{Context, Intent}
import _root_.android.net.Uri
import _root_.android.os.Build
@ -11,6 +12,8 @@ object ServiceNotifier {
abstract class ServiceNotifier {
val SERVICE_NOTIFICATION : Int = 1
var CALL_NOTIFICATION = SERVICE_NOTIFICATION + 1
val callIdMap = new scala.collection.mutable.HashMap[String, Int]()
def newNotification(ctx : Service, status : String) : Notification = {
val n = new Notification()
@ -25,10 +28,43 @@ abstract class ServiceNotifier {
n
}
def getNotificationMgr(ctx : Service) = ctx.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
def getCallNumber(call : String) : Int = {
if (callIdMap.contains(call)) {
callIdMap(call)
} else {
val id = CALL_NOTIFICATION
CALL_NOTIFICATION += 1
callIdMap(call) = id
id
}
}
def newMessageNotification(ctx : Service, call : String, message : String) : Notification = {
val n = new Notification()
n.icon = R.drawable.icon
n.when = System.currentTimeMillis
n.flags |= Notification.FLAG_AUTO_CANCEL
val i = new Intent(ctx, classOf[MessageActivity])
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
i.setData(Uri.parse(call))
n.contentIntent = PendingIntent.getActivity(ctx, 0, i, PendingIntent.FLAG_UPDATE_CURRENT)
n.setLatestEventInfo(ctx, call, message, n.contentIntent)
n
}
def getNotificationMgr(ctx : Context) = ctx.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
def start(ctx : Service, status : String)
def stop(ctx : Service)
def notifyMessage(ctx : Service, call : String, message : String) {
getNotificationMgr(ctx).notify(getCallNumber(call),
newMessageNotification(ctx, call, message))
}
def cancelMessage(ctx : Context, call : String) {
getNotificationMgr(ctx).cancel(getCallNumber(call))
}
}
class DonutNotifier extends ServiceNotifier {

Wyświetl plik

@ -12,7 +12,7 @@ import _root_.android.widget.{ListView,SimpleCursorAdapter}
class StationActivity extends LoadingListActivity
with OnClickListener {
lazy val targetcall = getIntent().getStringExtra("call")
lazy val targetcall = getIntent().getDataString()
lazy val storage = StorageDatabase.open(this)
lazy val postlist = findViewById(R.id.postlist).asInstanceOf[ListView]

Wyświetl plik

@ -14,8 +14,11 @@ import _root_.scala.math.{cos, Pi}
object StorageDatabase {
val TAG = "APRSdroid.Storage"
val DB_VERSION = 1
val DB_VERSION = 2
val DB_NAME = "storage.db"
val TSS_COL = "DATETIME(TS/1000, 'unixepoch', 'localtime') as TSS"
object Post {
val TABLE = "posts"
val _ID = "_id"
@ -25,7 +28,7 @@ object StorageDatabase {
val MESSAGE = "message"
lazy val TABLE_CREATE = "CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s LONG, %s INTEGER, %s TEXT, %s TEXT)"
.format(TABLE, _ID, TS, TYPE, STATUS, MESSAGE);
lazy val COLUMNS = Array(_ID, TS, "DATETIME(TS/1000, 'unixepoch', 'localtime') as TSS", TYPE, STATUS, MESSAGE);
lazy val COLUMNS = Array(_ID, TS, TSS_COL, TYPE, STATUS, MESSAGE);
val TYPE_POST = 0
val TYPE_INFO = 1
@ -84,6 +87,39 @@ object StorageDatabase {
lazy val TABLE_INDEX = "CREATE INDEX idx_position_%s ON position (%s)"
}
object Message {
val TABLE = "message"
val _ID = "_id"
val TS = "ts" // timestamp of RX or first TX
val RETRYCNT = "retrycnt" // attemp number for sending msg
val CALL = "call" // callsign of comms partner
val MSGID = "msgid" // message id (up to 5 alphanumeric symbols)
val TYPE = "type" // incoming / out-new / out-acked
val TEXT = "text" // message text
lazy val TABLE_CREATE = """CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT,
%s LONG, %s INT,
%s TEXT, %s TEXT,
%s INTEGER, %s TEXT)"""
.format(TABLE, _ID, TS, RETRYCNT,
CALL, MSGID,
TYPE, TEXT)
lazy val COLUMNS = Array(_ID, TS, TSS_COL, RETRYCNT, CALL, MSGID, TYPE, TEXT)
val COLUMN_TS = 1
val COLUMN_TTS = 2
val COLUMN_RETRYCNT = 3
val COLUMN_CALL = 4
val COLUMN_MSGID = 5
val COLUMN_TYPE = 6
val COLUMN_TEXT = 7
val TYPE_INCOMING = 1
val TYPE_OUT_NEW = 2
val TYPE_OUT_ACKED = 3
val TYPE_OUT_REJECTED = 4
}
var singleton : StorageDatabase = null
def open(context : Context) : StorageDatabase = {
if (singleton == null) {
@ -103,7 +139,7 @@ object StorageDatabase {
else
null
} else
c.getString(Position.COLUMN_CALL)
c.getString(callidx)
}
}
@ -117,27 +153,13 @@ class StorageDatabase(context : Context) extends
db.execSQL(Post.TABLE_CREATE);
db.execSQL(Position.TABLE_CREATE)
Array("call", "lat", "lon").map(col => db.execSQL(Position.TABLE_INDEX.format(col, col)))
db.execSQL(Message.TABLE_CREATE)
}
def resetPositionsTable(db : SQLiteDatabase) {
db.execSQL(Position.TABLE_DROP)
db.execSQL(Position.TABLE_CREATE)
Array("call", "lat", "lon").map(col => db.execSQL(Position.TABLE_INDEX.format(col, col)))
return; // this code causes a too long wait in onUpgrade...
// we can not call getPosts() here due to recursion issues
val c = db.query(Post.TABLE, Post.COLUMNS, "TYPE = 0 OR TYPE = 3",
null, null, null, "_ID DESC", null)
c.moveToFirst()
while (!c.isAfterLast()) {
val message = c.getString(c.getColumnIndexOrThrow(Post.MESSAGE))
val ts = c.getLong(c.getColumnIndexOrThrow(Post.TS))
parsePacket(ts, message)
c.moveToNext()
}
c.close()
}
def resetPositionsTable() : Unit = resetPositionsTable(getWritableDatabase())
override def onUpgrade(db: SQLiteDatabase, from : Int, to : Int) {
if (from <= 1 && to <= 2) {
db.execSQL(Message.TABLE_CREATE)
}
}
def trimPosts(ts : Long) = Benchmark("trimPosts") {
@ -175,27 +197,34 @@ class StorageDatabase(context : Context) extends
getWritableDatabase().insertOrThrow(TABLE, CALL, cv)
}
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 => addPosition(ts, fap, pp.getPosition(), null)
case op : ObjectPacket => addPosition(ts, fap, op.getPosition(), op.getObjectName())
}
} catch {
case e : Exception =>
Log.d(TAG, "parsePacket() unsupported packet: " + message)
e.printStackTrace()
}
def isMessageDuplicate(call : String, msgid : String, text : String) : Boolean = {
val c = getReadableDatabase().query(Message.TABLE, Message.COLUMNS,
"type = 1 AND call = ? AND msgid = ? AND text = ?",
Array(call, msgid, text),
null, null,
null, null)
val result = (c.getCount() > 0)
c.close()
result
}
def getPositions(sel : String, selArgs : Array[String], limit : String) : Cursor = Benchmark("getPositions") {
def addMessage(ts : Long, srccall : String, msg : MessagePacket) {
import Message._
if (isMessageDuplicate(srccall, msg.getMessageNumber(), msg.getMessageBody())) {
Log.i(TAG, "received duplicate message from %s: %s".format(srccall, msg))
return
}
val cv = new ContentValues()
cv.put(TS, ts.asInstanceOf[java.lang.Long])
cv.put(RETRYCNT, 0.asInstanceOf[java.lang.Integer])
cv.put(CALL, srccall)
cv.put(MSGID, msg.getMessageNumber())
cv.put(TYPE, TYPE_INCOMING.asInstanceOf[java.lang.Integer])
cv.put(TEXT, msg.getMessageBody())
addMessage(cv)
}
def getPositions(sel : String, selArgs : Array[String], limit : String) : Cursor = {
getReadableDatabase().query(Position.TABLE, Position.COLUMNS_MAP,
sel, selArgs,
null, null, "CALL, _ID", limit)
@ -207,23 +236,23 @@ class StorageDatabase(context : Context) extends
Array(lat1, lat2, lon1, lon2).map(_.toString), limit)
}
def getStaPosition(call : String) : Cursor = Benchmark("getStaPosition") {
def getStaPosition(call : String) : Cursor = {
getReadableDatabase().query(Position.TABLE, Position.COLUMNS,
"call LIKE ?", Array(call),
null, null, "_ID DESC", "1")
}
def getStaPositions(call : String, limit : String) : Cursor = Benchmark("getStaPositions") {
def getStaPositions(call : String, limit : String) : Cursor = {
getReadableDatabase().query(Position.TABLE, Position.COLUMNS,
"call LIKE ? AND TS > ?", Array(call, limit),
null, null, "_ID DESC", null)
}
def getAllSsids(call : String) : Cursor = Benchmark("getAllSsids") {
def getAllSsids(call : String) : Cursor = {
val querycall = call.split("[- _]+")(0) + "%"
getReadableDatabase().query(Position.TABLE, Position.COLUMNS,
"call LIKE ? or origin LIKE ?", Array(querycall, querycall),
"call", null, null, null)
}
def getNeighbors(mycall : String, lat : Int, lon : Int, ts : Long, limit : String) : Cursor = Benchmark("getNeighbors") {
def getNeighbors(mycall : String, lat : Int, lon : Int, ts : Long, limit : String) : Cursor = {
// calculate latitude correction
val corr = (cos(Pi*lat/180000000.)*cos(Pi*lat/180000000.)*100).toInt
Log.d(TAG, "getNeighbors: correcting by %d".format(corr))
@ -234,7 +263,7 @@ class StorageDatabase(context : Context) extends
"call", null, "dist", limit)
}
def getNeighborsLike(call : String, lat : Int, lon : Int, ts : Long, limit : String) : Cursor = Benchmark("getNeighborsLike") {
def getNeighborsLike(call : String, lat : Int, lon : Int, ts : Long, limit : String) : Cursor = {
// calculate latitude correction
val corr = (cos(Pi*lat/180000000.)*cos(Pi*lat/180000000.)*100).toInt
Log.d(TAG, "getNeighborsLike: correcting by %d".format(corr))
@ -252,12 +281,6 @@ class StorageDatabase(context : Context) extends
cv.put(Post.STATUS, status)
cv.put(Post.MESSAGE, message)
getWritableDatabase().insertOrThrow(Post.TABLE, Post.MESSAGE, cv)
if (posttype == Post.TYPE_POST || posttype == Post.TYPE_INCMG) {
parsePacket(ts, message)
} else {
// only log status messages
Log.d(TAG, "StorageDatabase.addPost: " + status + " - " + message)
}
if (Post.trimCounter == 0) {
trimPosts()
Post.trimCounter = 100
@ -308,4 +331,54 @@ class StorageDatabase(context : Context) extends
}
}
def getMessages(call : String) = {
getReadableDatabase().query(Message.TABLE, Message.COLUMNS,
"call = ?", Array(call),
null, null,
null, null)
}
def getPendingMessages(retries : Int) = {
getReadableDatabase().query(Message.TABLE, Message.COLUMNS,
"type = 2 and retrycnt < ?", Array(retries.toString),
null, null,
null, null)
}
def addMessage(cv : ContentValues) = {
getWritableDatabase().insertOrThrow(Message.TABLE, "_id", cv)
}
def updateMessage(id : Long, cv : ContentValues) = {
getWritableDatabase().update(Message.TABLE, cv, "_id = ?", Array(id.toString))
}
def updateMessageAcked(call : String, msgid : String, new_type : Int) = {
val cv = new ContentValues()
cv.put(Message.TYPE, new_type.asInstanceOf[java.lang.Integer])
getWritableDatabase().update(Message.TABLE, cv, "type = 2 AND call = ? AND msgid = ?",
Array(call, msgid))
}
def createMsgId(call : String) = {
val c = getReadableDatabase().query(Message.TABLE, Array("max(msgid)"),
"call = ? AND type != ?", Array(call, Message.TYPE_INCOMING.toString),
null, null,
null, null)
c.moveToFirst()
val result = if (c.getCount() == 0)
0
else c.getInt(0) + 1
Log.d(TAG, "createMsgId(%s) = %d".format(call, result))
c.close()
result
}
def getConversations() = {
getReadableDatabase().query(Message.TABLE, Message.COLUMNS,
null, null,
"call", null,
"_id DESC", null)
}
}

Wyświetl plik

@ -25,13 +25,21 @@ trait UIHelper extends Activity
}
def openDetails(call : String) {
startActivity(new Intent(this, classOf[StationActivity]).putExtra("call", call))
startActivity(new Intent(this, classOf[StationActivity]).setData(Uri.parse(call)))
}
def openMessaging(call : String) {
startActivity(new Intent(this, classOf[MessageActivity]).setData(Uri.parse(call)))
}
def openMessageSend(call : String, message : String) {
startActivity(new Intent(this, classOf[MessageActivity]).setData(Uri.parse(call)).putExtra("message", message))
}
def trackOnMap(call : String) {
val text = getString(R.string.map_track_call, call)
Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
startActivity(new Intent(this, classOf[MapAct]).putExtra("call", call))
startActivity(new Intent(this, classOf[MapAct]).setData(Uri.parse(call)))
}
def openPrefs(toastId : Int) {
@ -189,9 +197,10 @@ trait UIHelper extends Activity
mi.setTitle(if (AprsService.running) R.string.stoplog else R.string.startlog)
mi.setIcon(if (AprsService.running) android.R.drawable.ic_menu_close_clear_cancel else android.R.drawable.ic_menu_compass)
// disable the "own" menu
Array(R.id.hub, R.id.map, R.id.log).map((id) => {
Array(R.id.hub, R.id.map, R.id.log, R.id.conversations).map((id) => {
menu.findItem(id).setVisible(id != menu_id)
})
menu.findItem(R.id.age).setVisible(R.id.map == menu_id || R.id.hub == menu_id)
menu.findItem(R.id.overlays).setVisible(R.id.map == menu_id)
menu.findItem(R.id.objects).setChecked(prefs.getShowObjects())
menu.findItem(R.id.satellite).setChecked(prefs.getShowSatellite())
@ -223,6 +232,9 @@ trait UIHelper extends Activity
case R.id.log =>
startActivity(new Intent(this, classOf[LogActivity]));
true
case R.id.conversations =>
startActivity(new Intent(this, classOf[ConversationsActivity]));
true
// toggle service
case R.id.startstopbtn =>
val is_running = AprsService.running
@ -269,6 +281,9 @@ trait UIHelper extends Activity
case R.id.details =>
openDetails(targetcall)
true
case R.id.message =>
openMessaging(targetcall)
true
case R.id.mapbutton =>
trackOnMap(targetcall)
true