From 42cf18445c0674c0c38e954596cd69f019151d1a Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Sat, 18 Mar 2017 07:09:03 +0100 Subject: [PATCH] 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