From 37371a0e94f7dbb7edc7a46068f3e7f5a8d348b3 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Fri, 27 Jan 2017 08:25:40 +0100 Subject: [PATCH 01/18] Moved dependency so it isn't groupt with the test dependencies --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c78c327..61e6175 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -70,10 +70,10 @@ dependencies { transitive = true } compile 'com.github.angads25:filepicker:1.0.6' + compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' - compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' } idea.module { From 42cf18445c0674c0c38e954596cd69f019151d1a Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Sat, 18 Mar 2017 07:09:03 +0100 Subject: [PATCH 02/18] Bumped Jabit version to prepare for conversations --- app/build.gradle | 20 ++--- .../V4.0__Create_table_message_parent.sql | 11 +++ .../ch/dissem/apps/abit/MainActivity.java | 31 ++++--- .../ch/dissem/apps/abit/SettingsFragment.java | 6 +- .../repository/AndroidMessageRepository.java | 87 ++++++++++++++++++- .../abit/repository/AndroidNodeRegistry.java | 7 ++ .../apps/abit/repository/SqlHelper.java | 8 +- .../ch/dissem/apps/abit/util/UuidUtils.java | 37 ++++++++ build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 10 files changed, 176 insertions(+), 37 deletions(-) create mode 100644 app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java diff --git a/app/build.gradle b/app/build.gradle index 61e6175..aec9574 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,9 +40,9 @@ android { ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.1.0' - compile 'com.android.support:support-v4:25.1.0' - compile 'com.android.support:design:25.1.0' + compile 'com.android.support:appcompat-v7:25.3.0' + compile 'com.android.support:support-v4:25.3.0' + compile 'com.android.support:design:25.3.0' compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -52,25 +52,25 @@ dependencies { compile 'org.slf4j:slf4j-android:1.7.12' - compile 'com.mikepenz:materialize:1.0.0@aar' - compile('com.mikepenz:materialdrawer:5.6.0@aar') { + compile 'com.mikepenz:materialize:1.0.1@aar' + compile('com.mikepenz:materialdrawer:5.8.2@aar') { transitive = true } - compile('com.mikepenz:aboutlibraries:5.8.1@aar') { + compile('com.mikepenz:aboutlibraries:5.9.4@aar') { transitive = true } compile 'com.mikepenz:iconics:1.6.2@aar' - compile 'com.mikepenz:community-material-typeface:1.5.54.2@aar' + compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' compile 'com.google.zxing:core:3.3.0' compile 'io.github.yavski:fab-speed-dial:1.0.6' compile 'com.github.amlcurran.showcaseview:library:5.4.3' - compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.9.3@aar') { + compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true } - compile 'com.github.angads25:filepicker:1.0.6' - compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' + compile 'com.github.angads25:filepicker:1.0.9' + compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' diff --git a/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql b/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql new file mode 100644 index 0000000..855140a --- /dev/null +++ b/app/src/main/assets/db/migration/V4.0__Create_table_message_parent.sql @@ -0,0 +1,11 @@ +ALTER TABLE Message ADD COLUMN conversation BINARY[16]; + +CREATE TABLE Message_Parent ( + parent BINARY(64) NOT NULL, + child BINARY(64) NOT NULL, + pos INT NOT NULL, + conversation BINARY[16] NOT NULL, + + PRIMARY KEY (parent, child), + FOREIGN KEY (child) REFERENCES Message (iv) +); diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index c994207..fc45f75 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -55,6 +55,7 @@ import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; @@ -311,7 +312,15 @@ public class MainActivity extends AppCompatActivity public boolean onItemClick(View view, int position, IDrawerItem item) { if (item.getTag() instanceof Label) { selectedLabel = (Label) item.getTag(); - showSelectedLabel(); + if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof + MessageListFragment) { + ((MessageListFragment) getSupportFragmentManager() + .findFragmentById(R.id.item_list)).updateList(selectedLabel); + } else { + MessageListFragment listFragment = new MessageListFragment(); + changeList(listFragment); + listFragment.updateList(selectedLabel); + } return false; } else if (item instanceof Nameable) { Nameable ni = (Nameable) item; @@ -374,7 +383,7 @@ public class MainActivity extends AppCompatActivity for (Label label : labels) { addLabelEntry(label); } - showSelectedLabel(); + drawer.setSelection(drawer.getDrawerItem(selectedLabel)); } }.execute(); } @@ -389,7 +398,9 @@ public class MainActivity extends AppCompatActivity @SuppressWarnings("unchecked") protected void onRestoreInstanceState(Bundle savedInstanceState) { selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel"); - showSelectedLabel(); + + drawer.setSelection(drawer.getDrawerItem(selectedLabel)); + super.onRestoreInstanceState(savedInstanceState); } @@ -439,7 +450,7 @@ public class MainActivity extends AppCompatActivity item.withIcon(CommunityMaterial.Icon.cmd_file); break; case OUTBOX: - item.withIcon(CommunityMaterial.Icon.cmd_outbox); + item.withIcon(CommunityMaterial.Icon.cmd_inbox_arrow_up); break; case SENT: item.withIcon(CommunityMaterial.Icon.cmd_send); @@ -521,18 +532,6 @@ public class MainActivity extends AppCompatActivity } } - private void showSelectedLabel() { - if (getSupportFragmentManager().findFragmentById(R.id.item_list) instanceof - MessageListFragment) { - ((MessageListFragment) getSupportFragmentManager() - .findFragmentById(R.id.item_list)).updateList(selectedLabel); - } else { - MessageListFragment listFragment = new MessageListFragment(); - changeList(listFragment); - listFragment.updateList(selectedLabel); - } - } - /** * Callback method from {@link ListSelectionListener} * indicating that the item with the given ID was selected. diff --git a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java index 6879025..111bf08 100644 --- a/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/SettingsFragment.java @@ -29,8 +29,10 @@ import android.widget.Toast; import com.mikepenz.aboutlibraries.Libs; import com.mikepenz.aboutlibraries.LibsBuilder; +import ch.dissem.apps.abit.repository.AndroidNodeRegistry; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.bitmessage.BitmessageContext; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW; import static ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE; @@ -78,7 +80,9 @@ public class SettingsFragment @Override protected Void doInBackground(Void... voids) { - Singleton.getBitmessageContext(ctx).cleanup(); + BitmessageContext bmc = Singleton.getBitmessageContext(ctx); + bmc.cleanup(); + bmc.internals().getNodeRegistry().clear(); return null; } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 40bc9c6..3f7c8c1 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -31,8 +31,10 @@ import java.io.IOException; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.UUID; import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.util.UuidUtils; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -40,6 +42,8 @@ import ch.dissem.bitmessage.ports.AbstractMessageRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.utils.Encode; +import static ch.dissem.apps.abit.util.UuidUtils.asUuid; +import static ch.dissem.bitmessage.utils.Strings.hex; import static java.lang.String.valueOf; /** @@ -65,6 +69,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private static final String COLUMN_RETRIES = "retries"; private static final String COLUMN_NEXT_TRY = "next_try"; private static final String COLUMN_INITIAL_HASH = "initial_hash"; + private static final String COLUMN_CONVERSATION = "conversation"; + + private static final String PARENTS_TABLE_NAME = "Message_Parent"; private static final String JOIN_TABLE_NAME = "Message_Label"; private static final String JT_COLUMN_MESSAGE = "message_id"; @@ -164,7 +171,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { public int countUnread(Label label) { String[] args; String where; - if (label == null){ + if (label == null) { return 0; } if (label == LABEL_ARCHIVE) { @@ -187,6 +194,72 @@ public class AndroidMessageRepository extends AbstractMessageRepository { ); } + @Override + public List findConversations(Label label) { + String[] projection = { + COLUMN_CONVERSATION, + }; + + String where; + if (label == null) { + where = "id NOT IN (SELECT message_id FROM Message_Label)"; + } else { + where = "id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.getId() + ")"; + } + List result = new LinkedList<>(); + SQLiteDatabase db = sql.getReadableDatabase(); + try (Cursor c = db.query( + TABLE_NAME, projection, + where, + null, null, null, null + )) { + while (c.moveToNext()) { + byte[] uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)); + result.add(asUuid(uuidBytes)); + } + } + return result; + } + + + private void updateParents(SQLiteDatabase db, Plaintext message) { + if (message.getInventoryVector() == null || message.getParents().isEmpty()) { + // There are no parents to save yet (they are saved in the extended data, that's enough for now) + return; + } + db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(message.getInitialHash()).toString()}); + + byte[] childIV = message.getInventoryVector().getHash(); + // save new parents + int order = 0; + for (InventoryVector parentIV : message.getParents()) { + Plaintext parent = getMessage(parentIV); + mergeConversations(db, parent.getConversationId(), message.getConversationId()); + order++; + ContentValues values = new ContentValues(); + values.put("parent", parentIV.getHash()); + values.put("child", childIV); + values.put("pos", order); + values.put("conversation", UuidUtils.asBytes(message.getConversationId())); + db.insertOrThrow(PARENTS_TABLE_NAME, null, values); + } + } + + /** + * Replaces every occurrence of the source conversation ID with the target ID + * + * @param db is used to keep everything within one transaction + * @param source ID of the conversation to be merged + * @param target ID of the merge target + */ + private void mergeConversations(SQLiteDatabase db, UUID source, UUID target) { + ContentValues values = new ContentValues(); + values.put("conversation", UuidUtils.asBytes(target)); + String[] whereArgs = {hex(UuidUtils.asBytes(source)).toString()}; + db.update(TABLE_NAME, values, "conversation=?", whereArgs); + db.update(PARENTS_TABLE_NAME, values, "conversation=?", whereArgs); + } + protected List find(String where) { List<Plaintext> result = new LinkedList<>(); @@ -205,7 +278,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository { COLUMN_STATUS, COLUMN_TTL, COLUMN_RETRIES, - COLUMN_NEXT_TRY + COLUMN_NEXT_TRY, + COLUMN_CONVERSATION }; SQLiteDatabase db = sql.getReadableDatabase(); @@ -220,8 +294,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository { byte[] data = c.getBlob(c.getColumnIndex(COLUMN_DATA)); Plaintext.Type type = Plaintext.Type.valueOf(c.getString(c.getColumnIndex (COLUMN_TYPE))); - Plaintext.Builder builder = Plaintext.readWithoutSignature(type, new - ByteArrayInputStream(data)); + Plaintext.Builder builder = Plaintext.readWithoutSignature(type, + new ByteArrayInputStream(data)); long id = c.getLong(c.getColumnIndex(COLUMN_ID)); builder.id(id); builder.IV(new InventoryVector(iv)); @@ -240,6 +314,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { if (!c.isNull(nextTryColumn)) { builder.nextTry(c.getLong(nextTryColumn)); } + builder.conversation(asUuid(c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)))); builder.labels(findLabels(id)); result.add(builder.build()); } @@ -268,6 +343,8 @@ public class AndroidMessageRepository extends AbstractMessageRepository { update(db, message); } + updateParents(db, message); + // remove existing labels db.delete(JOIN_TABLE_NAME, "message_id=?", new String[]{valueOf(message.getId())}); @@ -302,6 +379,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { values.put(COLUMN_TTL, message.getTTL()); values.put(COLUMN_RETRIES, message.getRetries()); values.put(COLUMN_NEXT_TRY, message.getNextTry()); + values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId())); long id = db.insertOrThrow(TABLE_NAME, null, values); message.setId(id); } @@ -322,6 +400,7 @@ public class AndroidMessageRepository extends AbstractMessageRepository { values.put(COLUMN_TTL, message.getTTL()); values.put(COLUMN_RETRIES, message.getRetries()); values.put(COLUMN_NEXT_TRY, message.getNextTry()); + values.put(COLUMN_CONVERSATION, UuidUtils.asBytes(message.getConversationId())); db.update(TABLE_NAME, values, "id = " + message.getId(), null); } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java index b20c173..2183fcb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidNodeRegistry.java @@ -10,6 +10,7 @@ import android.database.sqlite.SQLiteStatement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -56,6 +57,12 @@ public class AndroidNodeRegistry implements NodeRegistry { db.delete(TABLE_NAME, "time < ?", new String[]{valueOf(now(-28 * DAY))}); } + @Override + public void clear() { + SQLiteDatabase db = sql.getWritableDatabase(); + db.delete(TABLE_NAME, null, null); + } + private Long loadExistingTime(NetworkAddress node) { SQLiteStatement statement = loadExistingStatement.get(); if (statement == null) { diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java index 0e95170..db7d411 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/SqlHelper.java @@ -27,7 +27,7 @@ import ch.dissem.apps.abit.util.Assets; */ public class SqlHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. - private static final int DATABASE_VERSION = 6; + private static final int DATABASE_VERSION = 7; private static final String DATABASE_NAME = "jabit.db"; private final Context ctx; @@ -61,6 +61,8 @@ public class SqlHelper extends SQLiteOpenHelper { executeMigration(db, "V3.3__Create_table_node"); case 5: executeMigration(db, "V3.4__Add_label_outbox"); + case 6: + executeMigration(db, "V4.0__Create_table_message_parent"); default: // Nothing to do. Let's assume we won't upgrade from a version that's newer than // DATABASE_VERSION. @@ -73,7 +75,7 @@ public class SqlHelper extends SQLiteOpenHelper { } } - public static StringBuilder join(long... numbers) { + static StringBuilder join(long... numbers) { StringBuilder streamList = new StringBuilder(); for (int i = 0; i < numbers.length; i++) { if (i > 0) streamList.append(", "); @@ -82,7 +84,7 @@ public class SqlHelper extends SQLiteOpenHelper { return streamList; } - public static StringBuilder join(Enum<?>... types) { + static StringBuilder join(Enum<?>... types) { StringBuilder streamList = new StringBuilder(); for (int i = 0; i < types.length; i++) { if (i > 0) streamList.append(", "); diff --git a/app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java b/app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java new file mode 100644 index 0000000..543dbac --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/UuidUtils.java @@ -0,0 +1,37 @@ +package ch.dissem.apps.abit.util; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * SQLite has no UUID data type, and UUIDs are therefore best saved as BINARY[16]. This class + * takes care of conversion between byte[16] and UUID. + * <p> + * Thanks to Brice Roncace on + * <a href="http://stackoverflow.com/questions/17893609/convert-uuid-to-byte-that-works-when-using-uuid-nameuuidfrombytesb"> + * Stack Overflow + * </a> + * for providing the UUID <-> byte[] conversions. + * </p> + */ +public class UuidUtils { + public static UUID asUuid(byte[] bytes) { + if (bytes == null) { + return null; + } + ByteBuffer bb = ByteBuffer.wrap(bytes); + long firstLong = bb.getLong(); + long secondLong = bb.getLong(); + return new UUID(firstLong, secondLong); + } + + public static byte[] asBytes(UUID uuid) { + if (uuid == null) { + return null; + } + ByteBuffer bb = ByteBuffer.wrap(new byte[16]); + bb.putLong(uuid.getMostSignificantBits()); + bb.putLong(uuid.getLeastSignificantBits()); + return bb.array(); + } +} diff --git a/build.gradle b/build.gradle index 149d3e7..b67f629 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:2.3.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e50d996..353d4a8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Aug 12 22:10:25 CEST 2016 +#Thu Mar 09 06:38:41 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip From 6be31c9f4e4f741b4cd656581717fa57b9c27fa1 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Mon, 20 Mar 2017 07:28:41 +0100 Subject: [PATCH 03/18] Fixed "add identity" dialog, updated dependencies --- app/build.gradle | 6 +++--- app/src/main/res/values/styles.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index aec9574..5cee257 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,7 +50,7 @@ dependencies { compile "ch.dissem.jabit:jabit-extensions:$jabitVersion" compile "ch.dissem.jabit:jabit-wif:$jabitVersion" - compile 'org.slf4j:slf4j-android:1.7.12' + compile 'org.slf4j:slf4j-android:1.7.25' compile 'com.mikepenz:materialize:1.0.1@aar' compile('com.mikepenz:materialdrawer:5.8.2@aar') { @@ -69,11 +69,11 @@ dependencies { compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true } - compile 'com.github.angads25:filepicker:1.0.9' + compile 'com.github.angads25:filepicker:1.1.0' compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.mockito:mockito-core:2.7.19' } idea.module { diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ba7dc9e..7d6f486 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,7 +19,7 @@ <item name="android:textColor">@color/colorAccent</item> </style> - <style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog"> + <style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog.MinWidth"> <item name="windowNoTitle">false</item> </style> </resources> From a203af654be1f863442fb5d557899dd713f8f3ab Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 23 Mar 2017 16:59:36 +0100 Subject: [PATCH 04/18] Add labels to message detail view --- app/build.gradle | 11 ++- .../ch/dissem/apps/abit/MainActivity.java | 40 +--------- .../apps/abit/MessageDetailFragment.java | 74 ++++++++++++++++++ .../repository/AndroidMessageRepository.java | 32 +------- .../java/ch/dissem/apps/abit/util/Labels.java | 77 +++++++++++++++++++ .../res/layout/fragment_message_detail.xml | 35 +++++---- app/src/main/res/layout/item_label.xml | 28 +++++++ 7 files changed, 215 insertions(+), 82 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/util/Labels.java create mode 100644 app/src/main/res/layout/item_label.xml diff --git a/app/build.gradle b/app/build.gradle index 5cee257..c87655e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,11 +38,12 @@ android { //ext.jabitVersion = '2.0.4' ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' +ext.supportVersion = '25.2.0' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:25.3.0' - compile 'com.android.support:support-v4:25.3.0' - compile 'com.android.support:design:25.3.0' + compile "com.android.support:appcompat-v7:$supportVersion" + compile "com.android.support:support-v4:$supportVersion" + compile "com.android.support:design:$supportVersion" compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -59,7 +60,8 @@ dependencies { compile('com.mikepenz:aboutlibraries:5.9.4@aar') { transitive = true } - compile 'com.mikepenz:iconics:1.6.2@aar' + compile "com.mikepenz:iconics-core:2.8.2@aar" + compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' @@ -71,6 +73,7 @@ dependencies { } compile 'com.github.angads25:filepicker:1.1.0' compile 'com.android.support.constraint:constraint-layout:1.0.2' + compile 'org.solovyev.android.views:linear-layout-manager:0.5@aar' testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:2.7.19' diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index fc45f75..9a2d9b0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -48,14 +48,10 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem; import com.mikepenz.materialdrawer.model.interfaces.IProfile; import com.mikepenz.materialdrawer.model.interfaces.Nameable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment; import ch.dissem.apps.abit.dialog.FullNodeDialogActivity; @@ -65,6 +61,7 @@ import ch.dissem.apps.abit.repository.AndroidMessageRepository; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Labels; import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; @@ -100,7 +97,6 @@ public class MainActivity extends AppCompatActivity public static final String EXTRA_REPLY_TO_MESSAGE = "ch.dissem.abit.ReplyToMessage"; public static final String ACTION_SHOW_INBOX = "ch.dissem.abit.ShowInbox"; - private static final Logger LOG = LoggerFactory.getLogger(MainActivity.class); private static final int ADD_IDENTITY = 1; private static final int MANAGE_IDENTITY = 2; @@ -437,37 +433,9 @@ public class MainActivity extends AppCompatActivity public void addLabelEntry(Label label) { PrimaryDrawerItem item = new PrimaryDrawerItem() .withName(label.toString()) - .withTag(label); - if (label.getType() == null) { - item.withIcon(CommunityMaterial.Icon.cmd_label) - .withIconColor(label.getColor()); - } else { - switch (label.getType()) { - case INBOX: - item.withIcon(GoogleMaterial.Icon.gmd_inbox); - break; - case DRAFT: - item.withIcon(CommunityMaterial.Icon.cmd_file); - break; - case OUTBOX: - item.withIcon(CommunityMaterial.Icon.cmd_inbox_arrow_up); - break; - case SENT: - item.withIcon(CommunityMaterial.Icon.cmd_send); - break; - case BROADCAST: - item.withIcon(CommunityMaterial.Icon.cmd_rss); - break; - case UNREAD: - item.withIcon(GoogleMaterial.Icon.gmd_markunread_mailbox); - break; - case TRASH: - item.withIcon(GoogleMaterial.Icon.gmd_delete); - break; - default: - item.withIcon(CommunityMaterial.Icon.cmd_label); - } - } + .withTag(label) + .withIcon(Labels.getIcon(label)) + .withIconColor(Labels.getColor(label)); drawer.addItemAtPosition(item, drawer.getDrawerItems().size() - 3); } diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 5dced97..857a195 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -16,8 +16,11 @@ package ch.dissem.apps.abit; +import android.content.Context; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.Menu; @@ -29,14 +32,19 @@ import android.widget.ImageView; import android.widget.TextView; import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.view.IconicsImageView; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Assets; import ch.dissem.apps.abit.util.Drawables; +import ch.dissem.apps.abit.util.Labels; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; @@ -106,6 +114,11 @@ public class MessageDetailFragment extends Fragment { } else if (item.getType() == Plaintext.Type.BROADCAST) { ((TextView) rootView.findViewById(R.id.recipient)).setText(R.string.broadcast); } + RecyclerView labelView = (RecyclerView) rootView.findViewById(R.id.labels); + LabelAdapter labelAdapter = new LabelAdapter(getActivity(), item.getLabels()); + labelView.setAdapter(labelAdapter); + labelView.setLayoutManager(new GridLayoutManager(getActivity(), 2)); + TextView messageBody = (TextView) rootView.findViewById(R.id.text); messageBody.setText(item.getText()); @@ -197,4 +210,65 @@ public class MessageDetailFragment extends Fragment { } return false; } + + private static class LabelAdapter extends + RecyclerView.Adapter<LabelAdapter.ViewHolder> { + + private final List<Label> labels; + private final Context ctx; + + private LabelAdapter(Context ctx, Set<Label> labels) { + this.labels = new ArrayList<>(labels); + this.ctx = ctx; + } + + @Override + public LabelAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + // Inflate the custom layout + View contactView = inflater.inflate(R.layout.item_label, parent, false); + + // Return a new holder instance + return new ViewHolder(contactView); + } + + // Involves populating data into the item through holder + @Override + public void onBindViewHolder(LabelAdapter.ViewHolder viewHolder, int position) { + // Get the data model based on position + Label label = labels.get(position); + + viewHolder.icon.setColor(Labels.getColor(label)); + viewHolder.icon.setIcon(Labels.getIcon(label)); + viewHolder.label.setText(Labels.getText(label, ctx)); + } + + // Returns the total count of items in the list + @Override + public int getItemCount() { + return labels.size(); + } + + // Provide a direct reference to each of the views within a data item + // Used to cache the views within the item layout for fast access + static class ViewHolder extends RecyclerView.ViewHolder { + // Your holder should contain a member variable + // for any view that will be set as you render a row + public IconicsImageView icon; + public TextView label; + + // We also create a constructor that accepts the entire item row + // and does the view lookups to find each subview + ViewHolder(View itemView) { + // Stores the itemView in a public final member variable that can be used + // to access the context from any ViewHolder instance. + super(itemView); + + icon = (IconicsImageView) itemView.findViewById(R.id.icon); + label = (TextView) itemView.findViewById(R.id.label); + } + } + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index 3f7c8c1..b38cb1d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -33,7 +33,7 @@ import java.util.LinkedList; import java.util.List; import java.util.UUID; -import ch.dissem.apps.abit.R; +import ch.dissem.apps.abit.util.Labels; import ch.dissem.apps.abit.util.UuidUtils; import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; @@ -129,35 +129,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository { private Label getLabel(Cursor c) { String typeName = c.getString(c.getColumnIndex(LBL_COLUMN_TYPE)); Label.Type type = typeName == null ? null : Label.Type.valueOf(typeName); - String text; - if (type == null) { + String text = Labels.getText(type, null, context); + if (text == null) { text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); - } else { - switch (type) { - case INBOX: - text = context.getString(R.string.inbox); - break; - case DRAFT: - text = context.getString(R.string.draft); - break; - case OUTBOX: - text = context.getString(R.string.outbox); - break; - case SENT: - text = context.getString(R.string.sent); - break; - case UNREAD: - text = context.getString(R.string.unread); - break; - case TRASH: - text = context.getString(R.string.trash); - break; - case BROADCAST: - text = context.getString(R.string.broadcasts); - break; - default: - text = c.getString(c.getColumnIndex(LBL_COLUMN_LABEL)); - } } Label label = new Label( text, diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Labels.java b/app/src/main/java/ch/dissem/apps/abit/util/Labels.java new file mode 100644 index 0000000..75c6def --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/util/Labels.java @@ -0,0 +1,77 @@ +package ch.dissem.apps.abit.util; + +import android.content.Context; +import android.support.annotation.ColorInt; + +import com.mikepenz.community_material_typeface_library.CommunityMaterial; +import com.mikepenz.google_material_typeface_library.GoogleMaterial; +import com.mikepenz.iconics.typeface.IIcon; + +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.valueobject.Label; + +/** + * Helper class to help with translating the default labels, getting label colors and so on. + */ +public class Labels { + public static String getText(Label label, Context ctx) { + return getText(label.getType(), label.toString(), ctx); + } + + public static String getText(Label.Type type, String alternative, Context ctx) { + if (type == null) { + return alternative; + } else { + switch (type) { + case INBOX: + return ctx.getString(R.string.inbox); + case DRAFT: + return ctx.getString(R.string.draft); + case OUTBOX: + return ctx.getString(R.string.outbox); + case SENT: + return ctx.getString(R.string.sent); + case UNREAD: + return ctx.getString(R.string.unread); + case TRASH: + return ctx.getString(R.string.trash); + case BROADCAST: + return ctx.getString(R.string.broadcasts); + default: + return alternative; + } + } + } + + public static IIcon getIcon(Label label) { + if (label.getType() == null) { + return CommunityMaterial.Icon.cmd_label; + } + switch (label.getType()) { + case INBOX: + return GoogleMaterial.Icon.gmd_inbox; + case DRAFT: + return CommunityMaterial.Icon.cmd_file; + case OUTBOX: + return CommunityMaterial.Icon.cmd_inbox_arrow_up; + case SENT: + return CommunityMaterial.Icon.cmd_send; + case BROADCAST: + return CommunityMaterial.Icon.cmd_rss; + case UNREAD: + return GoogleMaterial.Icon.gmd_markunread_mailbox; + case TRASH: + return GoogleMaterial.Icon.gmd_delete; + default: + return CommunityMaterial.Icon.cmd_label; + } + } + + @ColorInt + public static int getColor(Label label) { + if (label.getType() == null) { + return label.getColor(); + } + return 0xFF000000; + } +} diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index b0a7ef5..95ca52d 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -1,14 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent"> + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true" android:focusableInTouchMode="true" + android:paddingBottom="64dp" android:orientation="vertical"> <TextView @@ -24,7 +25,7 @@ android:padding="16dp" android:textAppearance="?android:attr/textAppearanceLarge" tools:ignore="UnusedAttribute" - tools:text="Subject"/> + tools:text="Subject" /> <ImageView android:id="@+id/status" @@ -32,17 +33,17 @@ android:layout_height="60dp" android:layout_alignParentEnd="true" android:layout_alignParentTop="true" - android:tint="@color/colorAccent" - tools:src="@drawable/ic_notification_proof_of_work" android:padding="16dp" - tools:ignore="ContentDescription"/> + android:tint="@color/colorAccent" + tools:ignore="ContentDescription" + tools:src="@drawable/ic_notification_proof_of_work" /> <View android:id="@+id/divider" android:layout_width="fill_parent" android:layout_height="2dip" android:layout_below="@id/subject" - android:background="@color/divider"/> + android:background="@color/divider" /> <ImageView android:id="@+id/avatar" @@ -52,7 +53,7 @@ android:layout_below="@+id/divider" android:layout_margin="16dp" android:src="@color/colorAccent" - tools:ignore="ContentDescription"/> + tools:ignore="ContentDescription" /> <TextView android:id="@+id/sender" @@ -64,7 +65,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:textStyle="bold" - tools:text="Sender"/> + tools:text="Sender" /> <TextView android:id="@+id/recipient" @@ -75,7 +76,7 @@ android:gravity="center_vertical" android:paddingLeft="8dp" android:paddingRight="8dp" - tools:text="Recipient"/> + tools:text="Recipient" /> <TextView android:id="@+id/text" @@ -86,8 +87,16 @@ android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginTop="32dp" - android:paddingBottom="64dp" + android:paddingBottom="16dp" android:text="New Text" - android:textIsSelectable="true"/> + android:textIsSelectable="true" /> + + <android.support.v7.widget.RecyclerView + android:id="@+id/labels" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/text" + android:paddingEnd="16dp" + android:paddingStart="16dp" /> </RelativeLayout> </ScrollView> diff --git a/app/src/main/res/layout/item_label.xml b/app/src/main/res/layout/item_label.xml new file mode 100644 index 0000000..26869a5 --- /dev/null +++ b/app/src/main/res/layout/item_label.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal"> + + <com.mikepenz.iconics.view.IconicsImageView + android:id="@+id/icon" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + app:iiv_color="@android:color/black" + app:iiv_icon="cmd-label" /> + + <TextView + android:id="@+id/label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingStart="8dp" + android:paddingEnd="24dp" + tools:text="Label" + android:layout_alignParentTop="true" + android:layout_toEndOf="@+id/icon" /> +</RelativeLayout> From e93dc43f89660242fefef13914186fc0074c4082 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 24 Mar 2017 07:35:57 +0100 Subject: [PATCH 05/18] Tiny code improvement --- app/src/main/res/layout/fragment_message_detail.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index 95ca52d..1c63f9b 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -88,7 +88,7 @@ android:layout_marginRight="16dp" android:layout_marginTop="32dp" android:paddingBottom="16dp" - android:text="New Text" + tools:text="Message Body" android:textIsSelectable="true" /> <android.support.v7.widget.RecyclerView @@ -96,7 +96,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@+id/text" - android:paddingEnd="16dp" - android:paddingStart="16dp" /> + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" /> </RelativeLayout> </ScrollView> From 61c9cde2f5690a21ec5a7e1469bb15ad52dfd145 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 4 Apr 2017 06:29:44 +0200 Subject: [PATCH 06/18] Gradle wrapper and dependency versions bumped --- app/build.gradle | 9 +++++---- build.gradle | 3 +++ gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 22 +++++++++++++++------- gradlew.bat | 6 ------ 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c87655e..e5ec049 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,7 @@ ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' ext.supportVersion = '25.2.0' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) + compile "com.android.support:appcompat-v7:$supportVersion" compile "com.android.support:support-v4:$supportVersion" compile "com.android.support:design:$supportVersion" @@ -64,19 +65,19 @@ dependencies { compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' - compile 'com.journeyapps:zxing-android-embedded:3.3.0@aar' + compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' compile 'com.google.zxing:core:3.3.0' - compile 'io.github.yavski:fab-speed-dial:1.0.6' + + compile 'io.github.yavski:fab-speed-dial:1.0.7' compile 'com.github.amlcurran.showcaseview:library:5.4.3' compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true } compile 'com.github.angads25:filepicker:1.1.0' compile 'com.android.support.constraint:constraint-layout:1.0.2' - compile 'org.solovyev.android.views:linear-layout-manager:0.5@aar' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.19' + testCompile 'org.mockito:mockito-core:2.7.21' } idea.module { diff --git a/build.gradle b/build.gradle index b67f629..c37a44a 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.github.ben-manes:gradle-versions-plugin:0.14.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -17,6 +18,8 @@ buildscript { } allprojects { + apply plugin: 'com.github.ben-manes.versions' + repositories { jcenter() mavenCentral() diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..9b92f4a573d78bc17ece97d1c612ce7db44f2c44 100644 GIT binary patch delta 21799 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 delta 21015 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 S8gVG|1v@Jj9C(#*>;C{k4ZhI; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 353d4a8..8164845 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Mar 09 06:38:41 CET 2017 +#Tue Apr 04 00:02:32 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/gradlew b/gradlew index 27309d9..4453cce 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f6d5974..e95643d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -49,7 +49,6 @@ goto fail @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line From 8770575c959126a099f1059c4cafca77c51933c6 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 7 Apr 2017 14:36:16 +0200 Subject: [PATCH 07/18] Fixed NullPointerException --- .../main/java/ch/dissem/apps/abit/MainActivity.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index 9a2d9b0..e7aaa9a 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -379,7 +379,10 @@ public class MainActivity extends AppCompatActivity for (Label label : labels) { addLabelEntry(label); } - drawer.setSelection(drawer.getDrawerItem(selectedLabel)); + IDrawerItem selectedDrawerItem = drawer.getDrawerItem(selectedLabel); + if (selectedDrawerItem != null) { + drawer.setSelection(selectedDrawerItem); + } } }.execute(); } @@ -395,8 +398,10 @@ public class MainActivity extends AppCompatActivity protected void onRestoreInstanceState(Bundle savedInstanceState) { selectedLabel = (Label) savedInstanceState.getSerializable("selectedLabel"); - drawer.setSelection(drawer.getDrawerItem(selectedLabel)); - + IDrawerItem selectedItem = drawer.getDrawerItem(selectedLabel); + if (selectedItem != null) { + drawer.setSelection(selectedItem); + } super.onRestoreInstanceState(savedInstanceState); } From f77bbe1a4363c6ef86a2009ba3a8dcc51c76bb74 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Apr 2017 22:35:26 +0200 Subject: [PATCH 08/18] Show related messages (parents, replies) --- .../apps/abit/MessageDetailFragment.java | 75 ++++++++++++++++++- .../dissem/apps/abit/service/Singleton.java | 14 ++++ app/src/main/res/drawable/border_bottom.xml | 10 +++ .../res/layout/fragment_message_detail.xml | 18 ++++- .../res/layout/item_message_minimized.xml | 64 ++++++++++++++++ app/src/main/res/layout/message_row.xml | 26 +++---- 6 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/border_bottom.xml create mode 100644 app/src/main/res/layout/item_message_minimized.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 857a195..0552cb6 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -18,8 +18,10 @@ package ch.dissem.apps.abit; import android.content.Context; import android.os.Bundle; +import android.support.annotation.IdRes; import android.support.v4.app.Fragment; import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.util.Linkify; import android.view.LayoutInflater; @@ -47,12 +49,14 @@ import ch.dissem.apps.abit.util.Drawables; import ch.dissem.apps.abit.util.Labels; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; import static android.text.util.Linkify.WEB_URLS; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN; import static ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA; +import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces; /** @@ -143,16 +147,30 @@ public class MessageDetailFragment extends Fragment { removed = true; } } + MessageRepository messageRepo = Singleton.getMessageRepository(inflater.getContext()); if (removed) { if (getActivity() instanceof ActionBarListener) { ((ActionBarListener) getActivity()).updateUnread(); } - Singleton.getMessageRepository(inflater.getContext()).save(item); + messageRepo.save(item); } + List<Plaintext> parents = new ArrayList<>(item.getParents().size()); + for (InventoryVector parentIV : item.getParents()) { + parents.add(messageRepo.getMessage(parentIV)); + } + showRelatedMessages(rootView, R.id.parents, parents); + showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item)); } return rootView; } + private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) { + RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.parents); + RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages); + recyclerView.setAdapter(adapter); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.message, menu); @@ -211,6 +229,61 @@ public class MessageDetailFragment extends Fragment { return false; } + private static class RelatedMessageAdapter extends RecyclerView.Adapter<RelatedMessageAdapter.ViewHolder> { + private final List<Plaintext> messages; + private final Context ctx; + + private RelatedMessageAdapter(Context ctx, List<Plaintext> messages) { + this.messages = messages; + this.ctx = ctx; + } + + @Override + public RelatedMessageAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + Context context = parent.getContext(); + LayoutInflater inflater = LayoutInflater.from(context); + + // Inflate the custom layout + View contactView = inflater.inflate(R.layout.item_message_minimized, parent, false); + + // Return a new holder instance + return new RelatedMessageAdapter.ViewHolder(contactView); + } + + // Involves populating data into the item through holder + @Override + public void onBindViewHolder(RelatedMessageAdapter.ViewHolder viewHolder, int position) { + // Get the data model based on position + Plaintext message = messages.get(position); + + viewHolder.avatar.setImageDrawable(new Identicon(message.getFrom())); + viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus())); + viewHolder.sender.setText(message.getFrom().toString()); + viewHolder.extract.setText(normalizeWhitespaces(message.getText())); + } + + // Returns the total count of items in the list + @Override + public int getItemCount() { + return messages.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final ImageView avatar; + private final ImageView status; + private final TextView sender; + private final TextView extract; + + ViewHolder(View itemView) { + super(itemView); + avatar = (ImageView) itemView.findViewById(R.id.avatar); + status = (ImageView) itemView.findViewById(R.id.status); + sender = (TextView) itemView.findViewById(R.id.sender); + extract = (TextView) itemView.findViewById(R.id.text); + } + } + } + private static class LabelAdapter extends RecyclerView.Adapter<LabelAdapter.ViewHolder> { diff --git a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java index 1bceb0b..e01f099 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/Singleton.java @@ -42,6 +42,7 @@ import ch.dissem.bitmessage.networking.nio.NioNetworkHandler; import ch.dissem.bitmessage.ports.AddressRepository; import ch.dissem.bitmessage.ports.MessageRepository; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; +import ch.dissem.bitmessage.utils.ConversationService; import ch.dissem.bitmessage.utils.TTL; import static ch.dissem.bitmessage.utils.UnixTime.DAY; @@ -51,6 +52,7 @@ import static ch.dissem.bitmessage.utils.UnixTime.DAY; */ public class Singleton { private static BitmessageContext bitmessageContext; + private static ConversationService conversationService; private static MessageListener messageListener; private static BitmessageAddress identity; private static AndroidProofOfWorkRepository powRepo; @@ -160,4 +162,16 @@ public class Singleton { throw new IllegalArgumentException("Identity expected, but no private key available"); Singleton.identity = identity; } + + public static ConversationService getConversationService(Context ctx) { + if (conversationService == null) { + final BitmessageContext bmc = getBitmessageContext(ctx); + synchronized (Singleton.class) { + if (conversationService == null) { + conversationService = new ConversationService(bmc.messages()); + } + } + } + return conversationService; + } } diff --git a/app/src/main/res/drawable/border_bottom.xml b/app/src/main/res/drawable/border_bottom.xml new file mode 100644 index 0000000..ab52873 --- /dev/null +++ b/app/src/main/res/drawable/border_bottom.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > + <item android:top="1dp" android:bottom="1dp"> + <shape + android:shape="rectangle"> + <stroke android:width="1dp" android:color="#FFDDDDDD" /> + <solid android:color="#00000000" /> + </shape> + </item> +</layer-list> diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index 1c63f9b..e698bf5 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -78,12 +78,20 @@ android:paddingRight="8dp" tools:text="Recipient" /> + <android.support.v7.widget.RecyclerView + android:id="@+id/parents" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/avatar" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" /> + <TextView android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentStart="true" - android:layout_below="@+id/avatar" + android:layout_below="@+id/parents" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginTop="32dp" @@ -98,5 +106,13 @@ android:layout_below="@+id/text" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" /> + + <android.support.v7.widget.RecyclerView + android:id="@+id/responses" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/text" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" /> </RelativeLayout> </ScrollView> diff --git a/app/src/main/res/layout/item_message_minimized.xml b/app/src/main/res/layout/item_message_minimized.xml new file mode 100644 index 0000000..f556959 --- /dev/null +++ b/app/src/main/res/layout/item_message_minimized.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/border_bottom"> + + <ImageView + android:id="@+id/avatar" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_margin="16dp" + android:src="@color/colorPrimaryDark" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/sender" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_alignTop="@+id/avatar" + android:layout_marginTop="-5dp" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingBottom="0dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="0dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + tools:text="Sender" /> + + <TextView + android:id="@+id/text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@+id/sender" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:gravity="center_vertical" + android:lines="1" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Text" /> + + <ImageView + android:id="@+id/status" + android:layout_width="24dp" + android:layout_height="wrap_content" + android:layout_alignBottom="@id/avatar" + android:layout_alignEnd="@+id/avatar" + android:layout_marginBottom="-8dp" + android:layout_marginEnd="-8dp" + android:tint="@color/colorAccent" + tools:ignore="ContentDescription" + tools:src="@drawable/ic_notification_proof_of_work" /> + +</RelativeLayout> diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index f23d5c1..9c9f88a 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -1,5 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- +<?xml version="1.0" encoding="utf-8"?><!-- ~ Copyright 2015 Christian Basler ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,8 +14,7 @@ ~ limitations under the License. --> -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -31,11 +29,10 @@ android:foreground="?attr/selectableItemBackground" tools:ignore="UselessParent"> - <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/selectableItemBackground"> + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground"> <ImageView android:id="@+id/avatar" @@ -45,7 +42,7 @@ android:layout_alignParentTop="true" android:layout_margin="16dp" android:src="@color/colorPrimaryDark" - tools:ignore="ContentDescription"/> + tools:ignore="ContentDescription" /> <TextView android:id="@+id/sender" @@ -63,8 +60,7 @@ android:paddingTop="0dp" android:textAppearance="?android:attr/textAppearanceMedium" android:textStyle="bold" - tools:text="Sender" - /> + tools:text="Sender" /> <TextView android:id="@+id/subject" @@ -78,7 +74,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:textAppearance="?android:attr/textAppearanceSmall" - tools:text="Subject"/> + tools:text="Subject" /> <TextView android:id="@+id/text" @@ -94,7 +90,7 @@ android:paddingLeft="8dp" android:paddingRight="8dp" android:textAppearance="?android:attr/textAppearanceSmall" - tools:text="Text"/> + tools:text="Text" /> <ImageView android:id="@+id/status" @@ -106,7 +102,7 @@ android:layout_marginEnd="-8dp" android:tint="@color/colorAccent" tools:ignore="ContentDescription" - tools:src="@drawable/ic_notification_proof_of_work"/> + tools:src="@drawable/ic_notification_proof_of_work" /> </RelativeLayout> From 55746743c43b40be83b45ae093554a48c6488b9c Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Apr 2017 22:36:23 +0200 Subject: [PATCH 09/18] Updated dependencies, enabled multidex --- app/build.gradle | 10 ++++++---- app/src/main/AndroidManifest.xml | 1 + build.gradle | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e5ec049..cdd925d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,7 @@ android { versionCode 11 versionName "1.0-beta11" jackOptions.enabled = false + multiDexEnabled true } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 @@ -45,6 +46,7 @@ dependencies { compile "com.android.support:appcompat-v7:$supportVersion" compile "com.android.support:support-v4:$supportVersion" compile "com.android.support:design:$supportVersion" + compile "com.android.support:multidex:1.0.1" compile "ch.dissem.jabit:jabit-core:$jabitVersion" compile "ch.dissem.jabit:jabit-networking:$jabitVersion" @@ -55,10 +57,10 @@ dependencies { compile 'org.slf4j:slf4j-android:1.7.25' compile 'com.mikepenz:materialize:1.0.1@aar' - compile('com.mikepenz:materialdrawer:5.8.2@aar') { + compile('com.mikepenz:materialdrawer:5.9.0@aar') { transitive = true } - compile('com.mikepenz:aboutlibraries:5.9.4@aar') { + compile('com.mikepenz:aboutlibraries:5.9.5@aar') { transitive = true } compile "com.mikepenz:iconics-core:2.8.2@aar" @@ -68,7 +70,7 @@ dependencies { compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' compile 'com.google.zxing:core:3.3.0' - compile 'io.github.yavski:fab-speed-dial:1.0.7' + compile 'io.github.yavski:fab-speed-dial:1.0.6' compile 'com.github.amlcurran.showcaseview:library:5.4.3' compile('com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:0.10.4@aar') { transitive = true @@ -77,7 +79,7 @@ dependencies { compile 'com.android.support.constraint:constraint-layout:1.0.2' testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:2.7.21' + testCompile 'org.mockito:mockito-core:2.7.22' } idea.module { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a7a2f74..d46c295 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" + android:name="android.support.multidex.MultiDexApplication" tools:replace="android:allowBackup"> <activity android:name=".MainActivity" diff --git a/build.gradle b/build.gradle index c37a44a..ceae43c 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.github.ben-manes:gradle-versions-plugin:0.14.0' // NOTE: Do not place your application dependencies here; they belong From 26fca259bcf5afce657fee07d07a0d58191d42b7 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sun, 16 Apr 2017 22:38:11 +0200 Subject: [PATCH 10/18] Alternative key exchange by providing the public key in the URI --- .../apps/abit/AddressDetailFragment.java | 16 +++++ .../apps/abit/CreateAddressActivity.java | 58 +++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 40b2410..95ce443 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -26,6 +26,7 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.text.Editable; import android.text.TextWatcher; +import android.util.Base64; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -48,13 +49,18 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.wif.WifExporter; import static android.graphics.Color.BLACK; import static android.graphics.Color.WHITE; +import static android.util.Base64.URL_SAFE; /** @@ -269,6 +275,16 @@ public class AddressDetailFragment extends Fragment { if (address.getAlias() != null) { link.append("?label=").append(address.getAlias()); } + if (address.getPubkey() != null) { + link.append(address.getAlias() == null ? '?' : '&'); + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); + try { + address.getPubkey().writeUnencrypted(pubkey); + } catch (IOException e) { + throw new ApplicationException(e); + } + link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); + } BitMatrix result; try { result = new MultiFormatWriter().encode(link.toString(), diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java index cf5d51f..27d344d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -20,17 +20,32 @@ import android.app.Activity; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; +import android.util.Base64; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Switch; import android.widget.TextView; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import ch.dissem.apps.abit.service.Singleton; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.entity.payload.Pubkey; +import ch.dissem.bitmessage.entity.payload.V2Pubkey; +import ch.dissem.bitmessage.entity.payload.V3Pubkey; +import ch.dissem.bitmessage.entity.payload.V4Pubkey; + +import static android.util.Base64.URL_SAFE; public class CreateAddressActivity extends AppCompatActivity { + private static final Pattern KEY_VALUE_PATTERN = Pattern.compile("^([a-zA-Z]+)=(.*)$"); + private byte[] pubkeyBytes; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -48,12 +63,19 @@ public class CreateAddressActivity extends AppCompatActivity { String addressText = getAddress(uri); String[] parameters = getParameters(uri); for (String parameter : parameters) { - String name = parameter.substring(0, 6).toLowerCase(); - if (name.startsWith("label")) { - label.setText(parameter.substring(parameter.indexOf('=') + 1).trim()); - } else if (name.startsWith("action")) { - parameter = parameter.toLowerCase(); - subscribe.setChecked(parameter.contains("subscribe")); + Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter); + String key = matcher.group(1).toLowerCase(); + String value = matcher.group(2); + switch (key) { + case "label": + label.setText(value.trim()); + break; + case "action": + subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); + break; + case "pubkey": + pubkeyBytes = Base64.decode(value, URL_SAFE); + break; } } @@ -83,6 +105,30 @@ public class CreateAddressActivity extends AppCompatActivity { if (subscribe.isChecked()) { bmc.addSubscribtion(bmAddress); } + if (pubkeyBytes != null) { + try { + final Pubkey pubkey; + InputStream pubkeyStream = new ByteArrayInputStream(pubkeyBytes); + long stream = bmAddress.getStream(); + switch ((int) bmAddress.getVersion()) { + case 2: + pubkey = V2Pubkey.read(pubkeyStream, stream); + break; + case 3: + pubkey = V3Pubkey.read(pubkeyStream, stream); + break; + case 4: + pubkey = new V4Pubkey(V3Pubkey.read(pubkeyStream, stream)); + break; + default: + pubkey = null; + } + if (pubkey != null) { + bmAddress.setPubkey(pubkey); + } + } catch (Exception ignore) { + } + } setResult(Activity.RESULT_OK); finish(); From 911dfa7a27b83a9f8eb5f927f66a817a7f5faa41 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 18 Apr 2017 16:38:45 +0200 Subject: [PATCH 11/18] Show QR code when clicking on profile image --- .../apps/abit/AddressDetailFragment.java | 60 +------------------ .../ch/dissem/apps/abit/MainActivity.java | 52 ++++++++++++++++ .../ch/dissem/apps/abit/util/Drawables.java | 59 ++++++++++++++++++ 3 files changed, 112 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java index 95ce443..704726d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.java @@ -20,13 +20,11 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; -import android.graphics.Bitmap; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.text.Editable; import android.text.TextWatcher; -import android.util.Base64; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -39,29 +37,14 @@ import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.MultiFormatWriter; -import com.google.zxing.WriterException; -import com.google.zxing.common.BitMatrix; import com.mikepenz.community_material_typeface_library.CommunityMaterial; import com.mikepenz.google_material_typeface_library.GoogleMaterial; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.util.Drawables; import ch.dissem.bitmessage.entity.BitmessageAddress; -import ch.dissem.bitmessage.exception.ApplicationException; import ch.dissem.bitmessage.wif.WifExporter; -import static android.graphics.Color.BLACK; -import static android.graphics.Color.WHITE; -import static android.util.Base64.URL_SAFE; - /** * A fragment representing a single Message detail screen. @@ -70,7 +53,6 @@ import static android.util.Base64.URL_SAFE; * on handsets. */ public class AddressDetailFragment extends Fragment { - private static final Logger LOG = LoggerFactory.getLogger(AddressDetailFragment.class); /** * The fragment argument representing the item ID that this fragment * represents. @@ -78,8 +60,6 @@ public class AddressDetailFragment extends Fragment { public static final String ARG_ITEM = "item"; public static final String EXPORT_POSTFIX = ".keys.dat"; - private static final int QR_CODE_SIZE = 350; - /** * The content this fragment is presenting. */ @@ -263,50 +243,12 @@ public class AddressDetailFragment extends Fragment { // QR code ImageView qrCode = (ImageView) rootView.findViewById(R.id.qr_code); - qrCode.setImageBitmap(qrCode(item)); + qrCode.setImageBitmap(Drawables.qrCode(item)); } return rootView; } - Bitmap qrCode(BitmessageAddress address) { - StringBuilder link = new StringBuilder("bitmessage:"); - link.append(address.getAddress()); - if (address.getAlias() != null) { - link.append("?label=").append(address.getAlias()); - } - if (address.getPubkey() != null) { - link.append(address.getAlias() == null ? '?' : '&'); - ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); - try { - address.getPubkey().writeUnencrypted(pubkey); - } catch (IOException e) { - throw new ApplicationException(e); - } - link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); - } - BitMatrix result; - try { - result = new MultiFormatWriter().encode(link.toString(), - BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null); - } catch (WriterException e) { - LOG.error(e.getMessage(), e); - return null; - } - int w = result.getWidth(); - int h = result.getHeight(); - int[] pixels = new int[w * h]; - for (int y = 0; y < h; y++) { - int offset = y * w; - for (int x = 0; x < w; x++) { - pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; - } - } - Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); - bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h); - return bitmap; - } - @Override public void onPause() { if (item != null) { diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index e7aaa9a..d744b99 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -16,16 +16,23 @@ package ch.dissem.apps.abit; +import android.app.Dialog; +import android.content.DialogInterface; import android.content.Intent; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.view.Display; import android.view.View; import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; import android.widget.CompoundButton; +import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.Toast; @@ -61,6 +68,7 @@ import ch.dissem.apps.abit.repository.AndroidMessageRepository; import ch.dissem.apps.abit.service.BitmessageService; import ch.dissem.apps.abit.service.Singleton; import ch.dissem.apps.abit.synchronization.SyncAdapter; +import ch.dissem.apps.abit.util.Drawables; import ch.dissem.apps.abit.util.Labels; import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; @@ -230,6 +238,50 @@ public class MainActivity extends AppCompatActivity .withActivity(this) .withHeaderBackground(R.drawable.header) .withProfiles(profiles) + .withOnAccountHeaderProfileImageListener(new AccountHeader.OnAccountHeaderProfileImageListener() { + @Override + public boolean onProfileImageClick(View view, IProfile profile, boolean current) { + if (current) { + // Show QR code in modal dialog + final Dialog dialog = new Dialog(MainActivity.this); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + + ImageView imageView = new ImageView(MainActivity.this); + imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(MainActivity.this))); + imageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + dialog.addContentView(imageView, new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + Window window = dialog.getWindow(); + if (window != null) { + Display display = window.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + int dim = size.x < size.y ? size.x : size.y; + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(window.getAttributes()); + lp.width = dim; + lp.height = dim; + + window.setAttributes(lp); + } + dialog.show(); + return true; + } + return false; + } + + @Override + public boolean onProfileImageLongClick(View view, IProfile iProfile, boolean b) { + return false; + } + }) .withOnAccountHeaderListener(new AccountHeader.OnAccountHeaderListener() { @Override public boolean onProfileChanged(View view, IProfile profile, boolean current) { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java index a35281a..d577515 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java @@ -19,19 +19,40 @@ package ch.dissem.apps.abit.util; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.util.Base64; import android.view.Menu; import android.view.MenuItem; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; import com.mikepenz.iconics.IconicsDrawable; import com.mikepenz.iconics.typeface.IIcon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + import ch.dissem.apps.abit.Identicon; import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.BitmessageAddress; +import ch.dissem.bitmessage.exception.ApplicationException; + +import static android.graphics.Color.BLACK; +import static android.graphics.Color.WHITE; +import static android.util.Base64.URL_SAFE; /** * Some helper methods to work with drawables. */ public class Drawables { + private static final Logger LOG = LoggerFactory.getLogger(Drawables.class); + + private static final int QR_CODE_SIZE = 350; + public static MenuItem addIcon(Context ctx, Menu menu, int menuItem, IIcon icon) { MenuItem item = menu.findItem(menuItem); item.setIcon(new IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()); @@ -49,4 +70,42 @@ public class Drawables { identicon.draw(canvas); return bitmap; } + + public static Bitmap qrCode(BitmessageAddress address) { + StringBuilder link = new StringBuilder("bitmessage:"); + link.append(address.getAddress()); + if (address.getAlias() != null) { + link.append("?label=").append(address.getAlias()); + } + if (address.getPubkey() != null) { + link.append(address.getAlias() == null ? '?' : '&'); + ByteArrayOutputStream pubkey = new ByteArrayOutputStream(); + try { + address.getPubkey().writeUnencrypted(pubkey); + } catch (IOException e) { + throw new ApplicationException(e); + } + link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); + } + BitMatrix result; + try { + result = new MultiFormatWriter().encode(link.toString(), + BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null); + } catch (WriterException e) { + LOG.error(e.getMessage(), e); + return null; + } + int w = result.getWidth(); + int h = result.getHeight(); + int[] pixels = new int[w * h]; + for (int y = 0; y < h; y++) { + int offset = y * w; + for (int x = 0; x < w; x++) { + pixels[offset + x] = result.get(x, y) ? BLACK : WHITE; + } + } + Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h); + return bitmap; + } } From a8dada6c89c0b1a049986d82310f2d17cf24d6d8 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Wed, 19 Apr 2017 00:20:51 +0200 Subject: [PATCH 12/18] Alternative key exchange by providing the public key in the URI (bugfixes) --- .../apps/abit/CreateAddressActivity.java | 26 ++++++++++--------- .../ch/dissem/apps/abit/util/Drawables.java | 3 ++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java index 27d344d..6ed8060 100644 --- a/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/CreateAddressActivity.java @@ -64,18 +64,20 @@ public class CreateAddressActivity extends AppCompatActivity { String[] parameters = getParameters(uri); for (String parameter : parameters) { Matcher matcher = KEY_VALUE_PATTERN.matcher(parameter); - String key = matcher.group(1).toLowerCase(); - String value = matcher.group(2); - switch (key) { - case "label": - label.setText(value.trim()); - break; - case "action": - subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); - break; - case "pubkey": - pubkeyBytes = Base64.decode(value, URL_SAFE); - break; + if (matcher.find()) { + String key = matcher.group(1).toLowerCase(); + String value = matcher.group(2); + switch (key) { + case "label": + label.setText(value.trim()); + break; + case "action": + subscribe.setChecked(value.trim().equalsIgnoreCase("subscribe")); + break; + case "pubkey": + pubkeyBytes = Base64.decode(value, URL_SAFE); + break; + } } } diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java index d577515..dfda9c8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Drawables.java @@ -43,6 +43,7 @@ import ch.dissem.bitmessage.exception.ApplicationException; import static android.graphics.Color.BLACK; import static android.graphics.Color.WHITE; +import static android.util.Base64.NO_WRAP; import static android.util.Base64.URL_SAFE; /** @@ -85,7 +86,7 @@ public class Drawables { } catch (IOException e) { throw new ApplicationException(e); } - link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE)); + link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE | NO_WRAP)); } BitMatrix result; try { From 572ecf1577d2f303349c642e206473d6af2d7510 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 20 Apr 2017 07:23:21 +0200 Subject: [PATCH 13/18] Some minor fixes for working with extended encoding --- .../apps/abit/ComposeMessageActivity.java | 2 +- .../apps/abit/ComposeMessageFragment.java | 17 +++++++++++++++++ .../dissem/apps/abit/MessageDetailFragment.java | 7 +++++-- .../repository/AndroidMessageRepository.java | 4 ++-- .../main/res/layout/fragment_message_detail.xml | 5 +++-- 5 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java index 2df2a68..1cde1b0 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.java @@ -87,8 +87,8 @@ public class ComposeMessageActivity extends AppCompatActivity { // so features like threading can be supported if (item.getEncoding() == EXTENDED) { replyIntent.putExtra(EXTRA_ENCODING, EXTENDED); - replyIntent.putExtra(EXTRA_PARENT, item); } + replyIntent.putExtra(EXTRA_PARENT, item); String prefix; if (item.getSubject().length() >= 3 && item.getSubject().substring(0, 3) .equalsIgnoreCase("RE:")) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java index 4413933..142e8f8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.java @@ -16,6 +16,7 @@ package ch.dissem.apps.abit; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; @@ -79,6 +80,11 @@ public class ComposeMessageFragment extends Fragment { if (getArguments() != null) { if (getArguments().containsKey(EXTRA_IDENTITY)) { identity = (BitmessageAddress) getArguments().getSerializable(EXTRA_IDENTITY); + if (getActivity() != null) { + if (identity == null || identity.getPrivateKey() == null) { + identity = Singleton.getIdentity(getActivity()); + } + } } else { throw new RuntimeException("No identity set for ComposeMessageFragment"); } @@ -156,6 +162,14 @@ public class ComposeMessageFragment extends Fragment { return rootView; } + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (identity == null || identity.getPrivateKey() == null) { + identity = Singleton.getIdentity(context); + } + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.compose, menu); @@ -170,6 +184,9 @@ public class ComposeMessageFragment extends Fragment { return true; case R.id.select_encoding: SelectEncodingDialogFragment encodingDialog = new SelectEncodingDialogFragment(); + Bundle args = new Bundle(); + args.putSerializable(EXTRA_ENCODING, encoding); + encodingDialog.setArguments(args); encodingDialog.setTargetFragment(this, 0); encodingDialog.show(getFragmentManager(), "select encoding dialog"); return true; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 0552cb6..90137ed 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -156,7 +156,10 @@ public class MessageDetailFragment extends Fragment { } List<Plaintext> parents = new ArrayList<>(item.getParents().size()); for (InventoryVector parentIV : item.getParents()) { - parents.add(messageRepo.getMessage(parentIV)); + Plaintext parent = messageRepo.getMessage(parentIV); + if (parent != null) { + parents.add(parent); + } } showRelatedMessages(rootView, R.id.parents, parents); showRelatedMessages(rootView, R.id.responses, messageRepo.findResponses(item)); @@ -165,7 +168,7 @@ public class MessageDetailFragment extends Fragment { } private void showRelatedMessages(View rootView, @IdRes int id, List<Plaintext> messages) { - RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.parents); + RecyclerView recyclerView = (RecyclerView) rootView.findViewById(id); RelatedMessageAdapter adapter = new RelatedMessageAdapter(getActivity(), messages); recyclerView.setAdapter(adapter); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java index b38cb1d..f305ffb 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.java @@ -201,9 +201,9 @@ public class AndroidMessageRepository extends AbstractMessageRepository { // There are no parents to save yet (they are saved in the extended data, that's enough for now) return; } - db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(message.getInitialHash()).toString()}); - byte[] childIV = message.getInventoryVector().getHash(); + db.delete(PARENTS_TABLE_NAME, "child=?", new String[]{hex(childIV).toString()}); + // save new parents int order = 0; for (InventoryVector parentIV : message.getParents()) { diff --git a/app/src/main/res/layout/fragment_message_detail.xml b/app/src/main/res/layout/fragment_message_detail.xml index e698bf5..73bb3d9 100644 --- a/app/src/main/res/layout/fragment_message_detail.xml +++ b/app/src/main/res/layout/fragment_message_detail.xml @@ -105,13 +105,14 @@ android:layout_height="wrap_content" android:layout_below="@+id/text" android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" /> + android:layout_marginRight="16dp" + android:layout_marginBottom="16dp"/> <android.support.v7.widget.RecyclerView android:id="@+id/responses" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:layout_below="@+id/text" + android:layout_below="@+id/labels" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" /> </RelativeLayout> From 30c5bf6b904f0f4210a31cc1cd61a888eb7fcedd Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Thu, 20 Apr 2017 23:24:28 +0200 Subject: [PATCH 14/18] Open related messages on click --- .../apps/abit/MessageDetailFragment.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 90137ed..ab851a9 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -17,6 +17,7 @@ package ch.dissem.apps.abit; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.support.annotation.IdRes; import android.support.v4.app.Fragment; @@ -263,6 +264,7 @@ public class MessageDetailFragment extends Fragment { viewHolder.status.setImageResource(Assets.getStatusDrawable(message.getStatus())); viewHolder.sender.setText(message.getFrom().toString()); viewHolder.extract.setText(normalizeWhitespaces(message.getText())); + viewHolder.item = message; } // Returns the total count of items in the list @@ -271,18 +273,33 @@ public class MessageDetailFragment extends Fragment { return messages.size(); } - static class ViewHolder extends RecyclerView.ViewHolder { + class ViewHolder extends RecyclerView.ViewHolder { private final ImageView avatar; private final ImageView status; private final TextView sender; private final TextView extract; + private Plaintext item; - ViewHolder(View itemView) { + ViewHolder(final View itemView) { super(itemView); avatar = (ImageView) itemView.findViewById(R.id.avatar); status = (ImageView) itemView.findViewById(R.id.status); sender = (TextView) itemView.findViewById(R.id.sender); extract = (TextView) itemView.findViewById(R.id.text); + itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (ctx instanceof MainActivity) { + ((MainActivity) ctx).onItemSelected(item); + } else { + Intent detailIntent; + detailIntent = new Intent(ctx, MessageDetailActivity.class); + detailIntent.putExtra(MessageDetailFragment.ARG_ITEM, item); + ctx.startActivity(detailIntent); + } + } + }); + } } } From 91cc90ec04ef0d756beb4fbe40ffe81345238ea3 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 21 Apr 2017 07:23:39 +0200 Subject: [PATCH 15/18] Fix unread badge for archive --- .../java/ch/dissem/apps/abit/MainActivity.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java index d744b99..f7d2c07 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MainActivity.java +++ b/app/src/main/java/ch/dissem/apps/abit/MainActivity.java @@ -78,6 +78,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; import static android.widget.Toast.LENGTH_LONG; import static ch.dissem.apps.abit.ComposeMessageActivity.launchReplyTo; +import static ch.dissem.apps.abit.repository.AndroidMessageRepository.LABEL_ARCHIVE; import static ch.dissem.apps.abit.service.BitmessageService.isRunning; @@ -321,7 +322,7 @@ public class MainActivity extends AppCompatActivity final ArrayList<IDrawerItem> drawerItems = new ArrayList<>(); drawerItems.add(new PrimaryDrawerItem() .withName(R.string.archive) - .withTag(AndroidMessageRepository.LABEL_ARCHIVE) + .withTag(LABEL_ARCHIVE) .withIcon(CommunityMaterial.Icon.cmd_archive) ); drawerItems.add(new DividerDrawerItem()); @@ -533,13 +534,15 @@ public class MainActivity extends AppCompatActivity for (IDrawerItem item : drawer.getDrawerItems()) { if (item.getTag() instanceof Label) { Label label = (Label) item.getTag(); - int unread = bmc.messages().countUnread(label); - if (unread > 0) { - ((PrimaryDrawerItem) item).withBadge(String.valueOf(unread)); - } else { - ((PrimaryDrawerItem) item).withBadge((String) null); + if (label != LABEL_ARCHIVE) { + int unread = bmc.messages().countUnread(label); + if (unread > 0) { + ((PrimaryDrawerItem) item).withBadge(String.valueOf(unread)); + } else { + ((PrimaryDrawerItem) item).withBadge((String) null); + } + drawer.updateItem(item); } - drawer.updateItem(item); } } } From d3f1e6abd27ad63c3f7166fa2918b5d7303bf8f8 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Fri, 21 Apr 2017 15:20:50 +0200 Subject: [PATCH 16/18] Minor layout improvements --- .../main/res/layout/item_message_minimized.xml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/item_message_minimized.xml b/app/src/main/res/layout/item_message_minimized.xml index f556959..0b2e209 100644 --- a/app/src/main/res/layout/item_message_minimized.xml +++ b/app/src/main/res/layout/item_message_minimized.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:padding="8dp" android:background="@drawable/border_bottom"> <ImageView @@ -11,7 +12,7 @@ android:layout_height="24dp" android:layout_alignParentStart="true" android:layout_alignParentTop="true" - android:layout_margin="16dp" + android:layout_marginTop="5dp" android:src="@color/colorPrimaryDark" tools:ignore="ContentDescription" /> @@ -20,16 +21,13 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_alignParentEnd="true" - android:layout_alignTop="@+id/avatar" - android:layout_marginTop="-5dp" + android:layout_alignParentTop="true" android:layout_toEndOf="@+id/avatar" android:ellipsize="end" android:lines="1" android:paddingBottom="0dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingTop="0dp" - android:textAppearance="?android:attr/textAppearanceMedium" + android:paddingStart="8dp" + android:paddingEnd="8dp" android:textStyle="bold" tools:text="Sender" /> @@ -44,8 +42,8 @@ android:gravity="center_vertical" android:lines="1" android:paddingBottom="8dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" + android:paddingStart="8dp" + android:paddingEnd="8dp" android:textAppearance="?android:attr/textAppearanceSmall" tools:text="Text" /> From d704a40b66c58d0d63b584ba0c5f9115982eb383 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Sat, 22 Apr 2017 07:42:20 +0200 Subject: [PATCH 17/18] Regularly call cleanup() --- app/build.gradle | 6 +++--- .../abit/dialog/SelectEncodingDialogFragment.java | 3 +-- .../apps/abit/service/BitmessageService.java | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cdd925d..6284311 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,7 +39,7 @@ android { //ext.jabitVersion = '2.0.4' ext.jabitVersion = 'feature-extended-encoding-SNAPSHOT' -ext.supportVersion = '25.2.0' +ext.supportVersion = '25.3.1' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) @@ -63,9 +63,9 @@ dependencies { compile('com.mikepenz:aboutlibraries:5.9.5@aar') { transitive = true } - compile "com.mikepenz:iconics-core:2.8.2@aar" + compile "com.mikepenz:iconics-core:2.8.3@aar" compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' - compile 'com.mikepenz:community-material-typeface:1.8.36.1@aar' + compile 'com.mikepenz:community-material-typeface:1.9.32.1@aar' compile 'com.journeyapps:zxing-android-embedded:3.5.0@aar' compile 'com.google.zxing:core:3.3.0' diff --git a/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java index 3b8b917..899ca0f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/dialog/SelectEncodingDialogFragment.java @@ -46,8 +46,7 @@ public class SelectEncodingDialogFragment extends AppCompatDialogFragment { @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle - savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (getArguments() != null && getArguments().containsKey(EXTRA_ENCODING)) { encoding = (Plaintext.Encoding) getArguments().getSerializable(EXTRA_ENCODING); } diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java index 5c8a528..ef64b61 100644 --- a/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java +++ b/app/src/main/java/ch/dissem/apps/abit/service/BitmessageService.java @@ -18,6 +18,7 @@ package ch.dissem.apps.abit.service; import android.app.Service; import android.content.Intent; +import android.os.Handler; import android.os.IBinder; import android.support.annotation.Nullable; @@ -38,6 +39,17 @@ public class BitmessageService extends Service { private NetworkNotification notification = null; + private final Handler cleanupHandler = new Handler(); + private final Runnable cleanupTask = new Runnable() { + @Override + public void run() { + bmc.cleanup(); + if (isRunning()) { + cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L); + } + } + }; + public static boolean isRunning() { return running && bmc.isRunning(); } @@ -61,6 +73,7 @@ public class BitmessageService extends Service { bmc.startup(); } notification.show(); + cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L); } return Service.START_STICKY; } @@ -72,6 +85,8 @@ public class BitmessageService extends Service { } running = false; notification.showShutdown(); + cleanupHandler.removeCallbacks(cleanupTask); + bmc.cleanup(); stopSelf(); } From d36ffe193973d4f10a28365d1a799f9a7b7d40f9 Mon Sep 17 00:00:00 2001 From: Christian Basler <chrigu.meyer@gmail.com> Date: Tue, 25 Apr 2017 08:07:33 +0200 Subject: [PATCH 18/18] Fixed layout - apparently ConstraintLayout doesn't work properly for dialogs yet, at least in this case --- .../layout/dialog_select_message_encoding.xml | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/app/src/main/res/layout/dialog_select_message_encoding.xml b/app/src/main/res/layout/dialog_select_message_encoding.xml index 4a1991f..02dcab0 100644 --- a/app/src/main/res/layout/dialog_select_message_encoding.xml +++ b/app/src/main/res/layout/dialog_select_message_encoding.xml @@ -15,7 +15,7 @@ ~ limitations under the License. --> -<android.support.constraint.ConstraintLayout +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" @@ -31,10 +31,9 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:text="@string/select_encoding_warning" - app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:layout_constraintLeft_creator="1" - tools:layout_constraintTop_creator="1"/> + android:layout_alignParentTop="true" + android:layout_alignParentStart="true" + android:layout_alignParentEnd="true"/> <RadioGroup @@ -43,9 +42,9 @@ android:layout_height="wrap_content" android:paddingBottom="24dp" android:paddingTop="24dp" - app:layout_constraintLeft_toLeftOf="@+id/description" - app:layout_constraintRight_toRightOf="@+id/description" - app:layout_constraintTop_toBottomOf="@+id/description"> + android:layout_alignStart="@+id/description" + android:layout_alignEnd="@+id/description" + android:layout_below="@+id/description"> <RadioButton android:id="@+id/simple" @@ -72,10 +71,8 @@ android:text="@string/ok" android:textColor="@color/colorAccent" app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintRight_toRightOf="@+id/radioGroup" - app:layout_constraintTop_toBottomOf="@+id/radioGroup" - tools:layout_constraintRight_creator="1" - tools:layout_constraintTop_creator="1"/> + android:layout_alignRight="@+id/radioGroup" + android:layout_below="@+id/radioGroup"/> <Button android:id="@+id/dismiss" @@ -84,7 +81,7 @@ android:layout_height="wrap_content" android:text="@string/cancel" android:textColor="@color/colorAccent" - app:layout_constraintRight_toLeftOf="@+id/ok" - app:layout_constraintTop_toBottomOf="@+id/radioGroup"/> + android:layout_toLeftOf="@+id/ok" + android:layout_below="@+id/radioGroup"/> -</android.support.constraint.ConstraintLayout> +</RelativeLayout>