Allow sending bitcoins in chat.

This commit is contained in:
Felix Ableitner 2015-04-24 00:43:21 +02:00
parent 284a4ceb02
commit c350ac12e0
32 changed files with 736 additions and 118 deletions

View file

@ -265,3 +265,29 @@ Text the string to be transferred, encoded as UTF-8.
Contains the sender's name and status, which should be used for Contains the sender's name and status, which should be used for
display to users. display to users.
### InitiatePayment (Type = 5)
Requests PaymentRequest message.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
### PaymentInformation (Type = 6)
Contains Bitcoin payment info.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payment Request Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Payment Request (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Payment Request is a protobuf-formatted Bitcoin payment message.

View file

@ -18,6 +18,7 @@ dependencies {
compile "org.scala-lang:scala-library:2.11.7" compile "org.scala-lang:scala-library:2.11.7"
compile 'com.google.guava:guava:18.0' compile 'com.google.guava:guava:18.0'
compile 'com.mobsandgeeks:adapter-kit:0.5.3' compile 'com.mobsandgeeks:adapter-kit:0.5.3'
// TODO: use @aar maven dependency for bitcoin
} }
// RtlHardcoded behaviour differs between target API versions. We only care about API 15. // RtlHardcoded behaviour differs between target API versions. We only care about API 15.

View file

@ -90,15 +90,16 @@ class RouterTest extends AndroidTestCase {
} }
def testHopLimit(): Unit = Range(19, 22).foreach { i => def testHopLimit(): Unit = Range(19, 22).foreach { i =>
val msg = new Message( val header =
new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(new Date()), i), new Text("")) new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(new Date()), false, i)
val msg = new Message(header, new Text(""))
val router: Router = new Router(neighbors, (a, m) => fail()) val router: Router = new Router(neighbors, (a, m) => fail())
router.onReceive(msg) router.onReceive(msg)
} }
private def generateMessage(sender: Address, receiver: Address, seqNum: Int): Message = { private def generateMessage(sender: Address, receiver: Address, seqNum: Int): Message = {
val header = new ContentHeader(sender, receiver, seqNum, UserInfo.Type, Some(5), val header = new ContentHeader(sender, receiver, seqNum, UserInfo.Type, Some(5),
Some(new GregorianCalendar(2014, 6, 10).getTime)) Some(new GregorianCalendar(2014, 6, 10).getTime), false)
new Message(header, new UserInfo("", "")) new Message(header, new UserInfo("", ""))
} }

View file

@ -22,7 +22,7 @@ class ConnectionInfoTest extends AndroidTestCase {
val ci = ConnectionInfoTest.generateCi(getContext) val ci = ConnectionInfoTest.generateCi(getContext)
val bytes = ci.write val bytes = ci.write
val body = ConnectionInfo.read(bytes) val body = ConnectionInfo.read(bytes)
Assert.assertEquals(ci.key, body.asInstanceOf[ConnectionInfo].key) Assert.assertEquals(ci, body)
} }
} }

View file

@ -0,0 +1,16 @@
package com.nutomic.ensichat.protocol.body
import android.test.AndroidTestCase
import junit.framework.Assert
class PaymentInformationTest extends AndroidTestCase {
def testWriteRead(): Unit = {
val pi = new PaymentInformation("testmessage".getBytes)
val bytes = pi.write
val body = PaymentInformation.read(bytes)
Assert.assertEquals(pi, body)
}
}

View file

@ -1,30 +1,33 @@
package com.nutomic.ensichat.protocol.header package com.nutomic.ensichat.protocol.header
import java.util.{GregorianCalendar, Date} import java.util.{Date, GregorianCalendar}
import android.test.AndroidTestCase import android.test.AndroidTestCase
import com.nutomic.ensichat.protocol.body.Text import com.nutomic.ensichat.protocol.body.{PaymentInformation, Text}
import com.nutomic.ensichat.protocol.{Address, AddressTest} import com.nutomic.ensichat.protocol.{Address, AddressTest}
import junit.framework.Assert._ import junit.framework.Assert._
object ContentHeaderTest { object ContentHeaderTest {
val h1 = new ContentHeader(AddressTest.a1, AddressTest.a2, 1234, val h1 = new ContentHeader(AddressTest.a1, AddressTest.a2, 1234,
Text.Type, Some(123), Some(new GregorianCalendar(1970, 1, 1).getTime), 5) Text.Type, Some(123), Some(new GregorianCalendar(1970, 1, 1).getTime), false, 5)
val h2 = new ContentHeader(AddressTest.a1, AddressTest.a3, val h2 = new ContentHeader(AddressTest.a1, AddressTest.a3,
30000, Text.Type, Some(8765), Some(new GregorianCalendar(2014, 6, 10).getTime), 20) 30000, Text.Type, Some(8765), Some(new GregorianCalendar(2014, 6, 10).getTime), false, 20)
val h3 = new ContentHeader(AddressTest.a4, AddressTest.a2, val h3 = new ContentHeader(AddressTest.a4, AddressTest.a2,
250, Text.Type, Some(77), Some(new GregorianCalendar(2020, 11, 11).getTime), 123) 250, Text.Type, Some(77), Some(new GregorianCalendar(2020, 11, 11).getTime), false, 123)
val h4 = new ContentHeader(Address.Null, Address.Broadcast, val h4 = new ContentHeader(Address.Null, Address.Broadcast,
ContentHeader.SeqNumRange.last, 0, Some(0xffff), Some(new Date(0L)), 0xff) ContentHeader.SeqNumRange.last, 0, Some(0xffff), Some(new Date(0L)), false, 0xff)
val h5 = new ContentHeader(Address.Broadcast, Address.Null, val h5 = new ContentHeader(Address.Broadcast, Address.Null,
0, 0xff, Some(0), Some(new Date(0xffffffffL)), 0) 0, 0xff, Some(0), Some(new Date(0xffffffffL)), false, 0)
val headers = Set(h1, h2, h3, h4, h5) val h6 = new ContentHeader(AddressTest.a1, AddressTest.a2, 1234,
PaymentInformation.Type, Some(123), Some(new GregorianCalendar(2015, 8, 9).getTime), false, 5)
val headers = Set(h1, h2, h3, h4, h5, h6)
} }

View file

