Allow sending bitcoins in chat.
This commit is contained in:
parent
284a4ceb02
commit
c350ac12e0
32 changed files with 736 additions and 118 deletions
26
PROTOCOL.md
26
PROTOCOL.md
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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("", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/main/res/drawable-hdpi/ic_bitcoin.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_bitcoin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 957 B |
BIN
app/src/main/res/drawable-mdpi/ic_bitcoin.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_bitcoin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 635 B |
BIN
app/src/main/res/drawable-xhdpi/ic_bitcoin.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_bitcoin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_bitcoin.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_bitcoin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue