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
|
||||
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 'com.google.guava:guava:18.0'
|
||||
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.
|
||||
|
|
|
@ -90,15 +90,16 @@ class RouterTest extends AndroidTestCase {
|
|||
}
|
||||
|
||||
def testHopLimit(): Unit = Range(19, 22).foreach { i =>
|
||||
val msg = new Message(
|
||||
new ContentHeader(AddressTest.a1, AddressTest.a2, 1, 1, Some(1), Some(new Date()), i), new Text(""))
|
||||
val header =
|
||||
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())
|
||||
router.onReceive(msg)
|
||||
}
|
||||
|
||||
private def generateMessage(sender: Address, receiver: Address, seqNum: Int): Message = {
|
||||
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("", ""))
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ class ConnectionInfoTest extends AndroidTestCase {
|
|||
val ci = ConnectionInfoTest.generateCi(getContext)
|
||||
val bytes = ci.write
|
||||
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
|
||||
|
||||
import java.util.{GregorianCalendar, Date}
|
||||
import java.util.{Date, GregorianCalendar}
|
||||
|
||||
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 junit.framework.Assert._
|
||||
|
||||
object ContentHeaderTest {
|
||||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.MessageTest._
|
||||
import com.nutomic.ensichat.protocol.body.CryptoData
|
||||
import com.nutomic.ensichat.protocol.header.{ContentHeader, ContentHeaderTest}
|
||||
import com.nutomic.ensichat.protocol.body.{CryptoData, PaymentInformation}
|
||||
import com.nutomic.ensichat.protocol.header.ContentHeader
|
||||
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._
|
||||
|
||||
object DatabaseTest {
|
||||
|
@ -68,18 +68,33 @@ class DatabaseTest extends AndroidTestCase {
|
|||
assertTrue(msg.contains(m3))
|
||||
}
|
||||
|
||||
def testMessageFields(): Unit = {
|
||||
val msg = database.getMessages(m2.header.target, 1).firstKey
|
||||
def testTextMessage(): Unit = {
|
||||
val msg = database.getMessages(m3.header.target, 1).firstKey
|
||||
val header = msg.header.asInstanceOf[ContentHeader]
|
||||
|
||||
assertEquals(h2.origin, header.origin)
|
||||
assertEquals(h2.target, header.target)
|
||||
assertEquals(-1, msg.header.seqNum)
|
||||
assertEquals(h2.contentType, header.contentType)
|
||||
assertEquals(h2.messageId, header.messageId)
|
||||
assertEquals(h2.time, header.time)
|
||||
assertEquals(h3.contentType, header.contentType)
|
||||
assertEquals(h3.messageId, header.messageId)
|
||||
assertEquals(h3.time, header.time)
|
||||
assertEquals(h3.read, header.read)
|
||||
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 = {
|
||||
|
|
|
@ -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: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
|
||||
android:id="@+id/message"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -3,10 +3,17 @@
|
|||
|
||||
<string name="default_user_status">Let\'s chat!</string>
|
||||
|
||||
<bool name="default_notification_sounds">true</bool>
|
||||
|
||||
<string name="default_scan_interval">15</string>
|
||||
|
||||
<bool name="default_notification_sounds">true</bool>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
<!-- Activity title -->
|
||||
|
@ -72,6 +77,14 @@
|
|||
<!-- Preference title -->
|
||||
<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)-->
|
||||
<string name="max_connections" translatable="false">Maximum Number of Connections</string>
|
||||
|
||||
|
|
|
@ -22,6 +22,13 @@
|
|||
android:inputType="number"
|
||||
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
|
||||
android:title="@string/max_connections"
|
||||
android:key="max_connections"
|
||||
|
@ -39,6 +46,7 @@
|
|||
|
||||
<Preference
|
||||
android:title="@string/version"
|
||||
android:key="version" />
|
||||
android:key="version"
|
||||
style="?android:preferenceInformationStyle" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
|
|
@ -5,10 +5,10 @@ import java.io._
|
|||
import android.bluetooth.{BluetoothDevice, BluetoothSocket}
|
||||
import android.content.{IntentFilter, Intent, Context, BroadcastReceiver}
|
||||
import android.util.Log
|
||||
import com.nutomic.ensichat.protocol.Message.ReadMessageException
|
||||
import com.nutomic.ensichat.protocol._
|
||||
import com.nutomic.ensichat.protocol.body.ConnectionInfo
|
||||
import com.nutomic.ensichat.protocol.header.MessageHeader
|
||||
import Message.ReadMessageException
|
||||
|
||||
/**
|
||||
* Transfers data between connnected devices.
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package com.nutomic.ensichat.fragments
|
||||
|
||||
import android.app.ListFragment
|
||||
import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.app.{Activity, ListFragment}
|
||||
import android.content.{ActivityNotFoundException, BroadcastReceiver, Context, Intent, IntentFilter}
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.inputmethod.EditorInfo
|
||||
|
@ -11,15 +12,19 @@ import android.widget.TextView.OnEditorActionListener
|
|||
import android.widget._
|
||||
import com.nutomic.ensichat.R
|
||||
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.util.Database
|
||||
import com.nutomic.ensichat.views.{DatesAdapter, MessagesAdapter}
|
||||
import de.schildbach.wallet.integration.android.BitcoinIntegration
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -35,6 +40,8 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
|
||||
private var chatService: ChatService = _
|
||||
|
||||
private var sendBitcoinButton: ImageButton = _
|
||||
|
||||
private var sendButton: Button = _
|
||||
|
||||
private var messageText: EditText = _
|
||||
|
@ -55,6 +62,8 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
adapter = new DatesAdapter(getActivity,
|
||||
new MessagesAdapter(getActivity, database.getMessagesCursor(address, None), address))
|
||||
|
||||
// TODO: mark messages read
|
||||
|
||||
if (listView != null) {
|
||||
listView.setAdapter(adapter)
|
||||
}
|
||||
|
@ -63,21 +72,17 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
|
||||
override def onCreateView(inflater: LayoutInflater, container: ViewGroup,
|
||||
savedInstanceState: Bundle): View = {
|
||||
val view: View = inflater.inflate(R.layout.fragment_chat, container, false)
|
||||
sendButton = view.findViewById(R.id.send).asInstanceOf[Button]
|
||||
val view = inflater.inflate(R.layout.fragment_chat, container, false)
|
||||
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)
|
||||
messageText = view.findViewById(R.id.message).asInstanceOf[EditText]
|
||||
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]
|
||||
messageText.setOnEditorActionListener(this)
|
||||
listView.setAdapter(adapter)
|
||||
|
||||
view
|
||||
}
|
||||
|
||||
|
@ -101,14 +106,24 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
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.
|
||||
*/
|
||||
override def onClick(view: View): Unit = view.getId match {
|
||||
case R.id.send_bitcoin =>
|
||||
chatService.sendTo(address, new InitiatePayment())
|
||||
case R.id.send =>
|
||||
val text = messageText.getText.toString.trim
|
||||
if (!text.isEmpty) {
|
||||
val message = new Text(text.toString)
|
||||
val message = new Text(text)
|
||||
chatService.sendTo(address, message)
|
||||
messageText.getText.clear()
|
||||
}
|
||||
|
@ -123,12 +138,48 @@ class ChatFragment extends ListFragment with OnClickListener {
|
|||
if (!Set(msg.header.origin, msg.header.target).contains(address))
|
||||
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 {
|
||||
case _: Text =>
|
||||
adapter.changeCursor(database.getMessagesCursor(address, None))
|
||||
case _ =>
|
||||
case _: InitiatePayment =>
|
||||
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 KeyUserStatus = "user_status"
|
||||
val KeyScanInterval = "scan_interval_seconds"
|
||||
val KeyBitcoinWallet = "bitcoin_wallet"
|
||||
val MaxConnections = "max_connections"
|
||||
val Version = "version"
|
||||
|
||||
|
@ -31,6 +32,7 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
|
|||
private lazy val name = findPreference(KeyUserName)
|
||||
private lazy val status = findPreference(KeyUserStatus)
|
||||
private lazy val scanInterval = findPreference(KeyScanInterval)
|
||||
private lazy val bitcoinWallet = findPreference(KeyBitcoinWallet)
|
||||
private lazy val maxConnections = findPreference(MaxConnections)
|
||||
private lazy val version = findPreference(Version)
|
||||
|
||||
|
@ -50,6 +52,10 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
|
|||
scanInterval.setSummary(prefs.getString(
|
||||
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) {
|
||||
maxConnections.setOnPreferenceChangeListener(this)
|
||||
maxConnections.setSummary(prefs.getString(
|
||||
|
@ -70,6 +76,7 @@ class SettingsFragment extends PreferenceFragment with OnPreferenceChangeListene
|
|||
val service = getActivity.asInstanceOf[EnsichatActivity].service
|
||||
val ui = new UserInfo(prefs.getString(KeyUserName, ""), prefs.getString(KeyUserStatus, ""))
|
||||
database.getContacts.foreach(c => service.sendTo(c.address, ui))
|
||||
case _ => // TODO: this correct? (check before rebase)
|
||||
}
|
||||
preference.setSummary(newValue.toString)
|
||||
true
|
||||
|
|
|
@ -13,7 +13,7 @@ import com.nutomic.ensichat.R
|
|||
import com.nutomic.ensichat.activities.MainActivity
|
||||
import com.nutomic.ensichat.bluetooth.BluetoothInterface
|
||||
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.util.{Database, FutureHelper, NotificationHandler}
|
||||
|
||||
|
@ -119,7 +119,7 @@ class ChatService extends Service {
|
|||
FutureHelper {
|
||||
val messageId = preferences.getLong("message_id", 0)
|
||||
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)
|
||||
|
||||
val msg = new Message(header, body)
|
||||
|
@ -151,25 +151,28 @@ class ChatService extends Service {
|
|||
/**
|
||||
* Handles all (locally and remotely sent) new messages.
|
||||
*/
|
||||
private def onNewMessage(msg: Message): Unit = msg.body match {
|
||||
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)
|
||||
private def onNewMessage(msg: Message): Unit = {
|
||||
|
||||
callConnectionListeners()
|
||||
case _ =>
|
||||
val origin = msg.header.origin
|
||||
if (origin != crypto.localAddress && database.getContact(origin).isEmpty)
|
||||
database.addContact(getUser(origin))
|
||||
// FIXME: looks like we completely broke message sending
|
||||
|
||||
database.onMessageReceived(msg)
|
||||
notificationHandler.onMessageReceived(msg)
|
||||
val i = new Intent(ChatService.ActionMessageReceived)
|
||||
i.putExtra(ChatService.ExtraMessage, msg)
|
||||
LocalBroadcastManager.getInstance(this)
|
||||
.sendBroadcast(i)
|
||||
msg.body match {
|
||||
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)
|
||||
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)
|
||||
val decrypted = copyThroughCipher(symmetricCipher, msg.body.asInstanceOf[EncryptedBody].data)
|
||||
val body = msg.header.asInstanceOf[ContentHeader].contentType match {
|
||||
case Text.Type => Text.read(decrypted)
|
||||
case UserInfo.Type => UserInfo.read(decrypted)
|
||||
case Text.Type => Text.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)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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.
|
||||
|
@ -30,7 +30,7 @@ class Router(activeConnections: () => Set[Address], send: (Address, Message) =>
|
|||
private def incHopCount(msg: Message): Message = {
|
||||
val updatedHeader = msg.header match {
|
||||
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,
|
||||
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 {
|
||||
|
||||
val Type = 6
|
||||
val Type = 3
|
||||
|
||||
/**
|
||||
* Constructs [[Text]] instance from byte array.
|
||||
|
|
|
@ -25,7 +25,7 @@ object ContentHeader {
|
|||
val time = BufferUtils.getUnsignedInt(b)
|
||||
|
||||
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())
|
||||
b.get(remaining, 0, b.remaining())
|
||||
|
@ -38,6 +38,8 @@ object ContentHeader {
|
|||
* Header for user-sent messages.
|
||||
*
|
||||
* 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,
|
||||
override val target: Address,
|
||||
|
@ -45,6 +47,7 @@ case class ContentHeader(override val origin: Address,
|
|||
contentType: Int,
|
||||
override val messageId: Some[Long],
|
||||
override val time: Some[Date],
|
||||
read: Boolean,
|
||||
override val hopCount: Int = 0,
|
||||
override val hopLimit: Int = AbstractHeader.DefaultHopLimit)
|
||||
extends AbstractHeader {
|
||||
|
@ -73,7 +76,8 @@ case class ContentHeader(override val origin: Address,
|
|||
super.equals(a) &&
|
||||
contentType == o.contentType &&
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ package com.nutomic.ensichat.protocol.header
|
|||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
import com.nutomic.ensichat.protocol.{Message, Address}
|
||||
import Message.ParseMessageException
|
||||
import com.nutomic.ensichat.protocol.Message.ParseMessageException
|
||||
import com.nutomic.ensichat.protocol.{Address, Message}
|
||||
import com.nutomic.ensichat.util.BufferUtils
|
||||
|
||||
object MessageHeader {
|
||||
|
|
|
@ -7,7 +7,7 @@ import android.database.Cursor
|
|||
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
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 scala.collection.SortedSet
|
||||
|
@ -24,28 +24,43 @@ object Database {
|
|||
// NOTE: We could make origin/target foreign keys to contacts, but:
|
||||
// - they don't change anyway
|
||||
// - 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," +
|
||||
"origin TEXT NOT NULL," +
|
||||
"target TEXT NOT NULL," +
|
||||
"message_id INT NOT NULL," +
|
||||
"text TEXT NOT NULL," +
|
||||
"date INT NOT NULL);" // Unix timestamp
|
||||
"type INT NOT NULL," +
|
||||
"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," +
|
||||
"address TEXT NOT NULL," +
|
||||
"name 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(
|
||||
c.getString(c.getColumnIndex("origin"))),
|
||||
new Address(c.getString(c.getColumnIndex("target"))),
|
||||
-1,
|
||||
Text.Type,
|
||||
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"))))
|
||||
new Message(header, body)
|
||||
}
|
||||
|
@ -58,14 +73,20 @@ object Database {
|
|||
class Database(context: Context)
|
||||
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 = {
|
||||
db.execSQL(Database.CreateContactsTable)
|
||||
db.execSQL(Database.CreateMessagesTable)
|
||||
db.execSQL(Database.CreateTableMessages)
|
||||
db.execSQL(Database.CreateTableTexts)
|
||||
db.execSQL(Database.CreateTablePaymentRequests)
|
||||
db.execSQL(Database.CreateTableContacts)
|
||||
}
|
||||
|
||||
def getMessagesCursor(address: Address, count: Option[Int]): Cursor = {
|
||||
getReadableDatabase.query(true,
|
||||
"messages", Array("_id", "origin", "target", "message_id", "text", "date"),
|
||||
getReadableDatabase.query(true, "messages",
|
||||
Array("_id", "origin", "target", "type", "message_id", "date", "read"),
|
||||
"origin = ? OR target = ?", Array(address.toString, address.toString),
|
||||
null, null, "date ASC", count.map(_.toString).orNull)
|
||||
}
|
||||
|
@ -77,7 +98,31 @@ class Database(context: Context)
|
|||
val c = getMessagesCursor(address, Option(count))
|
||||
var messages = new TreeSet[Message]()(Message.Ordering)
|
||||
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()
|
||||
messages
|
||||
|
@ -86,16 +131,46 @@ class Database(context: Context)
|
|||
/**
|
||||
* Inserts the given new message into the database.
|
||||
*/
|
||||
def onMessageReceived(msg: Message): Unit = msg.body match {
|
||||
case text: Text =>
|
||||
val cv = new ContentValues()
|
||||
cv.put("origin", msg.header.origin.toString)
|
||||
cv.put("target", msg.header.target.toString)
|
||||
// Need to use [[Long#toString]] because of https://issues.scala-lang.org/browse/SI-2991
|
||||
cv.put("message_id", msg.header.messageId.get.toString)
|
||||
cv.put("date", msg.header.time.get.getTime.toString)
|
||||
cv.put("text", text.text)
|
||||
getWritableDatabase.insert("messages", null, cv)
|
||||
def onMessageReceived(msg: Message): Unit = {
|
||||
// Only certain types of messages are stored.
|
||||
val types: Set[Class[_]] =
|
||||
Set(classOf[Text], classOf[InitiatePayment], classOf[PaymentInformation])
|
||||
if (!types.contains(msg.body.getClass))
|
||||
return
|
||||
|
||||
val header = msg.header.asInstanceOf[ContentHeader]
|
||||
val cv = new ContentValues()
|
||||
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
|
||||
|
||||
import android.os.{Looper, Handler}
|
||||
import android.os.{Handler, Looper}
|
||||
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.support.v4.app.NotificationCompat
|
|||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.MainActivity
|
||||
import com.nutomic.ensichat.protocol.body.Text
|
||||
import com.nutomic.ensichat.protocol.body.{InitiatePayment, PaymentInformation, Text}
|
||||
import com.nutomic.ensichat.protocol.{Crypto, Message}
|
||||
|
||||
object NotificationHandler {
|
||||
|
@ -22,25 +23,32 @@ object NotificationHandler {
|
|||
*/
|
||||
class NotificationHandler(context: Context) {
|
||||
|
||||
def onMessageReceived(msg: Message): Unit = msg.body match {
|
||||
case text: Text =>
|
||||
if (msg.header.origin == new Crypto(context).localAddress)
|
||||
return
|
||||
def onMessageReceived(msg: Message): Unit = {
|
||||
if (msg.header.origin == new Crypto(context).localAddress)
|
||||
return
|
||||
|
||||
val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
|
||||
val notification = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentTitle(context.getString(R.string.notification_message))
|
||||
.setContentText(text.text)
|
||||
.setDefaults(defaults())
|
||||
.setContentIntent(pi)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
showNotification(msg.body match {
|
||||
case t: Text => t.text
|
||||
case _: InitiatePayment => "InitiatePayment"
|
||||
case _: PaymentInformation => "PaymentInformation"
|
||||
case _ => return
|
||||
})
|
||||
}
|
||||
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
.asInstanceOf[NotificationManager]
|
||||
nm.notify(NotificationHandler.NotificationIdNewMessage, notification)
|
||||
case _ =>
|
||||
private def showNotification(text: String): Unit = {
|
||||
val pi = PendingIntent.getActivity(context, 0, new Intent(context, classOf[MainActivity]), 0)
|
||||
val notification = new NotificationCompat.Builder(context)
|
||||
.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 = {
|
||||
DateFormat
|
||||
.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