@ -8,10 +8,10 @@ import android.database.sqlite.SQLiteDatabase
import android.support.v4.content.LocalBroadcastManager import android.support.v4.content.LocalBroadcastManager
import android.test.AndroidTestCase import android.test.AndroidTestCase
import com.nutomic.ensichat.protocol.MessageTest._ import com.nutomic.ensichat.protocol.MessageTest._
import com.nutomic.ensichat.protocol.body.CryptoData import com.nutomic.ensichat.protocol.body.{CryptoData, PaymentInformation}
import com.nutomic.ensichat.protocol.header.{ContentHeader, ContentHeaderTest} import com.nutomic.ensichat.protocol.header.ContentHeader
import com.nutomic.ensichat.protocol.header.ContentHeaderTest._ import com.nutomic.ensichat.protocol.header.ContentHeaderTest._
import com.nutomic.ensichat.protocol.{MessageTest, UserTest} import com.nutomic.ensichat.protocol.{AddressTest, Message, MessageTest, UserTest}
import junit.framework.Assert._ import junit.framework.Assert._
object DatabaseTest { object DatabaseTest {
@ -68,18 +68,33 @@ class DatabaseTest extends AndroidTestCase {
assertTrue(msg.contains(m3)) assertTrue(msg.contains(m3))
} }
def testMessageFields(): Unit = { def testTextMessage(): Unit = {
val msg = database.getMessages(m2.header.target, 1).firstKey val msg = database.getMessages(m3.header.target, 1).firstKey
val header = msg.header.asInstanceOf[ContentHeader] val header = msg.header.asInstanceOf[ContentHeader]
assertEquals(h2.origin, header.origin) assertEquals(h2.origin, header.origin)
assertEquals(h2.target, header.target) assertEquals(h2.target, header.target)
assertEquals(-1, msg.header.seqNum) assertEquals(-1, msg.header.seqNum)
assertEquals(h2.contentType, header.contentType) assertEquals(h3.contentType, header.contentType)
assertEquals(h2.messageId, header.messageId) assertEquals(h3.messageId, header.messageId)
assertEquals(h2.time, header.time) assertEquals(h3.time, header.time)
assertEquals(h3.read, header.read)
assertEquals(new CryptoData(None, None), msg.crypto) assertEquals(new CryptoData(None, None), msg.crypto)
assertEquals(m2.body, msg.body) assertEquals(m3.body, msg.body)
}
def testPaymentRequestMessage(): Unit = {
val pr = new PaymentInformation("teststring".getBytes)
val msg = new Message(h6, pr)
database.onMessageReceived(msg)
val retrieved = database.getMessages(h6.origin, 1).firstKey
assertEquals(pr, retrieved.body)
}
def testMessageRead(): Unit = {
database.setMessageRead(h3)
val header = database.getMessages(AddressTest.a4, 1).firstKey.header.asInstanceOf[ContentHeader]
assertTrue(header.read)
} }
def testAddContact(): Unit = { def testAddContact(): Unit = {

View file

@ -0,0 +1,284 @@
/**
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.schildbach.wallet.integration.android;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.widget.Toast;
/**
* @author Andreas Schildbach
*/
public final class BitcoinIntegration
{
private static final String INTENT_EXTRA_PAYMENTREQUEST = "paymentrequest";
private static final String INTENT_EXTRA_PAYMENT = "payment";
private static final String INTENT_EXTRA_TRANSACTION_HASH = "transaction_hash";
private static final String MIMETYPE_PAYMENTREQUEST = "application/bitcoin-paymentrequest"; // BIP 71
/**
* Request any amount of Bitcoins (probably a donation) from user, without feedback from the app.
*
* @param context
* Android context
* @param address
* Bitcoin address
*/
public static void request(final Context context, final String address)
{
final Intent intent = makeBitcoinUriIntent(address, null);
start(context, intent);
}
/**
* Request specific amount of Bitcoins from user, without feedback from the app.
*
* @param context
* Android context
* @param address
* Bitcoin address
* @param amount
* Bitcoin amount in satoshis
*/
public static void request(final Context context, final String address, final long amount)
{
final Intent intent = makeBitcoinUriIntent(address, amount);
start(context, intent);
}
/**
* Request payment from user, without feedback from the app.
*
* @param context
* Android context
* @param paymentRequest
* BIP70 formatted payment request
*/
public static void request(final Context context, final byte[] paymentRequest)
{
final Intent intent = makePaymentRequestIntent(paymentRequest);
start(context, intent);
}
/**
* Request any amount of Bitcoins (probably a donation) from user, with feedback from the app. Result intent can be
* received by overriding {@link android.app.Activity#onActivityResult()}. Result indicates either
* {@link Activity#RESULT_OK} or {@link Activity#RESULT_CANCELED}. In the success case, use
* {@link #transactionHashFromResult(Intent)} to read the transaction hash from the intent.
*
* Warning: A success indication is no guarantee! To be on the safe side, you must drive your own Bitcoin
* infrastructure and validate the transaction.
*
* @param activity
* Calling Android activity
* @param requestCode
* Code identifying the call when {@link android.app.Activity#onActivityResult()} is called back
* @param address
* Bitcoin address
*/
public static void requestForResult(final Activity activity, final int requestCode, final String address)
{
final Intent intent = makeBitcoinUriIntent(address, null);
startForResult(activity, requestCode, intent);
}
/**
* Request specific amount of Bitcoins from user, with feedback from the app. Result intent can be received by
* overriding {@link android.app.Activity#onActivityResult()}. Result indicates either {@link Activity#RESULT_OK} or
* {@link Activity#RESULT_CANCELED}. In the success case, use {@link #transactionHashFromResult(Intent)} to read the
* transaction hash from the intent.
*
* Warning: A success indication is no guarantee! To be on the safe side, you must drive your own Bitcoin
* infrastructure and validate the transaction.
*
* @param activity
* Calling Android activity
* @param requestCode
* Code identifying the call when {@link android.app.Activity#onActivityResult()} is called back
* @param address
* Bitcoin address
*/
public static void requestForResult(final Activity activity, final int requestCode, final String address, final long amount)
{
final Intent intent = makeBitcoinUriIntent(address, amount);
startForResult(activity, requestCode, intent);
}
/**
* Request payment from user, with feedback from the app. Result intent can be received by overriding
* {@link android.app.Activity#onActivityResult()}. Result indicates either {@link Activity#RESULT_OK} or
* {@link Activity#RESULT_CANCELED}. In the success case, use {@link #transactionHashFromResult(Intent)} to read the
* transaction hash from the intent.
*
* Warning: A success indication is no guarantee! To be on the safe side, you must drive your own Bitcoin
* infrastructure and validate the transaction.
*
* @param activity
* Calling Android activity
* @param requestCode
* Code identifying the call when {@link android.app.Activity#onActivityResult()} is called back
* @param paymentRequest
* BIP70 formatted payment request
*/
public static void requestForResult(final Activity activity, final int requestCode, final byte[] paymentRequest)
{
final Intent intent = makePaymentRequestIntent(paymentRequest);
startForResult(activity, requestCode, intent);
}
/**
* Get payment request from intent. Meant for usage by applications accepting payment requests.
*
* @param intent
* intent
* @return payment request or null
*/
public static byte[] paymentRequestFromIntent(final Intent intent)
{
final byte[] paymentRequest = intent.getByteArrayExtra(INTENT_EXTRA_PAYMENTREQUEST);
return paymentRequest;
}
/**
* Put BIP70 payment message into result intent. Meant for usage by Bitcoin wallet applications.
*
* @param result
* result intent
* @param payment
* payment message
*/
public static void paymentToResult(final Intent result, final byte[] payment)
{
result.putExtra(INTENT_EXTRA_PAYMENT, payment);
}
/**
* Get BIP70 payment message from result intent. Meant for usage by applications initiating a Bitcoin payment.
*
* You can use the transactions contained in the payment to validate the payment. For this, you need your own
* Bitcoin infrastructure though. There is no guarantee that the payment will ever confirm.
*
* @param result
* result intent
* @return payment message
*/
public static byte[] paymentFromResult(final Intent result)
{
final byte[] payment = result.getByteArrayExtra(INTENT_EXTRA_PAYMENT);
return payment;
}
/**
* Put transaction hash into result intent. Meant for usage by Bitcoin wallet applications.
*
* @param result
* result intent
* @param txHash
* transaction hash
*/
public static void transactionHashToResult(final Intent result, final String txHash)
{
result.putExtra(INTENT_EXTRA_TRANSACTION_HASH, txHash);
}
/**
* Get transaction hash from result intent. Meant for usage by applications initiating a Bitcoin payment.
*
* You can use this hash to request the transaction from the Bitcoin network, in order to validate. For this, you
* need your own Bitcoin infrastructure though. There is no guarantee that the transaction has ever been broadcasted
* to the Bitcoin network.
*
* @param result
* result intent
* @return transaction hash
*/
public static String transactionHashFromResult(final Intent result)
{
final String txHash = result.getStringExtra(INTENT_EXTRA_TRANSACTION_HASH);
return txHash;
}
private static final int SATOSHIS_PER_COIN = 100000000;
private static Intent makeBitcoinUriIntent(final String address, final Long amount)
{
final StringBuilder uri = new StringBuilder("bitcoin:");
if (address != null)
uri.append(address);
if (amount != null)
uri.append("?amount=").append(String.format("%d.%08d", amount / SATOSHIS_PER_COIN, amount % SATOSHIS_PER_COIN));
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri.toString()));
return intent;
}
private static Intent makePaymentRequestIntent(final byte[] paymentRequest)
{
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setType(MIMETYPE_PAYMENTREQUEST);
intent.putExtra(INTENT_EXTRA_PAYMENTREQUEST, paymentRequest);
return intent;
}
private static void start(final Context context, final Intent intent)
{
final PackageManager pm = context.getPackageManager();
if (pm.resolveActivity(intent, 0) != null)
context.startActivity(intent);
else
redirectToDownload(context);
}
private static void startForResult(final Activity activity, final int requestCode, final Intent intent)
{
final PackageManager pm = activity.getPackageManager();
if (pm.resolveActivity(intent, 0) != null)
activity.startActivityForResult(intent, requestCode);
else
redirectToDownload(activity);
}
private static void redirectToDownload(final Context context)
{
Toast.makeText(context, "No Bitcoin application found.\nPlease install Bitcoin Wallet.", Toast.LENGTH_LONG).show();
final Intent marketIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=de.schildbach.wallet"));
final Intent binaryIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/schildbach/bitcoin-wallet/releases"));
final PackageManager pm = context.getPackageManager();
if (pm.resolveActivity(marketIntent, 0) != null)
context.startActivity(marketIntent);
else if (pm.resolveActivity(binaryIntent, 0) != null)
context.startActivity(binaryIntent);
// else out of luck
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -36,6 +36,13 @@
android:id="@+id/linearLayout" android:id="@+id/linearLayout"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageButton
android:layout_width="45dp"
android:layout_height="45dp"
android:id="@+id/send_bitcoin"
android:src="@drawable/ic_bitcoin"
android:background="@android:color/transparent"/>
<EditText <EditText
android:id="@+id/message" android:id="@+id/message"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -3,10 +3,17 @@
<string name="default_user_status">Let\'s chat!</string> <string name="default_user_status">Let\'s chat!</string>
<bool name="default_notification_sounds">true</bool>
<string name="default_scan_interval">15</string> <string name="default_scan_interval">15</string>
<bool name="default_notification_sounds">true</bool>
<string name="default_max_connections">1000000</string> <string name="default_max_connections">1000000</string>
</resources> <string-array name="bitcoin_wallets">
<item>de.schildbach.wallet</item>
<item>de.schildbach.wallet_test</item>
</string-array>
<string name="default_bitcoin_wallet">de.schildbach.wallet</string>
</resources>

View file

@ -52,6 +52,11 @@
<string name="toast_contact_added">Contact added</string> <string name="toast_contact_added">Contact added</string>
<!-- ChatFragment -->
<string name="bitcoin_wallet_not_found">Bitcoin wallet not found. Please install wallet app or select another wallet in the app settings.</string>
<!-- SettingsActivity --> <!-- SettingsActivity -->
<!-- Activity title --> <!-- Activity title -->
@ -72,6 +77,14 @@
<!-- Preference title --> <!-- Preference title -->
<string name="notification_sounds">Notification Sounds</string> <string name="notification_sounds">Notification Sounds</string>
<string name="bitcoin_wallet">Bitcoin Wallet Application</string>
<!-- List of supported wallet apps -->
<string-array name="bitcoin_wallet_values">
<item>Bitcoin Wallet</item>
<item>Bitcoin Wallet for Testnet</item>
</string-array>
<!-- Preference title (debug only)--> <!-- Preference title (debug only)-->
<string name="max_connections" translatable="false">Maximum Number of Connections</string> <string name="max_connections" translatable="false">Maximum Number of Connections</string>

View file

@ -22,6 +22,13 @@
android:inputType="number" android:inputType="number"
android:numeric="integer" /> android:numeric="integer" />
<ListPreference
android:title="@string/bitcoin_wallet"
android:key="bitcoin_wallet"
android:entries="@array/bitcoin_wallet_values"
android:entryValues="@array/bitcoin_wallets"
android:defaultValue="@string/default_bitcoin_wallet" />
<EditTextPreference <EditTextPreference
android:title="@string/max_connections" android:title="@string/max_connections"
android:key="max_connections" android:key="max_connections"
@ -39,6 +46,7 @@
<Preference <Preference
android:title="@string/version" android:title="@string/version"
android:key="version" /> android:key="version"
style="?android:preferenceInformationStyle" />
</PreferenceScreen> </PreferenceScreen>

View file

@ -5,10 +5,10 @@ import java.io._
import android.bluetooth.{BluetoothDevice, BluetoothSocket} import android.bluetooth.{BluetoothDevice, BluetoothSocket}
import android.content.{IntentFilter, Intent, Context, BroadcastReceiver} import android.content.{IntentFilter, Intent, Context, BroadcastReceiver}
import android.util.Log import android.util.Log
import com.nutomic.ensichat.protocol.Message.ReadMessageException
import com.nutomic.ensichat.protocol._ import com.nutomic.ensichat.protocol._
import com.nutomic.ensichat.protocol.body.ConnectionInfo import com.nutomic.ensichat.protocol.body.ConnectionInfo
import com.nutomic.ensichat.protocol.header.MessageHeader import com.nutomic.ensichat.protocol.header.MessageHeader
import Message.ReadMessageException
/** /**
* Transfers data between connnected devices. * Transfers data between connnected devices.

View file

@ -1,8 +1,9 @@
package com.nutomic.ensichat.fragments package com.nutomic.ensichat.fragments
import android.app.ListFragment import android.app.{Activity, ListFragment}
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter} import android.content.{ActivityNotFoundException, BroadcastReceiver, Context, Intent, IntentFilter}
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v4.content.LocalBroadcastManager import android.support.v4.content.LocalBroadcastManager
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
@ -11,15 +12,19 @@ import android.widget.TextView.OnEditorActionListener
import android.widget._ import android.widget._
import com.nutomic.ensichat.R import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.EnsichatActivity import com.nutomic.ensichat.activities.EnsichatActivity
import com.nutomic.ensichat.protocol.body.Text import com.nutomic.ensichat.protocol.body.{InitiatePayment, PaymentInformation, Text}
import com.nutomic.ensichat.protocol.header.ContentHeader
import com.nutomic.ensichat.protocol.{Address, ChatService, Message} import com.nutomic.ensichat.protocol.{Address, ChatService, Message}
import com.nutomic.ensichat.util.Database import com.nutomic.ensichat.util.Database
import com.nutomic.ensichat.views.{DatesAdapter, MessagesAdapter} import com.nutomic.ensichat.views.{DatesAdapter, MessagesAdapter}
import de.schildbach.wallet.integration.android.BitcoinIntegration
/** /**
* Represents a single chat with another specific device. * Represents a single chat with another specific device.
*/ */
class ChatFragment extends ListFragment with OnClickListener { class ChatFragment extends ListFragment with OnClickListener with OnEditorActionListener {
private val REQUEST_FETCH_PAYMENT_REQUEST = 1
/** /**
* Fragments need to have a default constructor, so this is optional. * Fragments need to have a default constructor, so this is optional.
@ -35,6 +40,8 @@ class ChatFragment extends ListFragment with OnClickListener {
private var chatService: ChatService = _ private var chatService: ChatService = _
private var sendBitcoinButton: ImageButton = _
private var sendButton: Button = _ private var sendButton: Button = _
private var messageText: EditText = _ private var messageText: EditText = _
@ -55,6 +62,8 @@ class ChatFragment extends ListFragment with OnClickListener {
adapter = new DatesAdapter(getActivity, adapter = new DatesAdapter(getActivity,
new MessagesAdapter(getActivity, database.getMessagesCursor(address, None), address)) new MessagesAdapter(getActivity, database.getMessagesCursor(address, None), address))
// TODO: mark messages read
if (listView != null) { if (listView != null) {
listView.setAdapter(adapter) listView.setAdapter(adapter)
} }
@ -63,21 +72,17 @@ class ChatFragment extends ListFragment with OnClickListener {
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
savedInstanceState: Bundle): View = { savedInstanceState: Bundle): View = {
val view: View = inflater.inflate(R.layout.fragment_chat, container, false) val view = inflater.inflate(R.layout.fragment_chat, container, false)
sendButton = view.findViewById(R.id.send).asInstanceOf[Button] sendBitcoinButton = view.findViewById(R.id.send_bitcoin).asInstanceOf[ImageButton]
sendButton = view.findViewById(R.id.send).asInstanceOf[Button]
messageText = view.findViewById(R.id.message).asInstanceOf[EditText]
listView = view.findViewById(android.R.id.list).asInstanceOf[ListView]
sendBitcoinButton.setOnClickListener(this)
sendButton.setOnClickListener(this) sendButton.setOnClickListener(this)
messageText = view.findViewById(R.id.message).asInstanceOf[EditText] messageText.setOnEditorActionListener(this)
messageText.setOnEditorActionListener(new OnEditorActionListener {
override def onEditorAction(view: TextView, actionId: Int, event: KeyEvent): Boolean = {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onClick(sendButton)
true
} else
false
}
})
listView = view.findViewById(android.R.id.list).asInstanceOf[ListView]
listView.setAdapter(adapter) listView.setAdapter(adapter)
view view
} }
@ -101,14 +106,24 @@ class ChatFragment extends ListFragment with OnClickListener {
LocalBroadcastManager.getInstance(getActivity).unregisterReceiver(onMessageReceivedReceiver) LocalBroadcastManager.getInstance(getActivity).unregisterReceiver(onMessageReceivedReceiver)
} }
override def onEditorAction(view: TextView, actionId: Int, event: KeyEvent): Boolean = {
if (actionId == EditorInfo.IME_ACTION_DONE) {
onClick(sendButton)
true
} else
false
}
/** /**
* Send message if send button was clicked. * Send message if send button was clicked.
*/ */
override def onClick(view: View): Unit = view.getId match { override def onClick(view: View): Unit = view.getId match {
case R.id.send_bitcoin =>
chatService.sendTo(address, new InitiatePayment())
case R.id.send => case R.id.send =>
val text = messageText.getText.toString.trim val text = messageText.getText.toString.trim
if (!text.isEmpty) { if (!text.isEmpty) {
val message = new Text(text.toString) val message = new Text(text)
chatService.sendTo(address, message) chatService.sendTo(address, message)
messageText.getText.clear() messageText.getText.clear()
} }
@ -123,12 +138,48 @@ class ChatFragment extends ListFragment with OnClickListener {
if (!Set(msg.header.origin, msg.header.target).contains(address)) if (!Set(msg.header.origin, msg.header.target).contains(address))
return return
val types: Set[Class[_]] =
Set(classOf[Text], classOf[InitiatePayment], classOf[PaymentInformation])
if (!types.contains(msg.body.getClass))
return
val header = msg.header.asInstanceOf[ContentHeader]
if (msg.header.origin != address || header.read)
return
database.setMessageRead(header)
adapter.changeCursor(database.getMessagesCursor(address, None))
// Special handling for Bitcoin messages.
// TODO: is this stuff working from background?
msg.body match { msg.body match {
case _: Text => case _: Text =>
adapter.changeCursor(database.getMessagesCursor(address, None)) case _: InitiatePayment =>
case _ => val pm = PreferenceManager.getDefaultSharedPreferences(getActivity)
val wallet = pm.getString(SettingsFragment.KeyBitcoinWallet,
getString(R.string.default_bitcoin_wallet))
val intent = new Intent()
intent.setClassName(wallet, "de.schildbach.wallet.ui.FetchPaymentRequestActivity")
intent.putExtra("sender_name", chatService.getUser(msg.header.origin).name)
try {
startActivityForResult(intent, REQUEST_FETCH_PAYMENT_REQUEST)
} catch {
case e: ActivityNotFoundException =>
Toast.makeText(getActivity, R.string.bitcoin_wallet_not_found, Toast.LENGTH_LONG).show();
}
case pr: PaymentInformation =>
BitcoinIntegration.request(getActivity, pr.bytes)
} }
} }
} }
override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = requestCode match {
case REQUEST_FETCH_PAYMENT_REQUEST =>
if (resultCode == Activity.RESULT_OK) {
val pr = new PaymentInformation(data.getByteArrayExtra("payment_request"))
chatService.sendTo(address, pr)
}
}
} }

View file

@ -16,6 +16,7 @@ object SettingsFragment {
val KeyUserName = "user_name" val KeyUserName = "user_name"
val KeyUserStatus = "user_status" val KeyUserStatus = "user_status"
val KeyScanInterval = "scan_interval_seconds" val KeyScanInterval = "scan_interval_seconds"
val KeyBitcoinWallet = "bitcoin_wallet"
val MaxConnections = "max_connections" val MaxConnections = "max_connections"
val Version = "version" val Version = "version"
@ -31,6 +32,7 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
private lazy val name = findPreference(KeyUserName) private lazy val name = findPreference(KeyUserName)
private lazy val status = findPreference(KeyUserStatus) private lazy val status = findPreference(KeyUserStatus)
private lazy val scanInterval = findPreference(KeyScanInterval) private lazy val scanInterval = findPreference(KeyScanInterval)
private lazy val bitcoinWallet = findPreference(KeyBitcoinWallet)
private lazy val maxConnections = findPreference(MaxConnections) private lazy val maxConnections = findPreference(MaxConnections)
private lazy val version = findPreference(Version) private lazy val version = findPreference(Version)
@ -50,6 +52,10 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
scanInterval.setSummary(prefs.getString( scanInterval.setSummary(prefs.getString(
KeyScanInterval, getResources.getString(R.string.default_scan_interval))) KeyScanInterval, getResources.getString(R.string.default_scan_interval)))
bitcoinWallet.setOnPreferenceChangeListener(this)
bitcoinWallet.setSummary(prefs.getString(KeyBitcoinWallet,
getResources.getString(R.string.default_bitcoin_wallet)))
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
maxConnections.setOnPreferenceChangeListener(this) maxConnections.setOnPreferenceChangeListener(this)
maxConnections.setSummary(prefs.getString( maxConnections.setSummary(prefs.getString(
@ -70,6 +76,7 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
val service = getActivity.asInstanceOf[EnsichatActivity].service val service = getActivity.asInstanceOf[EnsichatActivity].service
val ui = new UserInfo(prefs.getString(KeyUserName, ""), prefs.getString(KeyUserStatus, "")) val ui = new UserInfo(prefs.getString(KeyUserName, ""), prefs.getString(KeyUserStatus, ""))
database.getContacts.foreach(c => service.sendTo(c.address, ui)) database.getContacts.foreach(c => service.sendTo(c.address, ui))
case _ => // TODO: this correct? (check before rebase)
} }
preference.setSummary(newValue.toString) preference.setSummary(newValue.toString)
true true

View file

@ -13,7 +13,7 @@ import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.MainActivity import com.nutomic.ensichat.activities.MainActivity
import com.nutomic.ensichat.bluetooth.BluetoothInterface import com.nutomic.ensichat.bluetooth.BluetoothInterface
import com.nutomic.ensichat.fragments.SettingsFragment import com.nutomic.ensichat.fragments.SettingsFragment
import com.nutomic.ensichat.protocol.body.{ConnectionInfo, MessageBody, UserInfo} import com.nutomic.ensichat.protocol.body.{ConnectionInfo, MessageBody, UserInfo, _}
import com.nutomic.ensichat.protocol.header.ContentHeader import com.nutomic.ensichat.protocol.header.ContentHeader
import com.nutomic.ensichat.util.{Database, FutureHelper, NotificationHandler} import com.nutomic.ensichat.util.{Database, FutureHelper, NotificationHandler}
@ -119,7 +119,7 @@ class ChatService extends Service {
FutureHelper { FutureHelper {
val messageId = preferences.getLong("message_id", 0) val messageId = preferences.getLong("message_id", 0)
val header = new ContentHeader(crypto.localAddress, target, seqNumGenerator.next(), val header = new ContentHeader(crypto.localAddress, target, seqNumGenerator.next(),
body.contentType, Some(messageId), Some(new Date())) body.contentType, Some(messageId), Some(new Date()), true)
preferences.edit().putLong("message_id", messageId + 1) preferences.edit().putLong("message_id", messageId + 1)
val msg = new Message(header, body) val msg = new Message(header, body)
@ -151,25 +151,28 @@ class ChatService extends Service {
/** /**
* Handles all (locally and remotely sent) new messages. * Handles all (locally and remotely sent) new messages.
*/ */
private def onNewMessage(msg: Message): Unit = msg.body match { private def onNewMessage(msg: Message): Unit = {
case ui: UserInfo =>
val contact = new User(msg.header.origin, ui.name, ui.status)
knownUsers += contact
if (database.getContact(msg.header.origin).nonEmpty)
database.updateContact(contact)
callConnectionListeners() // FIXME: looks like we completely broke message sending
case _ =>
val origin = msg.header.origin
if (origin != crypto.localAddress && database.getContact(origin).isEmpty)
database.addContact(getUser(origin))
database.onMessageReceived(msg) msg.body match {
notificationHandler.onMessageReceived(msg) case ui: UserInfo =>
val i = new Intent(ChatService.ActionMessageReceived) val contact = new User(msg.header.origin, ui.name, ui.status)
i.putExtra(ChatService.ExtraMessage, msg) knownUsers += contact
LocalBroadcastManager.getInstance(this) if (database.getContact(msg.header.origin).nonEmpty)
.sendBroadcast(i) database.updateContact(contact)
case _: InitiatePayment | _: PaymentInformation | _: Text =>
val origin = msg.header.origin
if (origin != crypto.localAddress && database.getContact(origin).isEmpty)
database.addContact(getUser(origin))
val i = new Intent(ChatService.ActionMessageReceived)
i.putExtra(ChatService.ExtraMessage, msg)
LocalBroadcastManager.getInstance(this)
.sendBroadcast(i)
case _: ConnectionInfo =>
callConnectionListeners()
}
} }
/** /**

View file

@ -254,8 +254,10 @@ class Crypto(context: Context) {
symmetricCipher.init(Cipher.DECRYPT_MODE, key) symmetricCipher.init(Cipher.DECRYPT_MODE, key)
val decrypted = copyThroughCipher(symmetricCipher, msg.body.asInstanceOf[EncryptedBody].data) val decrypted = copyThroughCipher(symmetricCipher, msg.body.asInstanceOf[EncryptedBody].data)
val body = msg.header.asInstanceOf[ContentHeader].contentType match { val body = msg.header.asInstanceOf[ContentHeader].contentType match {
case Text.Type => Text.read(decrypted) case Text.Type => Text.read(decrypted)
case UserInfo.Type => UserInfo.read(decrypted) case UserInfo.Type => UserInfo.read(decrypted)
case InitiatePayment.Type => InitiatePayment.read(decrypted)
case PaymentInformation.Type => PaymentInformation.read(decrypted)
} }
new Message(msg.header, msg.crypto, body) new Message(msg.header, msg.crypto, body)
} }

View file

@ -1,6 +1,6 @@
package com.nutomic.ensichat.protocol package com.nutomic.ensichat.protocol
import com.nutomic.ensichat.protocol.header.{MessageHeader, ContentHeader} import com.nutomic.ensichat.protocol.header.{ContentHeader, MessageHeader}
/** /**
* Forwards messages to all connected devices. * Forwards messages to all connected devices.
@ -30,7 +30,7 @@ class Router(activeConnections: () => Set[Address], send: (Address, Message) =>
private def incHopCount(msg: Message): Message = { private def incHopCount(msg: Message): Message = {
val updatedHeader = msg.header match { val updatedHeader = msg.header match {
case ch: ContentHeader => new ContentHeader(ch.origin, ch.target, ch.seqNum, ch.contentType, case ch: ContentHeader => new ContentHeader(ch.origin, ch.target, ch.seqNum, ch.contentType,
ch.messageId, ch.time, ch.hopCount + 1, ch.hopLimit) ch.messageId, ch.time, ch.read, ch.hopCount + 1, ch.hopLimit)
case mh: MessageHeader => new MessageHeader(mh.protocolType, mh.origin, mh.target, mh.seqNum, case mh: MessageHeader => new MessageHeader(mh.protocolType, mh.origin, mh.target, mh.seqNum,
mh.hopCount + 1, mh.hopLimit) mh.hopCount + 1, mh.hopLimit)
} }

View file

@ -0,0 +1,34 @@
package com.nutomic.ensichat.protocol.body
import java.nio.ByteBuffer
object InitiatePayment {
val Type = 5
/**
* Constructs [[InitiatePayment]] instance from byte array.
*/
def read(array: Array[Byte]): InitiatePayment = {
new InitiatePayment()
}
}
/**
* Sent to initiate a bitcoin payment. This should get a [[PaymentInformation]] as a response.
*/
case class InitiatePayment() extends MessageBody {
override def protocolType = -1
override def contentType = InitiatePayment.Type
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
b.array()
}
override def length = 4
}

View file

@ -0,0 +1,53 @@
package com.nutomic.ensichat.protocol.body
import java.nio.ByteBuffer
import java.util
import com.nutomic.ensichat.util.BufferUtils
object PaymentInformation {
val Type = 6
/**
* Constructs [[PaymentInformation]] instance from byte array.
*/
def read(array: Array[Byte]): PaymentInformation = {
val b = ByteBuffer.wrap(array)
val length = BufferUtils.getUnsignedInt(b).toInt
val bytes = new Array[Byte](length)
b.get(bytes, 0, length)
new PaymentInformation(bytes)
}
}
/**
* Contains bitcoin payment information so bitcoins can be sent to the origin of this message.
*
* @param bytes Protobuf-formatted Bitcoin payment message.
*/
case class PaymentInformation(bytes: Array[Byte])
extends MessageBody {
override def protocolType = -1
override def contentType = PaymentInformation.Type
override def write: Array[Byte] = {
val b = ByteBuffer.allocate(length)
BufferUtils.putUnsignedInt(b, bytes.length)
b.put(bytes)
b.array()
}
override def length = 4 + bytes.length
override def equals(a: Any): Boolean = a match {
case o: PaymentInformation => util.Arrays.equals(bytes, o.bytes)
case _ => false
}
override def toString = BufferUtils.toString(bytes)
}

View file

@ -7,7 +7,7 @@ import com.nutomic.ensichat.util.BufferUtils
object Text { object Text {
val Type = 6 val Type = 3
/** /**
* Constructs [[Text]] instance from byte array. * Constructs [[Text]] instance from byte array.

View file

@ -25,7 +25,7 @@ object ContentHeader {
val time = BufferUtils.getUnsignedInt(b) val time = BufferUtils.getUnsignedInt(b)
val ch = new ContentHeader(mh.origin, mh.target, mh.seqNum, contentType, Some(messageId), val ch = new ContentHeader(mh.origin, mh.target, mh.seqNum, contentType, Some(messageId),
Some(new Date(time * 1000)), mh.hopCount) Some(new Date(time * 1000)), false, mh.hopCount)
val remaining = new Array[Byte](b.remaining()) val remaining = new Array[Byte](b.remaining())
b.get(remaining, 0, b.remaining()) b.get(remaining, 0, b.remaining())
@ -38,6 +38,8 @@ object ContentHeader {
* Header for user-sent messages. * Header for user-sent messages.
* *
* This is [[AbstractHeader]] with messageId and time fields set. * This is [[AbstractHeader]] with messageId and time fields set.
*
* @param read Specifies if the message was read by the local user. Never transmitted.
*/ */
case class ContentHeader(override val origin: Address, case class ContentHeader(override val origin: Address,
override val target: Address, override val target: Address,
@ -45,6 +47,7 @@ case class ContentHeader(override val origin: Address,
contentType: Int, contentType: Int,
override val messageId: Some[Long], override val messageId: Some[Long],
override val time: Some[Date], override val time: Some[Date],
read: Boolean,
override val hopCount: Int = 0, override val hopCount: Int = 0,
override val hopLimit: Int = AbstractHeader.DefaultHopLimit) override val hopLimit: Int = AbstractHeader.DefaultHopLimit)
extends AbstractHeader { extends AbstractHeader {
@ -73,7 +76,8 @@ case class ContentHeader(override val origin: Address,
super.equals(a) && super.equals(a) &&
contentType == o.contentType && contentType == o.contentType &&
messageId == o.messageId && messageId == o.messageId &&
time.get.getTime / 1000 == o.time.get.getTime / 1000 time.get.getTime / 1000 == o.time.get.getTime / 1000 &&
read == o.read
case _ => false case _ => false
} }

View file

@ -2,8 +2,8 @@ package com.nutomic.ensichat.protocol.header
import java.nio.ByteBuffer import java.nio.ByteBuffer
import com.nutomic.ensichat.protocol.{Message, Address} import com.nutomic.ensichat.protocol.Message.ParseMessageException
import Message.ParseMessageException import com.nutomic.ensichat.protocol.{Address, Message}
import com.nutomic.ensichat.util.BufferUtils import com.nutomic.ensichat.util.BufferUtils
object MessageHeader { object MessageHeader {

View file

@ -7,7 +7,7 @@ import android.database.Cursor
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper} import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
import android.support.v4.content.LocalBroadcastManager import android.support.v4.content.LocalBroadcastManager
import com.nutomic.ensichat.protocol._ import com.nutomic.ensichat.protocol._
import com.nutomic.ensichat.protocol.body.Text import com.nutomic.ensichat.protocol.body.{Text, _}
import com.nutomic.ensichat.protocol.header.ContentHeader import com.nutomic.ensichat.protocol.header.ContentHeader
import scala.collection.SortedSet import scala.collection.SortedSet
@ -24,28 +24,43 @@ object Database {
// NOTE: We could make origin/target foreign keys to contacts, but: // NOTE: We could make origin/target foreign keys to contacts, but:
// - they don't change anyway // - they don't change anyway
// - we'd have to insert the local user into contacts // - we'd have to insert the local user into contacts
private val CreateMessagesTable = "CREATE TABLE messages(" + private val CreateTableMessages = "CREATE TABLE messages(" +
"_id INTEGER PRIMARY KEY," + "_id INTEGER PRIMARY KEY," +
"origin TEXT NOT NULL," + "origin TEXT NOT NULL," +
"target TEXT NOT NULL," + "target TEXT NOT NULL," +
"message_id INT NOT NULL," + "message_id INT NOT NULL," +
"text TEXT NOT NULL," + "type INT NOT NULL," +
"date INT NOT NULL);" // Unix timestamp "date INT NOT NULL," + // Unix timestamp
"read INT NOT NULL);"
private val CreateContactsTable = "CREATE TABLE contacts(" + private val CreateTableTexts = "CREATE TABLE texts(" +
"_id INTEGER PRIMARY KEY," +
"message_id INT," +
"text TEXT NOT NULL," +
"FOREIGN KEY (message_id) REFERENCES messages(_id))"
private val CreateTablePaymentRequests = "CREATE TABLE payment_requests(" +
"_id INTEGER PRIMARY KEY," +
"message_id INT," +
"bytes TEXT NOT NULL," +
"FOREIGN KEY (message_id) REFERENCES messages(_id))"
private val CreateTableContacts = "CREATE TABLE contacts(" +
"_id INTEGER PRIMARY KEY," + "_id INTEGER PRIMARY KEY," +
"address TEXT NOT NULL," + "address TEXT NOT NULL," +
"name TEXT NOT NULL," + "name TEXT NOT NULL," +
"status TEXT NOT NULL)" "status TEXT NOT NULL)"
def messageFromCursor(c: Cursor): Message = { // TODO: only used for fragment right now, merge with other code (and make general method for all msgs)
def textMessageFromCursor(c: Cursor): Message = {
val header = new ContentHeader(new Address( val header = new ContentHeader(new Address(
c.getString(c.getColumnIndex("origin"))), c.getString(c.getColumnIndex("origin"))),
new Address(c.getString(c.getColumnIndex("target"))), new Address(c.getString(c.getColumnIndex("target"))),
-1, -1,
Text.Type, Text.Type,
Some(c.getLong(c.getColumnIndex("message_id"))), Some(c.getLong(c.getColumnIndex("message_id"))),
Some(new Date(c.getLong(c.getColumnIndex("date"))))) Some(new Date(c.getLong(c.getColumnIndex("date")))),
c.getInt(c.getColumnIndex("read")) == 1)
val body = new Text(new String(c.getString(c.getColumnIndex ("text")))) val body = new Text(new String(c.getString(c.getColumnIndex ("text"))))
new Message(header, body) new Message(header, body)
} }
@ -58,14 +73,20 @@ object Database {
class Database(context: Context) class Database(context: Context)
extends SQLiteOpenHelper(context, Database.DatabaseName, null, Database.DatabaseVersion) { extends SQLiteOpenHelper(context, Database.DatabaseName, null, Database.DatabaseVersion) {
override def onConfigure(db: SQLiteDatabase): Unit = {
db.execSQL("PRAGMA foreign_keys = ON;")
}
override def onCreate(db: SQLiteDatabase): Unit = { override def onCreate(db: SQLiteDatabase): Unit = {
db.execSQL(Database.CreateContactsTable) db.execSQL(Database.CreateTableMessages)
db.execSQL(Database.CreateMessagesTable) db.execSQL(Database.CreateTableTexts)
db.execSQL(Database.CreateTablePaymentRequests)
db.execSQL(Database.CreateTableContacts)
} }
def getMessagesCursor(address: Address, count: Option[Int]): Cursor = { def getMessagesCursor(address: Address, count: Option[Int]): Cursor = {
getReadableDatabase.query(true, getReadableDatabase.query(true, "messages",
"messages", Array("_id", "origin", "target", "message_id", "text", "date"), Array("_id", "origin", "target", "type", "message_id", "date", "read"),
"origin = ? OR target = ?", Array(address.toString, address.toString), "origin = ? OR target = ?", Array(address.toString, address.toString),
null, null, "date ASC", count.map(_.toString).orNull) null, null, "date ASC", count.map(_.toString).orNull)
} }
@ -77,7 +98,31 @@ class Database(context: Context)
val c = getMessagesCursor(address, Option(count)) val c = getMessagesCursor(address, Option(count))
var messages = new TreeSet[Message]()(Message.Ordering) var messages = new TreeSet[Message]()(Message.Ordering)
while (c.moveToNext()) { while (c.moveToNext()) {
messages += Database.messageFromCursor(c) val header = new ContentHeader(
new Address(c.getString(c.getColumnIndex("origin"))),
new Address(c.getString(c.getColumnIndex("target"))),
-1,
c.getInt(c.getColumnIndex("type")),
Some(c.getLong(c.getColumnIndex("message_id"))),
Some(new Date(c.getLong(c.getColumnIndex("date")))),
c.getInt(c.getColumnIndex("read")) == 1)
val id = c.getString(c.getColumnIndex("_id"))
val body = header.contentType match {
case Text.Type =>
val c2 = getReadableDatabase.query(
"texts", Array("text"), "message_id=?", Array(id), null, null, null)
c2.moveToFirst()
new Text(new String(c2.getString(c2.getColumnIndex ("text"))))
case InitiatePayment.Type =>
new InitiatePayment()
case PaymentInformation.Type =>
val c2 = getReadableDatabase.query(
"payment_requests", Array("bytes"), "message_id=?", Array(id), null, null, null)
c2.moveToFirst()
new PaymentInformation(c2.getBlob(c2.getColumnIndex("bytes")))
}
messages += new Message(header, body)
} }
c.close() c.close()
messages messages
@ -86,16 +131,46 @@ class Database(context: Context)
/** /**
* Inserts the given new message into the database. * Inserts the given new message into the database.
*/ */
def onMessageReceived(msg: Message): Unit = msg.body match { def onMessageReceived(msg: Message): Unit = {
case text: Text => // Only certain types of messages are stored.
val cv = new ContentValues() val types: Set[Class[_]] =
cv.put("origin", msg.header.origin.toString) Set(classOf[Text], classOf[InitiatePayment], classOf[PaymentInformation])
cv.put("target", msg.header.target.toString) if (!types.contains(msg.body.getClass))
// Need to use [[Long#toString]] because of https://issues.scala-lang.org/browse/SI-2991 return
cv.put("message_id", msg.header.messageId.get.toString)
cv.put("date", msg.header.time.get.getTime.toString) val header = msg.header.asInstanceOf[ContentHeader]
cv.put("text", text.text) val cv = new ContentValues()
getWritableDatabase.insert("messages", null, cv) cv.put("origin", header.origin.toString)
cv.put("target", header.target.toString)
// Need to use [[Long#toString]] because of https://issues.scala-lang.org/browse/SI-2991
cv.put("message_id", header.messageId.toString)
cv.put("type", header.contentType.toString)
cv.put("date", header.time.get.getTime.toString)
cv.put("read", header.read)
val id = getWritableDatabase.insert("messages", null, cv)
val cvExtra = new ContentValues()
cvExtra.put("message_id", id.toString)
msg.body match {
case text: Text =>
cvExtra.put("text", text.text)
getWritableDatabase.insert("texts", null, cvExtra)
case pr: PaymentInformation =>
cvExtra.put("bytes", pr.bytes)
getWritableDatabase.insert("payment_requests", null, cvExtra)
case _: InitiatePayment =>
}
}
/**
* Marks the message as read by the user.
*/
def setMessageRead(header: ContentHeader): Unit = {
val cv = new ContentValues()
cv.put("read", "1")
getReadableDatabase.update("messages", cv, "origin=? AND message_id=?",
Array(header.origin.toString, header.messageId.toString))
} }
/** /**

View file

@ -1,11 +1,11 @@
package com.nutomic.ensichat.util package com.nutomic.ensichat.util
import android.os.{Looper, Handler} import android.os.{Handler, Looper}
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
/** /**
* Wraps [[Future]], so that exceptions are always thrown. * Use this instead of [[Future]], to make sure exceptions are logged.
* *
* @see https://github.com/saturday06/gradle-android-scala-plugin/issues/56 * @see https://github.com/saturday06/gradle-android-scala-plugin/issues/56
*/ */

View file

@ -7,6 +7,7 @@ import android.support.v4.app.NotificationCompat
import com.nutomic.ensichat.R import com.nutomic.ensichat.R
import com.nutomic.ensichat.activities.MainActivity import com.nutomic.ensichat.activities.MainActivity
import com.nutomic.ensichat.protocol.body.Text import com.nutomic.ensichat.protocol.body.Text
import com.nutomic.ensichat.protocol.body.{InitiatePayment, PaymentInformation, Text}
import com.nutomic.ensichat.protocol.{Crypto, Message} import com.nutomic.ensichat.protocol.{Crypto, Message}
object NotificationHandler { object NotificationHandler {
@ -22,25 +23,32 @@ object NotificationHandler {
*/ */
class NotificationHandler(context: Context) { class NotificationHandler(context: Context) {
def onMessageReceived(msg: Message): Unit = msg.body match { def onMessageReceived(msg: Message): Unit = {
case text: Text => if (msg.header.origin == new Crypto(context).localAddress)
if (msg.header.origin == new Crypto(context).localAddress) return
return
val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0) showNotification(msg.body match {
val notification = new NotificationCompat.Builder(context) case t: Text => t.text
.setSmallIcon(R.drawable.ic_launcher) case _: InitiatePayment => "InitiatePayment"
.setContentTitle(context.getString(R.string.notification_message)) case _: PaymentInformation => "PaymentInformation"
.setContentText(text.text) case _ => return
.setDefaults(defaults()) })
.setContentIntent(pi) }
.setAutoCancel(true)
.build()
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) private def showNotification(text: String): Unit = {
.asInstanceOf[NotificationManager] val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
nm.notify(NotificationHandler.NotificationIdNewMessage, notification) val notification = new NotificationCompat.Builder(context)
case _ => .setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(context.getString(R.string.notification_message))
.setContentText(text)
.setDefaults(defaults())
.setDefaults(defaults())
.setContentIntent(pi)
.setAutoCancel(true)
.build()
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE)
.asInstanceOf[NotificationManager]
nm.notify(NotificationHandler.NotificationIdNewMessage, notification)
} }
/** /**

View file

@ -17,7 +17,7 @@ object DatesAdapter {
override def getSectionTitleForItem(item: Cursor): String = { override def getSectionTitleForItem(item: Cursor): String = {
DateFormat DateFormat
.getDateInstance(DateFormat.MEDIUM) .getDateInstance(DateFormat.MEDIUM)
.format(Database.messageFromCursor(item).header.time.get) .format(Database.textMessageFromCursor(item).header.time.get)
} }
} }

View file

@ -52,6 +52,6 @@ class MessagesAdapter(context: Context, cursor: Cursor, remoteAddress: Address)
} }
}) })
override def getInstance (cursor: Cursor) = Database.messageFromCursor(cursor) override def getInstance (cursor: Cursor) = Database.textMessageFromCursor(cursor)
} }