Notifications could still use some fine tuning, but should work fine for now
@ -6,6 +6,8 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
@ -29,6 +29,9 @@ public class Identicon extends Drawable {
|
|||||||
private static final int CENTER_COLUMN = 5;
|
private static final int CENTER_COLUMN = 5;
|
||||||
|
|
||||||
private final Paint paint;
|
private final Paint paint;
|
||||||
|
private float width;
|
||||||
|
private float height;
|
||||||
|
|
||||||
private float cellWidth;
|
private float cellWidth;
|
||||||
private float cellHeight;
|
private float cellHeight;
|
||||||
private byte[] hash;
|
private byte[] hash;
|
||||||
@ -54,15 +57,11 @@ public class Identicon extends Drawable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected byte getByte(int index) {
|
|
||||||
return hash[index % hash.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(Canvas canvas) {
|
public void draw(Canvas canvas) {
|
||||||
float x, y;
|
float x, y;
|
||||||
paint.setColor(background);
|
paint.setColor(background);
|
||||||
canvas.drawPaint(paint);
|
canvas.drawCircle(width/2, height/2, width/2, paint);
|
||||||
paint.setColor(color);
|
paint.setColor(color);
|
||||||
for (int row = 0; row < SIZE; row++) {
|
for (int row = 0; row < SIZE; row++) {
|
||||||
for (int column = 0; column < SIZE; column++) {
|
for (int column = 0; column < SIZE; column++) {
|
||||||
@ -88,13 +87,16 @@ public class Identicon extends Drawable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getOpacity() {
|
public int getOpacity() {
|
||||||
return PixelFormat.OPAQUE;
|
return PixelFormat.TRANSPARENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onBoundsChange(Rect bounds) {
|
protected void onBoundsChange(Rect bounds) {
|
||||||
super.onBoundsChange(bounds);
|
super.onBoundsChange(bounds);
|
||||||
|
|
||||||
|
width = bounds.width();
|
||||||
|
height = bounds.height();
|
||||||
|
|
||||||
cellWidth = bounds.width() / (float) SIZE;
|
cellWidth = bounds.width() / (float) SIZE;
|
||||||
cellHeight = bounds.height() / (float) SIZE;
|
cellHeight = bounds.height() / (float) SIZE;
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,8 @@ import java.util.ArrayList;
|
|||||||
*/
|
*/
|
||||||
public class MessageListActivity extends AppCompatActivity
|
public class MessageListActivity extends AppCompatActivity
|
||||||
implements MessageListFragment.Callbacks {
|
implements MessageListFragment.Callbacks {
|
||||||
public static final String EXTRA_SHOW_MESSAGE = "show_message";
|
public static final String EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage";
|
||||||
|
public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox";
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(MessageListActivity.class);
|
private static final Logger LOG = LoggerFactory.getLogger(MessageListActivity.class);
|
||||||
private static final int ADD_IDENTITY = 1;
|
private static final int ADD_IDENTITY = 1;
|
||||||
@ -95,7 +96,12 @@ public class MessageListActivity extends AppCompatActivity
|
|||||||
|
|
||||||
createDrawer(toolbar);
|
createDrawer(toolbar);
|
||||||
|
|
||||||
// TODO: If exposing deep links into your app, handle intents here.
|
Singleton.getMessageListener(this).resetNotification();
|
||||||
|
|
||||||
|
// handle intents
|
||||||
|
if (getIntent().hasExtra(EXTRA_SHOW_MESSAGE)) {
|
||||||
|
onItemSelected((Plaintext) getIntent().getSerializableExtra(EXTRA_SHOW_MESSAGE));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createDrawer(Toolbar toolbar) {
|
private void createDrawer(Toolbar toolbar) {
|
||||||
@ -103,6 +109,7 @@ public class MessageListActivity extends AppCompatActivity
|
|||||||
for (BitmessageAddress identity : bmc.addresses().getIdentities()) {
|
for (BitmessageAddress identity : bmc.addresses().getIdentities()) {
|
||||||
LOG.info("Adding identity " + identity.getAddress());
|
LOG.info("Adding identity " + identity.getAddress());
|
||||||
profiles.add(new ProfileDrawerItem()
|
profiles.add(new ProfileDrawerItem()
|
||||||
|
.withIcon(new Identicon(identity))
|
||||||
.withName(identity.toString())
|
.withName(identity.toString())
|
||||||
.withEmail(identity.getAddress())
|
.withEmail(identity.getAddress())
|
||||||
.withTag(identity)
|
.withTag(identity)
|
||||||
@ -173,6 +180,7 @@ public class MessageListActivity extends AppCompatActivity
|
|||||||
selectedLabel = (Label) item.getTag();
|
selectedLabel = (Label) item.getTag();
|
||||||
((MessageListFragment) getSupportFragmentManager()
|
((MessageListFragment) getSupportFragmentManager()
|
||||||
.findFragmentById(R.id.message_list)).updateList(selectedLabel);
|
.findFragmentById(R.id.message_list)).updateList(selectedLabel);
|
||||||
|
return true;
|
||||||
} else if (item instanceof Nameable<?>) {
|
} else if (item instanceof Nameable<?>) {
|
||||||
Nameable<?> ni = (Nameable<?>) item;
|
Nameable<?> ni = (Nameable<?>) item;
|
||||||
switch (ni.getNameRes()) {
|
switch (ni.getNameRes()) {
|
||||||
@ -237,7 +245,6 @@ public class MessageListActivity extends AppCompatActivity
|
|||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.replace(R.id.message_detail_container, fragment)
|
.replace(R.id.message_detail_container, fragment)
|
||||||
.commit();
|
.commit();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// In single-pane mode, simply start the detail activity
|
// In single-pane mode, simply start the detail activity
|
||||||
// for the selected item ID.
|
// for the selected item ID.
|
||||||
|
@ -82,6 +82,11 @@ public class MessageListFragment extends ListFragment {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
bmc = Singleton.getBitmessageContext(getActivity());
|
bmc = Singleton.getBitmessageContext(getActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
updateList(((MessageListActivity) getActivity()).getSelectedLabel());
|
updateList(((MessageListActivity) getActivity()).getSelectedLabel());
|
||||||
}
|
}
|
||||||
|
@ -16,54 +16,134 @@
|
|||||||
|
|
||||||
package ch.dissem.apps.abit;
|
package ch.dissem.apps.abit;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.ContactsContract;
|
||||||
import android.support.v7.app.NotificationCompat;
|
import android.support.v7.app.NotificationCompat;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
import ch.dissem.bitmessage.BitmessageContext;
|
import ch.dissem.bitmessage.BitmessageContext;
|
||||||
import ch.dissem.bitmessage.entity.Plaintext;
|
import ch.dissem.bitmessage.entity.Plaintext;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by chris on 22.08.15.
|
* Listens for decrypted Bitmessage messages. Does show a notification.
|
||||||
|
* <p>
|
||||||
|
* Should show a notification when the app isn't running, but update the message list when it is. Also,
|
||||||
|
* notifications should be combined.
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
public class MessageListener implements BitmessageContext.Listener {
|
public class MessageListener implements BitmessageContext.Listener {
|
||||||
|
private static final StyleSpan SPAN_EMPHASIS = new StyleSpan(Typeface.BOLD);
|
||||||
private final Context ctx;
|
private final Context ctx;
|
||||||
private final NotificationManager manager;
|
private final NotificationManager manager;
|
||||||
|
private final LinkedList<Plaintext> unacknowledged = new LinkedList<>();
|
||||||
|
private final int pictureSize;
|
||||||
|
private int numberOfUnacknowledgedMessages = 0;
|
||||||
|
|
||||||
public MessageListener(Context ctx) {
|
public MessageListener(Context ctx) {
|
||||||
this.ctx = ctx.getApplicationContext();
|
this.ctx = ctx.getApplicationContext();
|
||||||
this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
|
this.manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
this.pictureSize = getMaxContactPhotoSize(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||||
|
public static int getMaxContactPhotoSize(final Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
|
// Note that this URI is safe to call on the UI thread.
|
||||||
|
final Uri uri = ContactsContract.DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI;
|
||||||
|
final String[] projection = new String[]{ContactsContract.DisplayPhoto.DISPLAY_MAX_DIM};
|
||||||
|
final Cursor c = context.getContentResolver().query(uri, projection, null, null, null);
|
||||||
|
try {
|
||||||
|
c.moveToFirst();
|
||||||
|
return c.getInt(0);
|
||||||
|
} finally {
|
||||||
|
c.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback: 96x96 is the max contact photo size for pre-ICS versions
|
||||||
|
return 96;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive(final Plaintext plaintext) {
|
public void receive(final Plaintext plaintext) {
|
||||||
// TODO
|
synchronized (unacknowledged) {
|
||||||
// ctx.runOnUiThread(new Runnable() {
|
unacknowledged.addFirst(plaintext);
|
||||||
// @Override
|
numberOfUnacknowledgedMessages++;
|
||||||
// public void run() {
|
if (unacknowledged.size() > 5) {
|
||||||
|
unacknowledged.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx);
|
||||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
if (numberOfUnacknowledgedMessages == 1) {
|
||||||
.setContentTitle(plaintext.getFrom().toString())
|
Spannable bigText = new SpannableString(plaintext.getSubject() + "\n" + plaintext.getText());
|
||||||
.setContentText(plaintext.getSubject());
|
bigText.setSpan(SPAN_EMPHASIS, 0, plaintext.getSubject().length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||||
|
.setLargeIcon(toBitmap(new Identicon(plaintext.getFrom())))
|
||||||
|
.setContentTitle(plaintext.getFrom().toString())
|
||||||
|
.setContentText(plaintext.getSubject())
|
||||||
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText))
|
||||||
|
.setContentInfo("Info");
|
||||||
|
|
||||||
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
Intent showMessageIntent = new Intent(ctx, MessageListActivity.class);
|
||||||
inboxStyle.setBigContentTitle(plaintext.getFrom().toString());
|
showMessageIntent.putExtra(MessageListActivity.EXTRA_SHOW_MESSAGE, plaintext);
|
||||||
inboxStyle.setSummaryText(plaintext.getSubject());
|
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, showMessageIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
String text = plaintext.getText();
|
builder.setContentIntent(pendingIntent);
|
||||||
if (text.length() > 100)
|
|
||||||
inboxStyle.addLine(text.substring(0, 100) + "…");
|
|
||||||
else
|
|
||||||
inboxStyle.addLine(text);
|
|
||||||
builder.setStyle(inboxStyle);
|
|
||||||
|
|
||||||
Intent intent = new Intent(ctx, MessageListActivity.class);
|
builder.addAction(R.drawable.ic_action_reply, ctx.getString(R.string.reply), pendingIntent);
|
||||||
intent.putExtra(MessageListActivity.EXTRA_SHOW_MESSAGE, plaintext);
|
builder.addAction(R.drawable.ic_action_delete, ctx.getString(R.string.delete), pendingIntent);
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, intent, 0);
|
} else {
|
||||||
builder.setContentIntent(pendingIntent);
|
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||||
|
.setContentTitle(ctx.getString(R.string.n_new_messages, this.unacknowledged.size()))
|
||||||
|
.setContentText(ctx.getString(R.string.app_name));
|
||||||
|
|
||||||
|
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
||||||
|
synchronized (unacknowledged) {
|
||||||
|
inboxStyle.setBigContentTitle(ctx.getString(R.string.n_new_messages, numberOfUnacknowledgedMessages));
|
||||||
|
for (Plaintext msg : unacknowledged) {
|
||||||
|
Spannable sb = new SpannableString(msg.getFrom() + " " + msg.getSubject());
|
||||||
|
sb.setSpan(SPAN_EMPHASIS, 0, String.valueOf(msg.getFrom()).length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
inboxStyle.addLine(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.setStyle(inboxStyle);
|
||||||
|
|
||||||
|
Intent intent = new Intent(ctx, MessageListActivity.class);
|
||||||
|
intent.setAction(MessageListActivity.ACTION_SHOW_INBOX);
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 1, intent, 0);
|
||||||
|
builder.setContentIntent(pendingIntent);
|
||||||
|
}
|
||||||
|
|
||||||
manager.notify(0, builder.build());
|
manager.notify(0, builder.build());
|
||||||
// }
|
}
|
||||||
// });
|
|
||||||
|
private Bitmap toBitmap(Identicon identicon) {
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(pictureSize, pictureSize, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(bitmap);
|
||||||
|
identicon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||||
|
identicon.draw(canvas);
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetNotification() {
|
||||||
|
manager.cancel(0);
|
||||||
|
synchronized (unacknowledged) {
|
||||||
|
unacknowledged.clear();
|
||||||
|
numberOfUnacknowledgedMessages = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package ch.dissem.apps.abit.repositories;
|
|||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteConstraintException;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import ch.dissem.apps.abit.R;
|
import ch.dissem.apps.abit.R;
|
||||||
import ch.dissem.bitmessage.InternalContext;
|
import ch.dissem.bitmessage.InternalContext;
|
||||||
@ -151,6 +152,24 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countUnread(Label label) {
|
||||||
|
String where;
|
||||||
|
if (label != null) {
|
||||||
|
where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ") AND ";
|
||||||
|
} else {
|
||||||
|
where = "";
|
||||||
|
}
|
||||||
|
SQLiteDatabase db = sql.getReadableDatabase();
|
||||||
|
Cursor c = db.query(
|
||||||
|
TABLE_NAME, new String[]{COLUMN_ID},
|
||||||
|
where + "id IN (SELECT message_id FROM Message_Label WHERE label_id IN (" +
|
||||||
|
"SELECT id FROM Label WHERE type = '" + Label.Type.UNREAD.name() + "'))",
|
||||||
|
null, null, null, null
|
||||||
|
);
|
||||||
|
return c.getColumnCount();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Plaintext> findMessages(Label label) {
|
public List<Plaintext> findMessages(Label label) {
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
@ -258,6 +277,8 @@ public class AndroidMessageRepository implements MessageRepository, InternalCont
|
|||||||
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
|
db.insertOrThrow(JOIN_TABLE_NAME, null, values);
|
||||||
}
|
}
|
||||||
db.setTransactionSuccessful();
|
db.setTransactionSuccessful();
|
||||||
|
} catch (SQLiteConstraintException e) {
|
||||||
|
LOG.trace(e.getMessage(), e);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error(e.getMessage(), e);
|
LOG.error(e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
|
BIN
app/src/main/res/drawable-hdpi/ic_action_delete.png
Normal file
After Width: | Height: | Size: 642 B |
BIN
app/src/main/res/drawable-hdpi/ic_action_reply.png
Normal file
After Width: | Height: | Size: 486 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_delete.png
Normal file
After Width: | Height: | Size: 428 B |
BIN
app/src/main/res/drawable-mdpi/ic_action_reply.png
Normal file
After Width: | Height: | Size: 330 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_delete.png
Normal file
After Width: | Height: | Size: 775 B |
BIN
app/src/main/res/drawable-xhdpi/ic_action_reply.png
Normal file
After Width: | Height: | Size: 623 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_delete.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_action_reply.png
Normal file
After Width: | Height: | Size: 929 B |
@ -2,18 +2,21 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
xmlns:fab="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:id="@id/android:list"
|
android:id="@id/android:list"
|
||||||
|
|
||||||
|
android:paddingBottom="88dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:scrollbarStyle="outsideOverlay"
|
||||||
|
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
android:layout_alignParentLeft="true"
|
android:layout_alignParentLeft="true"
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentBottom="true" />
|
android:layout_alignParentBottom="true"/>
|
||||||
|
|
||||||
<android.support.design.widget.FloatingActionButton
|
<android.support.design.widget.FloatingActionButton
|
||||||
android:id="@+id/fab_compose_message"
|
android:id="@+id/fab_compose_message"
|
||||||
|
@ -24,4 +24,7 @@
|
|||||||
<string name="do_import">Import</string>
|
<string name="do_import">Import</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="broadcast">Broadcast</string>
|
<string name="broadcast">Broadcast</string>
|
||||||
|
<string name="n_new_messages">%d new messages</string>
|
||||||
|
<string name="reply">Reply</string>
|
||||||
|
<string name="delete">Delete</string>
|
||||||
</resources>
|
</resources>
|
||||||
|