diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml
index 00c6f6c865..a34f4219d9 100644
--- a/.idea/dictionaries/bmarty.xml
+++ b/.idea/dictionaries/bmarty.xml
@@ -18,6 +18,7 @@
pbkdf
pkcs
signin
+ signout
signup
diff --git a/CHANGES.md b/CHANGES.md
index 654f2e4fa4..e752e1879c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,22 +2,24 @@ Changes in RiotX 0.11.0 (2019-XX-XX)
===================================================
Features ✨:
- -
+ - Implement soft logout (#281)
Improvements 🙌:
-
Other changes:
- -
+ - Use same default room colors than Riot-Web
Bugfix 🐛:
- -
+ - Scroll breadcrumbs to top when opened
+ - Render default room name when it starts with an emoji (#477)
+ - Do not display " (IRC)") in display names https://github.com/vector-im/riot-android/issues/444
Translations 🗣:
-
Build 🧱:
- -
+ - Include diff-match-patch sources as dependency
Changes in RiotX 0.10.0 (2019-12-10)
===================================================
diff --git a/build.gradle b/build.gradle
index 714152370e..29351e403f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ buildscript {
}
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.1'
+ classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.google.gms:google-services:4.3.2'
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
@@ -45,12 +45,6 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google()
jcenter()
- maven {
- url 'https://repo.adobe.com/nexus/content/repositories/public/'
- content {
- includeGroupByRegex "diff_match_patch"
- }
- }
}
tasks.withType(JavaCompile).all {
diff --git a/diff-match-patch/.gitignore b/diff-match-patch/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/diff-match-patch/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/diff-match-patch/build.gradle b/diff-match-patch/build.gradle
new file mode 100644
index 0000000000..82292e24db
--- /dev/null
+++ b/diff-match-patch/build.gradle
@@ -0,0 +1,8 @@
+apply plugin: 'java-library'
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+}
+
+sourceCompatibility = "8"
+targetCompatibility = "8"
diff --git a/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java b/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java
new file mode 100644
index 0000000000..9d07867de5
--- /dev/null
+++ b/diff-match-patch/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java
@@ -0,0 +1,2471 @@
+/*
+ * Diff Match and Patch
+ * Copyright 2018 The diff-match-patch Authors.
+ * https://github.com/google/diff-match-patch
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package name.fraser.neil.plaintext;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/*
+ * Functions for diff, match and patch.
+ * Computes the difference between two texts to create a patch.
+ * Applies the patch onto another text, allowing for errors.
+ *
+ * @author fraser@google.com (Neil Fraser)
+ */
+
+/**
+ * Class containing the diff, match and patch methods.
+ * Also contains the behaviour settings.
+ */
+public class diff_match_patch {
+
+ // Defaults.
+ // Set these on your diff_match_patch instance to override the defaults.
+
+ /**
+ * Number of seconds to map a diff before giving up (0 for infinity).
+ */
+ public float Diff_Timeout = 1.0f;
+ /**
+ * Cost of an empty edit operation in terms of edit characters.
+ */
+ public short Diff_EditCost = 4;
+ /**
+ * At what point is no match declared (0.0 = perfection, 1.0 = very loose).
+ */
+ public float Match_Threshold = 0.5f;
+ /**
+ * How far to search for a match (0 = exact location, 1000+ = broad match).
+ * A match this many characters away from the expected location will add
+ * 1.0 to the score (0.0 is a perfect match).
+ */
+ public int Match_Distance = 1000;
+ /**
+ * When deleting a large block of text (over ~64 characters), how close do
+ * the contents have to be to match the expected contents. (0.0 = perfection,
+ * 1.0 = very loose). Note that Match_Threshold controls how closely the
+ * end points of a delete need to match.
+ */
+ public float Patch_DeleteThreshold = 0.5f;
+ /**
+ * Chunk size for context length.
+ */
+ public short Patch_Margin = 4;
+
+ /**
+ * The number of bits in an int.
+ */
+ private short Match_MaxBits = 32;
+
+ /**
+ * Internal class for returning results from diff_linesToChars().
+ * Other less paranoid languages just use a three-element array.
+ */
+ protected static class LinesToCharsResult {
+ protected String chars1;
+ protected String chars2;
+ protected List lineArray;
+
+ protected LinesToCharsResult(String chars1, String chars2,
+ List lineArray) {
+ this.chars1 = chars1;
+ this.chars2 = chars2;
+ this.lineArray = lineArray;
+ }
+ }
+
+
+ // DIFF FUNCTIONS
+
+
+ /**
+ * The data structure representing a diff is a Linked list of Diff objects:
+ * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"),
+ * Diff(Operation.EQUAL, " world.")}
+ * which means: delete "Hello", add "Goodbye" and keep " world."
+ */
+ public enum Operation {
+ DELETE, INSERT, EQUAL
+ }
+
+ /**
+ * Find the differences between two texts.
+ * Run a faster, slightly less optimal diff.
+ * This method allows the 'checklines' of diff_main() to be optional.
+ * Most of the time checklines is wanted, so default to true.
+ * @param text1 Old string to be diffed.
+ * @param text2 New string to be diffed.
+ * @return Linked List of Diff objects.
+ */
+ public LinkedList diff_main(String text1, String text2) {
+ return diff_main(text1, text2, true);
+ }
+
+ /**
+ * Find the differences between two texts.
+ * @param text1 Old string to be diffed.
+ * @param text2 New string to be diffed.
+ * @param checklines Speedup flag. If false, then don't run a
+ * line-level diff first to identify the changed areas.
+ * If true, then run a faster slightly less optimal diff.
+ * @return Linked List of Diff objects.
+ */
+ public LinkedList diff_main(String text1, String text2,
+ boolean checklines) {
+ // Set a deadline by which time the diff must be complete.
+ long deadline;
+ if (Diff_Timeout <= 0) {
+ deadline = Long.MAX_VALUE;
+ } else {
+ deadline = System.currentTimeMillis() + (long) (Diff_Timeout * 1000);
+ }
+ return diff_main(text1, text2, checklines, deadline);
+ }
+
+ /**
+ * Find the differences between two texts. Simplifies the problem by
+ * stripping any common prefix or suffix off the texts before diffing.
+ * @param text1 Old string to be diffed.
+ * @param text2 New string to be diffed.
+ * @param checklines Speedup flag. If false, then don't run a
+ * line-level diff first to identify the changed areas.
+ * If true, then run a faster slightly less optimal diff.
+ * @param deadline Time when the diff should be complete by. Used
+ * internally for recursive calls. Users should set DiffTimeout instead.
+ * @return Linked List of Diff objects.
+ */
+ private LinkedList diff_main(String text1, String text2,
+ boolean checklines, long deadline) {
+ // Check for null inputs.
+ if (text1 == null || text2 == null) {
+ throw new IllegalArgumentException("Null inputs. (diff_main)");
+ }
+
+ // Check for equality (speedup).
+ LinkedList diffs;
+ if (text1.equals(text2)) {
+ diffs = new LinkedList();
+ if (text1.length() != 0) {
+ diffs.add(new Diff(Operation.EQUAL, text1));
+ }
+ return diffs;
+ }
+
+ // Trim off common prefix (speedup).
+ int commonlength = diff_commonPrefix(text1, text2);
+ String commonprefix = text1.substring(0, commonlength);
+ text1 = text1.substring(commonlength);
+ text2 = text2.substring(commonlength);
+
+ // Trim off common suffix (speedup).
+ commonlength = diff_commonSuffix(text1, text2);
+ String commonsuffix = text1.substring(text1.length() - commonlength);
+ text1 = text1.substring(0, text1.length() - commonlength);
+ text2 = text2.substring(0, text2.length() - commonlength);
+
+ // Compute the diff on the middle block.
+ diffs = diff_compute(text1, text2, checklines, deadline);
+
+ // Restore the prefix and suffix.
+ if (commonprefix.length() != 0) {
+ diffs.addFirst(new Diff(Operation.EQUAL, commonprefix));
+ }
+ if (commonsuffix.length() != 0) {
+ diffs.addLast(new Diff(Operation.EQUAL, commonsuffix));
+ }
+
+ diff_cleanupMerge(diffs);
+ return diffs;
+ }
+
+ /**
+ * Find the differences between two texts. Assumes that the texts do not
+ * have any common prefix or suffix.
+ * @param text1 Old string to be diffed.
+ * @param text2 New string to be diffed.
+ * @param checklines Speedup flag. If false, then don't run a
+ * line-level diff first to identify the changed areas.
+ * If true, then run a faster slightly less optimal diff.
+ * @param deadline Time when the diff should be complete by.
+ * @return Linked List of Diff objects.
+ */
+ private LinkedList diff_compute(String text1, String text2,
+ boolean checklines, long deadline) {
+ LinkedList diffs = new LinkedList();
+
+ if (text1.length() == 0) {
+ // Just add some text (speedup).
+ diffs.add(new Diff(Operation.INSERT, text2));
+ return diffs;
+ }
+
+ if (text2.length() == 0) {
+ // Just delete some text (speedup).
+ diffs.add(new Diff(Operation.DELETE, text1));
+ return diffs;
+ }
+
+ String longtext = text1.length() > text2.length() ? text1 : text2;
+ String shorttext = text1.length() > text2.length() ? text2 : text1;
+ int i = longtext.indexOf(shorttext);
+ if (i != -1) {
+ // Shorter text is inside the longer text (speedup).
+ Operation op = (text1.length() > text2.length()) ?
+ Operation.DELETE : Operation.INSERT;
+ diffs.add(new Diff(op, longtext.substring(0, i)));
+ diffs.add(new Diff(Operation.EQUAL, shorttext));
+ diffs.add(new Diff(op, longtext.substring(i + shorttext.length())));
+ return diffs;
+ }
+
+ if (shorttext.length() == 1) {
+ // Single character string.
+ // After the previous speedup, the character can't be an equality.
+ diffs.add(new Diff(Operation.DELETE, text1));
+ diffs.add(new Diff(Operation.INSERT, text2));
+ return diffs;
+ }
+
+ // Check to see if the problem can be split in two.
+ String[] hm = diff_halfMatch(text1, text2);
+ if (hm != null) {
+ // A half-match was found, sort out the return data.
+ String text1_a = hm[0];
+ String text1_b = hm[1];
+ String text2_a = hm[2];
+ String text2_b = hm[3];
+ String mid_common = hm[4];
+ // Send both pairs off for separate processing.
+ LinkedList diffs_a = diff_main(text1_a, text2_a,
+ checklines, deadline);
+ LinkedList diffs_b = diff_main(text1_b, text2_b,
+ checklines, deadline);
+ // Merge the results.
+ diffs = diffs_a;
+ diffs.add(new Diff(Operation.EQUAL, mid_common));
+ diffs.addAll(diffs_b);
+ return diffs;
+ }
+
+ if (checklines && text1.length() > 100 && text2.length() > 100) {
+ return diff_lineMode(text1, text2, deadline);
+ }
+
+ return diff_bisect(text1, text2, deadline);
+ }
+
+ /**
+ * Do a quick line-level diff on both strings, then rediff the parts for
+ * greater accuracy.
+ * This speedup can produce non-minimal diffs.
+ * @param text1 Old string to be diffed.
+ * @param text2 New string to be diffed.
+ * @param deadline Time when the diff should be complete by.
+ * @return Linked List of Diff objects.
+ */
+ private LinkedList diff_lineMode(String text1, String text2,
+ long deadline) {
+ // Scan the text on a line-by-line basis first.
+ LinesToCharsResult a = diff_linesToChars(text1, text2);
+ text1 = a.chars1;
+ text2 = a.chars2;
+ List linearray = a.lineArray;
+
+ LinkedList diffs = diff_main(text1, text2, false, deadline);
+
+ // Convert the diff back to original text.
+ diff_charsToLines(diffs, linearray);
+ // Eliminate freak matches (e.g. blank lines)
+ diff_cleanupSemantic(diffs);
+
+ // Rediff any replacement blocks, this time character-by-character.
+ // Add a dummy entry at the end.
+ diffs.add(new Diff(Operation.EQUAL, ""));
+ int count_delete = 0;
+ int count_insert = 0;
+ String text_delete = "";
+ String text_insert = "";
+ ListIterator pointer = diffs.listIterator();
+ Diff thisDiff = pointer.next();
+ while (thisDiff != null) {
+ switch (thisDiff.operation) {
+ case INSERT:
+ count_insert++;
+ text_insert += thisDiff.text;
+ break;
+ case DELETE:
+ count_delete++;
+ text_delete += thisDiff.text;
+ break;
+ case EQUAL:
+ // Upon reaching an equality, check for prior redundancies.
+ if (count_delete >= 1 && count_insert >= 1) {
+ // Delete the offending records and add the merged ones.
+ pointer.previous();
+ for (int j = 0; j < count_delete + count_insert; j++) {
+ pointer.previous();
+ pointer.remove();
+ }
+ for (Diff subDiff : diff_main(text_delete, text_insert, false,
+ deadline)) {
+ pointer.add(subDiff);
+ }
+ }
+ count_insert = 0;
+ count_delete = 0;
+ text_delete = "";
+ text_insert = "";
+ break;
+ }
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+ diffs.removeLast(); // Remove the dummy entry at the end.
+
+ return diffs;
+ }
+
+ /**
+ * Find the 'middle snake' of a diff, split the problem in two
+ * and return the recursively constructed diff.
+ * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+ * @param text1 Old string to be diffed.
+ * @param text2 New string to be diffed.
+ * @param deadline Time at which to bail if not yet complete.
+ * @return LinkedList of Diff objects.
+ */
+ protected LinkedList diff_bisect(String text1, String text2,
+ long deadline) {
+ // Cache the text lengths to prevent multiple calls.
+ int text1_length = text1.length();
+ int text2_length = text2.length();
+ int max_d = (text1_length + text2_length + 1) / 2;
+ int v_offset = max_d;
+ int v_length = 2 * max_d;
+ int[] v1 = new int[v_length];
+ int[] v2 = new int[v_length];
+ for (int x = 0; x < v_length; x++) {
+ v1[x] = -1;
+ v2[x] = -1;
+ }
+ v1[v_offset + 1] = 0;
+ v2[v_offset + 1] = 0;
+ int delta = text1_length - text2_length;
+ // If the total number of characters is odd, then the front path will
+ // collide with the reverse path.
+ boolean front = (delta % 2 != 0);
+ // Offsets for start and end of k loop.
+ // Prevents mapping of space beyond the grid.
+ int k1start = 0;
+ int k1end = 0;
+ int k2start = 0;
+ int k2end = 0;
+ for (int d = 0; d < max_d; d++) {
+ // Bail out if deadline is reached.
+ if (System.currentTimeMillis() > deadline) {
+ break;
+ }
+
+ // Walk the front path one step.
+ for (int k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+ int k1_offset = v_offset + k1;
+ int x1;
+ if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
+ x1 = v1[k1_offset + 1];
+ } else {
+ x1 = v1[k1_offset - 1] + 1;
+ }
+ int y1 = x1 - k1;
+ while (x1 < text1_length && y1 < text2_length
+ && text1.charAt(x1) == text2.charAt(y1)) {
+ x1++;
+ y1++;
+ }
+ v1[k1_offset] = x1;
+ if (x1 > text1_length) {
+ // Ran off the right of the graph.
+ k1end += 2;
+ } else if (y1 > text2_length) {
+ // Ran off the bottom of the graph.
+ k1start += 2;
+ } else if (front) {
+ int k2_offset = v_offset + delta - k1;
+ if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
+ // Mirror x2 onto top-left coordinate system.
+ int x2 = text1_length - v2[k2_offset];
+ if (x1 >= x2) {
+ // Overlap detected.
+ return diff_bisectSplit(text1, text2, x1, y1, deadline);
+ }
+ }
+ }
+ }
+
+ // Walk the reverse path one step.
+ for (int k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+ int k2_offset = v_offset + k2;
+ int x2;
+ if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
+ x2 = v2[k2_offset + 1];
+ } else {
+ x2 = v2[k2_offset - 1] + 1;
+ }
+ int y2 = x2 - k2;
+ while (x2 < text1_length && y2 < text2_length
+ && text1.charAt(text1_length - x2 - 1)
+ == text2.charAt(text2_length - y2 - 1)) {
+ x2++;
+ y2++;
+ }
+ v2[k2_offset] = x2;
+ if (x2 > text1_length) {
+ // Ran off the left of the graph.
+ k2end += 2;
+ } else if (y2 > text2_length) {
+ // Ran off the top of the graph.
+ k2start += 2;
+ } else if (!front) {
+ int k1_offset = v_offset + delta - k2;
+ if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
+ int x1 = v1[k1_offset];
+ int y1 = v_offset + x1 - k1_offset;
+ // Mirror x2 onto top-left coordinate system.
+ x2 = text1_length - x2;
+ if (x1 >= x2) {
+ // Overlap detected.
+ return diff_bisectSplit(text1, text2, x1, y1, deadline);
+ }
+ }
+ }
+ }
+ }
+ // Diff took too long and hit the deadline or
+ // number of diffs equals number of characters, no commonality at all.
+ LinkedList diffs = new LinkedList();
+ diffs.add(new Diff(Operation.DELETE, text1));
+ diffs.add(new Diff(Operation.INSERT, text2));
+ return diffs;
+ }
+
+ /**
+ * Given the location of the 'middle snake', split the diff in two parts
+ * and recurse.
+ * @param text1 Old string to be diffed.
+ * @param text2 New string to be diffed.
+ * @param x Index of split point in text1.
+ * @param y Index of split point in text2.
+ * @param deadline Time at which to bail if not yet complete.
+ * @return LinkedList of Diff objects.
+ */
+ private LinkedList diff_bisectSplit(String text1, String text2,
+ int x, int y, long deadline) {
+ String text1a = text1.substring(0, x);
+ String text2a = text2.substring(0, y);
+ String text1b = text1.substring(x);
+ String text2b = text2.substring(y);
+
+ // Compute both diffs serially.
+ LinkedList diffs = diff_main(text1a, text2a, false, deadline);
+ LinkedList diffsb = diff_main(text1b, text2b, false, deadline);
+
+ diffs.addAll(diffsb);
+ return diffs;
+ }
+
+ /**
+ * Split two texts into a list of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * @param text1 First string.
+ * @param text2 Second string.
+ * @return An object containing the encoded text1, the encoded text2 and
+ * the List of unique strings. The zeroth element of the List of
+ * unique strings is intentionally blank.
+ */
+ protected LinesToCharsResult diff_linesToChars(String text1, String text2) {
+ List lineArray = new ArrayList();
+ Map lineHash = new HashMap();
+ // e.g. linearray[4] == "Hello\n"
+ // e.g. linehash.get("Hello\n") == 4
+
+ // "\x00" is a valid character, but various debuggers don't like it.
+ // So we'll insert a junk entry to avoid generating a null character.
+ lineArray.add("");
+
+ // Allocate 2/3rds of the space for text1, the rest for text2.
+ String chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash, 40000);
+ String chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash, 65535);
+ return new LinesToCharsResult(chars1, chars2, lineArray);
+ }
+
+ /**
+ * Split a text into a list of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * @param text String to encode.
+ * @param lineArray List of unique strings.
+ * @param lineHash Map of strings to indices.
+ * @param maxLines Maximum length of lineArray.
+ * @return Encoded string.
+ */
+ private String diff_linesToCharsMunge(String text, List lineArray,
+ Map lineHash, int maxLines) {
+ int lineStart = 0;
+ int lineEnd = -1;
+ String line;
+ StringBuilder chars = new StringBuilder();
+ // Walk the text, pulling out a substring for each line.
+ // text.split('\n') would would temporarily double our memory footprint.
+ // Modifying text would create many large strings to garbage collect.
+ while (lineEnd < text.length() - 1) {
+ lineEnd = text.indexOf('\n', lineStart);
+ if (lineEnd == -1) {
+ lineEnd = text.length() - 1;
+ }
+ line = text.substring(lineStart, lineEnd + 1);
+
+ if (lineHash.containsKey(line)) {
+ chars.append(String.valueOf((char) (int) lineHash.get(line)));
+ } else {
+ if (lineArray.size() == maxLines) {
+ // Bail out at 65535 because
+ // String.valueOf((char) 65536).equals(String.valueOf(((char) 0)))
+ line = text.substring(lineStart);
+ lineEnd = text.length();
+ }
+ lineArray.add(line);
+ lineHash.put(line, lineArray.size() - 1);
+ chars.append(String.valueOf((char) (lineArray.size() - 1)));
+ }
+ lineStart = lineEnd + 1;
+ }
+ return chars.toString();
+ }
+
+ /**
+ * Rehydrate the text in a diff from a string of line hashes to real lines of
+ * text.
+ * @param diffs List of Diff objects.
+ * @param lineArray List of unique strings.
+ */
+ protected void diff_charsToLines(List diffs,
+ List lineArray) {
+ StringBuilder text;
+ for (Diff diff : diffs) {
+ text = new StringBuilder();
+ for (int j = 0; j < diff.text.length(); j++) {
+ text.append(lineArray.get(diff.text.charAt(j)));
+ }
+ diff.text = text.toString();
+ }
+ }
+
+ /**
+ * Determine the common prefix of two strings
+ * @param text1 First string.
+ * @param text2 Second string.
+ * @return The number of characters common to the start of each string.
+ */
+ public int diff_commonPrefix(String text1, String text2) {
+ // Performance analysis: https://neil.fraser.name/news/2007/10/09/
+ int n = Math.min(text1.length(), text2.length());
+ for (int i = 0; i < n; i++) {
+ if (text1.charAt(i) != text2.charAt(i)) {
+ return i;
+ }
+ }
+ return n;
+ }
+
+ /**
+ * Determine the common suffix of two strings
+ * @param text1 First string.
+ * @param text2 Second string.
+ * @return The number of characters common to the end of each string.
+ */
+ public int diff_commonSuffix(String text1, String text2) {
+ // Performance analysis: https://neil.fraser.name/news/2007/10/09/
+ int text1_length = text1.length();
+ int text2_length = text2.length();
+ int n = Math.min(text1_length, text2_length);
+ for (int i = 1; i <= n; i++) {
+ if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) {
+ return i - 1;
+ }
+ }
+ return n;
+ }
+
+ /**
+ * Determine if the suffix of one string is the prefix of another.
+ * @param text1 First string.
+ * @param text2 Second string.
+ * @return The number of characters common to the end of the first
+ * string and the start of the second string.
+ */
+ protected int diff_commonOverlap(String text1, String text2) {
+ // Cache the text lengths to prevent multiple calls.
+ int text1_length = text1.length();
+ int text2_length = text2.length();
+ // Eliminate the null case.
+ if (text1_length == 0 || text2_length == 0) {
+ return 0;
+ }
+ // Truncate the longer string.
+ if (text1_length > text2_length) {
+ text1 = text1.substring(text1_length - text2_length);
+ } else if (text1_length < text2_length) {
+ text2 = text2.substring(0, text1_length);
+ }
+ int text_length = Math.min(text1_length, text2_length);
+ // Quick check for the worst case.
+ if (text1.equals(text2)) {
+ return text_length;
+ }
+
+ // Start by looking for a single character match
+ // and increase length until no match is found.
+ // Performance analysis: https://neil.fraser.name/news/2010/11/04/
+ int best = 0;
+ int length = 1;
+ while (true) {
+ String pattern = text1.substring(text_length - length);
+ int found = text2.indexOf(pattern);
+ if (found == -1) {
+ return best;
+ }
+ length += found;
+ if (found == 0 || text1.substring(text_length - length).equals(
+ text2.substring(0, length))) {
+ best = length;
+ length++;
+ }
+ }
+ }
+
+ /**
+ * Do the two texts share a substring which is at least half the length of
+ * the longer text?
+ * This speedup can produce non-minimal diffs.
+ * @param text1 First string.
+ * @param text2 Second string.
+ * @return Five element String array, containing the prefix of text1, the
+ * suffix of text1, the prefix of text2, the suffix of text2 and the
+ * common middle. Or null if there was no match.
+ */
+ protected String[] diff_halfMatch(String text1, String text2) {
+ if (Diff_Timeout <= 0) {
+ // Don't risk returning a non-optimal diff if we have unlimited time.
+ return null;
+ }
+ String longtext = text1.length() > text2.length() ? text1 : text2;
+ String shorttext = text1.length() > text2.length() ? text2 : text1;
+ if (longtext.length() < 4 || shorttext.length() * 2 < longtext.length()) {
+ return null; // Pointless.
+ }
+
+ // First check if the second quarter is the seed for a half-match.
+ String[] hm1 = diff_halfMatchI(longtext, shorttext,
+ (longtext.length() + 3) / 4);
+ // Check again based on the third quarter.
+ String[] hm2 = diff_halfMatchI(longtext, shorttext,
+ (longtext.length() + 1) / 2);
+ String[] hm;
+ if (hm1 == null && hm2 == null) {
+ return null;
+ } else if (hm2 == null) {
+ hm = hm1;
+ } else if (hm1 == null) {
+ hm = hm2;
+ } else {
+ // Both matched. Select the longest.
+ hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2;
+ }
+
+ // A half-match was found, sort out the return data.
+ if (text1.length() > text2.length()) {
+ return hm;
+ //return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]};
+ } else {
+ return new String[]{hm[2], hm[3], hm[0], hm[1], hm[4]};
+ }
+ }
+
+ /**
+ * Does a substring of shorttext exist within longtext such that the
+ * substring is at least half the length of longtext?
+ * @param longtext Longer string.
+ * @param shorttext Shorter string.
+ * @param i Start index of quarter length substring within longtext.
+ * @return Five element String array, containing the prefix of longtext, the
+ * suffix of longtext, the prefix of shorttext, the suffix of shorttext
+ * and the common middle. Or null if there was no match.
+ */
+ private String[] diff_halfMatchI(String longtext, String shorttext, int i) {
+ // Start with a 1/4 length substring at position i as a seed.
+ String seed = longtext.substring(i, i + longtext.length() / 4);
+ int j = -1;
+ String best_common = "";
+ String best_longtext_a = "", best_longtext_b = "";
+ String best_shorttext_a = "", best_shorttext_b = "";
+ while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
+ int prefixLength = diff_commonPrefix(longtext.substring(i),
+ shorttext.substring(j));
+ int suffixLength = diff_commonSuffix(longtext.substring(0, i),
+ shorttext.substring(0, j));
+ if (best_common.length() < suffixLength + prefixLength) {
+ best_common = shorttext.substring(j - suffixLength, j)
+ + shorttext.substring(j, j + prefixLength);
+ best_longtext_a = longtext.substring(0, i - suffixLength);
+ best_longtext_b = longtext.substring(i + prefixLength);
+ best_shorttext_a = shorttext.substring(0, j - suffixLength);
+ best_shorttext_b = shorttext.substring(j + prefixLength);
+ }
+ }
+ if (best_common.length() * 2 >= longtext.length()) {
+ return new String[]{best_longtext_a, best_longtext_b,
+ best_shorttext_a, best_shorttext_b, best_common};
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Reduce the number of edits by eliminating semantically trivial equalities.
+ * @param diffs LinkedList of Diff objects.
+ */
+ public void diff_cleanupSemantic(LinkedList diffs) {
+ if (diffs.isEmpty()) {
+ return;
+ }
+ boolean changes = false;
+ Deque equalities = new ArrayDeque(); // Double-ended queue of qualities.
+ String lastEquality = null; // Always equal to equalities.peek().text
+ ListIterator pointer = diffs.listIterator();
+ // Number of characters that changed prior to the equality.
+ int length_insertions1 = 0;
+ int length_deletions1 = 0;
+ // Number of characters that changed after the equality.
+ int length_insertions2 = 0;
+ int length_deletions2 = 0;
+ Diff thisDiff = pointer.next();
+ while (thisDiff != null) {
+ if (thisDiff.operation == Operation.EQUAL) {
+ // Equality found.
+ equalities.push(thisDiff);
+ length_insertions1 = length_insertions2;
+ length_deletions1 = length_deletions2;
+ length_insertions2 = 0;
+ length_deletions2 = 0;
+ lastEquality = thisDiff.text;
+ } else {
+ // An insertion or deletion.
+ if (thisDiff.operation == Operation.INSERT) {
+ length_insertions2 += thisDiff.text.length();
+ } else {
+ length_deletions2 += thisDiff.text.length();
+ }
+ // Eliminate an equality that is smaller or equal to the edits on both
+ // sides of it.
+ if (lastEquality != null && (lastEquality.length()
+ <= Math.max(length_insertions1, length_deletions1))
+ && (lastEquality.length()
+ <= Math.max(length_insertions2, length_deletions2))) {
+ //System.out.println("Splitting: '" + lastEquality + "'");
+ // Walk back to offending equality.
+ while (thisDiff != equalities.peek()) {
+ thisDiff = pointer.previous();
+ }
+ pointer.next();
+
+ // Replace equality with a delete.
+ pointer.set(new Diff(Operation.DELETE, lastEquality));
+ // Insert a corresponding an insert.
+ pointer.add(new Diff(Operation.INSERT, lastEquality));
+
+ equalities.pop(); // Throw away the equality we just deleted.
+ if (!equalities.isEmpty()) {
+ // Throw away the previous equality (it needs to be reevaluated).
+ equalities.pop();
+ }
+ if (equalities.isEmpty()) {
+ // There are no previous equalities, walk back to the start.
+ while (pointer.hasPrevious()) {
+ pointer.previous();
+ }
+ } else {
+ // There is a safe equality we can fall back to.
+ thisDiff = equalities.peek();
+ while (thisDiff != pointer.previous()) {
+ // Intentionally empty loop.
+ }
+ }
+
+ length_insertions1 = 0; // Reset the counters.
+ length_insertions2 = 0;
+ length_deletions1 = 0;
+ length_deletions2 = 0;
+ lastEquality = null;
+ changes = true;
+ }
+ }
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+
+ // Normalize the diff.
+ if (changes) {
+ diff_cleanupMerge(diffs);
+ }
+ diff_cleanupSemanticLossless(diffs);
+
+ // Find any overlaps between deletions and insertions.
+ // e.g: abcxxxxxxdef
+ // -> abcxxxdef
+ // e.g: xxxabcdefxxx
+ // -> defxxxabc
+ // Only extract an overlap if it is as big as the edit ahead or behind it.
+ pointer = diffs.listIterator();
+ Diff prevDiff = null;
+ thisDiff = null;
+ if (pointer.hasNext()) {
+ prevDiff = pointer.next();
+ if (pointer.hasNext()) {
+ thisDiff = pointer.next();
+ }
+ }
+ while (thisDiff != null) {
+ if (prevDiff.operation == Operation.DELETE &&
+ thisDiff.operation == Operation.INSERT) {
+ String deletion = prevDiff.text;
+ String insertion = thisDiff.text;
+ int overlap_length1 = this.diff_commonOverlap(deletion, insertion);
+ int overlap_length2 = this.diff_commonOverlap(insertion, deletion);
+ if (overlap_length1 >= overlap_length2) {
+ if (overlap_length1 >= deletion.length() / 2.0 ||
+ overlap_length1 >= insertion.length() / 2.0) {
+ // Overlap found. Insert an equality and trim the surrounding edits.
+ pointer.previous();
+ pointer.add(new Diff(Operation.EQUAL,
+ insertion.substring(0, overlap_length1)));
+ prevDiff.text =
+ deletion.substring(0, deletion.length() - overlap_length1);
+ thisDiff.text = insertion.substring(overlap_length1);
+ // pointer.add inserts the element before the cursor, so there is
+ // no need to step past the new element.
+ }
+ } else {
+ if (overlap_length2 >= deletion.length() / 2.0 ||
+ overlap_length2 >= insertion.length() / 2.0) {
+ // Reverse overlap found.
+ // Insert an equality and swap and trim the surrounding edits.
+ pointer.previous();
+ pointer.add(new Diff(Operation.EQUAL,
+ deletion.substring(0, overlap_length2)));
+ prevDiff.operation = Operation.INSERT;
+ prevDiff.text =
+ insertion.substring(0, insertion.length() - overlap_length2);
+ thisDiff.operation = Operation.DELETE;
+ thisDiff.text = deletion.substring(overlap_length2);
+ // pointer.add inserts the element before the cursor, so there is
+ // no need to step past the new element.
+ }
+ }
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+ prevDiff = thisDiff;
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+ }
+
+ /**
+ * Look for single edits surrounded on both sides by equalities
+ * which can be shifted sideways to align the edit to a word boundary.
+ * e.g: The cat came. -> The cat came.
+ * @param diffs LinkedList of Diff objects.
+ */
+ public void diff_cleanupSemanticLossless(LinkedList diffs) {
+ String equality1, edit, equality2;
+ String commonString;
+ int commonOffset;
+ int score, bestScore;
+ String bestEquality1, bestEdit, bestEquality2;
+ // Create a new iterator at the start.
+ ListIterator pointer = diffs.listIterator();
+ Diff prevDiff = pointer.hasNext() ? pointer.next() : null;
+ Diff thisDiff = pointer.hasNext() ? pointer.next() : null;
+ Diff nextDiff = pointer.hasNext() ? pointer.next() : null;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (nextDiff != null) {
+ if (prevDiff.operation == Operation.EQUAL &&
+ nextDiff.operation == Operation.EQUAL) {
+ // This is a single edit surrounded by equalities.
+ equality1 = prevDiff.text;
+ edit = thisDiff.text;
+ equality2 = nextDiff.text;
+
+ // First, shift the edit as far left as possible.
+ commonOffset = diff_commonSuffix(equality1, edit);
+ if (commonOffset != 0) {
+ commonString = edit.substring(edit.length() - commonOffset);
+ equality1 = equality1.substring(0, equality1.length() - commonOffset);
+ edit = commonString + edit.substring(0, edit.length() - commonOffset);
+ equality2 = commonString + equality2;
+ }
+
+ // Second, step character by character right, looking for the best fit.
+ bestEquality1 = equality1;
+ bestEdit = edit;
+ bestEquality2 = equality2;
+ bestScore = diff_cleanupSemanticScore(equality1, edit)
+ + diff_cleanupSemanticScore(edit, equality2);
+ while (edit.length() != 0 && equality2.length() != 0
+ && edit.charAt(0) == equality2.charAt(0)) {
+ equality1 += edit.charAt(0);
+ edit = edit.substring(1) + equality2.charAt(0);
+ equality2 = equality2.substring(1);
+ score = diff_cleanupSemanticScore(equality1, edit)
+ + diff_cleanupSemanticScore(edit, equality2);
+ // The >= encourages trailing rather than leading whitespace on edits.
+ if (score >= bestScore) {
+ bestScore = score;
+ bestEquality1 = equality1;
+ bestEdit = edit;
+ bestEquality2 = equality2;
+ }
+ }
+
+ if (!prevDiff.text.equals(bestEquality1)) {
+ // We have an improvement, save it back to the diff.
+ if (bestEquality1.length() != 0) {
+ prevDiff.text = bestEquality1;
+ } else {
+ pointer.previous(); // Walk past nextDiff.
+ pointer.previous(); // Walk past thisDiff.
+ pointer.previous(); // Walk past prevDiff.
+ pointer.remove(); // Delete prevDiff.
+ pointer.next(); // Walk past thisDiff.
+ pointer.next(); // Walk past nextDiff.
+ }
+ thisDiff.text = bestEdit;
+ if (bestEquality2.length() != 0) {
+ nextDiff.text = bestEquality2;
+ } else {
+ pointer.remove(); // Delete nextDiff.
+ nextDiff = thisDiff;
+ thisDiff = prevDiff;
+ }
+ }
+ }
+ prevDiff = thisDiff;
+ thisDiff = nextDiff;
+ nextDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+ }
+
+ /**
+ * Given two strings, compute a score representing whether the internal
+ * boundary falls on logical boundaries.
+ * Scores range from 6 (best) to 0 (worst).
+ * @param one First string.
+ * @param two Second string.
+ * @return The score.
+ */
+ private int diff_cleanupSemanticScore(String one, String two) {
+ if (one.length() == 0 || two.length() == 0) {
+ // Edges are the best.
+ return 6;
+ }
+
+ // Each port of this function behaves slightly differently due to
+ // subtle differences in each language's definition of things like
+ // 'whitespace'. Since this function's purpose is largely cosmetic,
+ // the choice has been made to use each language's native features
+ // rather than force total conformity.
+ char char1 = one.charAt(one.length() - 1);
+ char char2 = two.charAt(0);
+ boolean nonAlphaNumeric1 = !Character.isLetterOrDigit(char1);
+ boolean nonAlphaNumeric2 = !Character.isLetterOrDigit(char2);
+ boolean whitespace1 = nonAlphaNumeric1 && Character.isWhitespace(char1);
+ boolean whitespace2 = nonAlphaNumeric2 && Character.isWhitespace(char2);
+ boolean lineBreak1 = whitespace1
+ && Character.getType(char1) == Character.CONTROL;
+ boolean lineBreak2 = whitespace2
+ && Character.getType(char2) == Character.CONTROL;
+ boolean blankLine1 = lineBreak1 && BLANKLINEEND.matcher(one).find();
+ boolean blankLine2 = lineBreak2 && BLANKLINESTART.matcher(two).find();
+
+ if (blankLine1 || blankLine2) {
+ // Five points for blank lines.
+ return 5;
+ } else if (lineBreak1 || lineBreak2) {
+ // Four points for line breaks.
+ return 4;
+ } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) {
+ // Three points for end of sentences.
+ return 3;
+ } else if (whitespace1 || whitespace2) {
+ // Two points for whitespace.
+ return 2;
+ } else if (nonAlphaNumeric1 || nonAlphaNumeric2) {
+ // One point for non-alphanumeric.
+ return 1;
+ }
+ return 0;
+ }
+
+ // Define some regex patterns for matching boundaries.
+ private Pattern BLANKLINEEND
+ = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL);
+ private Pattern BLANKLINESTART
+ = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL);
+
+ /**
+ * Reduce the number of edits by eliminating operationally trivial equalities.
+ * @param diffs LinkedList of Diff objects.
+ */
+ public void diff_cleanupEfficiency(LinkedList diffs) {
+ if (diffs.isEmpty()) {
+ return;
+ }
+ boolean changes = false;
+ Deque equalities = new ArrayDeque(); // Double-ended queue of equalities.
+ String lastEquality = null; // Always equal to equalities.peek().text
+ ListIterator pointer = diffs.listIterator();
+ // Is there an insertion operation before the last equality.
+ boolean pre_ins = false;
+ // Is there a deletion operation before the last equality.
+ boolean pre_del = false;
+ // Is there an insertion operation after the last equality.
+ boolean post_ins = false;
+ // Is there a deletion operation after the last equality.
+ boolean post_del = false;
+ Diff thisDiff = pointer.next();
+ Diff safeDiff = thisDiff; // The last Diff that is known to be unsplittable.
+ while (thisDiff != null) {
+ if (thisDiff.operation == Operation.EQUAL) {
+ // Equality found.
+ if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) {
+ // Candidate found.
+ equalities.push(thisDiff);
+ pre_ins = post_ins;
+ pre_del = post_del;
+ lastEquality = thisDiff.text;
+ } else {
+ // Not a candidate, and can never become one.
+ equalities.clear();
+ lastEquality = null;
+ safeDiff = thisDiff;
+ }
+ post_ins = post_del = false;
+ } else {
+ // An insertion or deletion.
+ if (thisDiff.operation == Operation.DELETE) {
+ post_del = true;
+ } else {
+ post_ins = true;
+ }
+ /*
+ * Five types to be split:
+ * ABXYCD
+ * AXCD
+ * ABXC
+ * AXCD
+ * ABXC
+ */
+ if (lastEquality != null
+ && ((pre_ins && pre_del && post_ins && post_del)
+ || ((lastEquality.length() < Diff_EditCost / 2)
+ && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0)
+ + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) {
+ //System.out.println("Splitting: '" + lastEquality + "'");
+ // Walk back to offending equality.
+ while (thisDiff != equalities.peek()) {
+ thisDiff = pointer.previous();
+ }
+ pointer.next();
+
+ // Replace equality with a delete.
+ pointer.set(new Diff(Operation.DELETE, lastEquality));
+ // Insert a corresponding an insert.
+ pointer.add(thisDiff = new Diff(Operation.INSERT, lastEquality));
+
+ equalities.pop(); // Throw away the equality we just deleted.
+ lastEquality = null;
+ if (pre_ins && pre_del) {
+ // No changes made which could affect previous entry, keep going.
+ post_ins = post_del = true;
+ equalities.clear();
+ safeDiff = thisDiff;
+ } else {
+ if (!equalities.isEmpty()) {
+ // Throw away the previous equality (it needs to be reevaluated).
+ equalities.pop();
+ }
+ if (equalities.isEmpty()) {
+ // There are no previous questionable equalities,
+ // walk back to the last known safe diff.
+ thisDiff = safeDiff;
+ } else {
+ // There is an equality we can fall back to.
+ thisDiff = equalities.peek();
+ }
+ while (thisDiff != pointer.previous()) {
+ // Intentionally empty loop.
+ }
+ post_ins = post_del = false;
+ }
+
+ changes = true;
+ }
+ }
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+
+ if (changes) {
+ diff_cleanupMerge(diffs);
+ }
+ }
+
+ /**
+ * Reorder and merge like edit sections. Merge equalities.
+ * Any edit section can move as long as it doesn't cross an equality.
+ * @param diffs LinkedList of Diff objects.
+ */
+ public void diff_cleanupMerge(LinkedList diffs) {
+ diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end.
+ ListIterator pointer = diffs.listIterator();
+ int count_delete = 0;
+ int count_insert = 0;
+ String text_delete = "";
+ String text_insert = "";
+ Diff thisDiff = pointer.next();
+ Diff prevEqual = null;
+ int commonlength;
+ while (thisDiff != null) {
+ switch (thisDiff.operation) {
+ case INSERT:
+ count_insert++;
+ text_insert += thisDiff.text;
+ prevEqual = null;
+ break;
+ case DELETE:
+ count_delete++;
+ text_delete += thisDiff.text;
+ prevEqual = null;
+ break;
+ case EQUAL:
+ if (count_delete + count_insert > 1) {
+ boolean both_types = count_delete != 0 && count_insert != 0;
+ // Delete the offending records.
+ pointer.previous(); // Reverse direction.
+ while (count_delete-- > 0) {
+ pointer.previous();
+ pointer.remove();
+ }
+ while (count_insert-- > 0) {
+ pointer.previous();
+ pointer.remove();
+ }
+ if (both_types) {
+ // Factor out any common prefixies.
+ commonlength = diff_commonPrefix(text_insert, text_delete);
+ if (commonlength != 0) {
+ if (pointer.hasPrevious()) {
+ thisDiff = pointer.previous();
+ assert thisDiff.operation == Operation.EQUAL
+ : "Previous diff should have been an equality.";
+ thisDiff.text += text_insert.substring(0, commonlength);
+ pointer.next();
+ } else {
+ pointer.add(new Diff(Operation.EQUAL,
+ text_insert.substring(0, commonlength)));
+ }
+ text_insert = text_insert.substring(commonlength);
+ text_delete = text_delete.substring(commonlength);
+ }
+ // Factor out any common suffixies.
+ commonlength = diff_commonSuffix(text_insert, text_delete);
+ if (commonlength != 0) {
+ thisDiff = pointer.next();
+ thisDiff.text = text_insert.substring(text_insert.length()
+ - commonlength) + thisDiff.text;
+ text_insert = text_insert.substring(0, text_insert.length()
+ - commonlength);
+ text_delete = text_delete.substring(0, text_delete.length()
+ - commonlength);
+ pointer.previous();
+ }
+ }
+ // Insert the merged records.
+ if (text_delete.length() != 0) {
+ pointer.add(new Diff(Operation.DELETE, text_delete));
+ }
+ if (text_insert.length() != 0) {
+ pointer.add(new Diff(Operation.INSERT, text_insert));
+ }
+ // Step forward to the equality.
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ } else if (prevEqual != null) {
+ // Merge this equality with the previous one.
+ prevEqual.text += thisDiff.text;
+ pointer.remove();
+ thisDiff = pointer.previous();
+ pointer.next(); // Forward direction
+ }
+ count_insert = 0;
+ count_delete = 0;
+ text_delete = "";
+ text_insert = "";
+ prevEqual = thisDiff;
+ break;
+ }
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+ if (diffs.getLast().text.length() == 0) {
+ diffs.removeLast(); // Remove the dummy entry at the end.
+ }
+
+ /*
+ * Second pass: look for single edits surrounded on both sides by equalities
+ * which can be shifted sideways to eliminate an equality.
+ * e.g: ABAC -> ABAC
+ */
+ boolean changes = false;
+ // Create a new iterator at the start.
+ // (As opposed to walking the current one back.)
+ pointer = diffs.listIterator();
+ Diff prevDiff = pointer.hasNext() ? pointer.next() : null;
+ thisDiff = pointer.hasNext() ? pointer.next() : null;
+ Diff nextDiff = pointer.hasNext() ? pointer.next() : null;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (nextDiff != null) {
+ if (prevDiff.operation == Operation.EQUAL &&
+ nextDiff.operation == Operation.EQUAL) {
+ // This is a single edit surrounded by equalities.
+ if (thisDiff.text.endsWith(prevDiff.text)) {
+ // Shift the edit over the previous equality.
+ thisDiff.text = prevDiff.text
+ + thisDiff.text.substring(0, thisDiff.text.length()
+ - prevDiff.text.length());
+ nextDiff.text = prevDiff.text + nextDiff.text;
+ pointer.previous(); // Walk past nextDiff.
+ pointer.previous(); // Walk past thisDiff.
+ pointer.previous(); // Walk past prevDiff.
+ pointer.remove(); // Delete prevDiff.
+ pointer.next(); // Walk past thisDiff.
+ thisDiff = pointer.next(); // Walk past nextDiff.
+ nextDiff = pointer.hasNext() ? pointer.next() : null;
+ changes = true;
+ } else if (thisDiff.text.startsWith(nextDiff.text)) {
+ // Shift the edit over the next equality.
+ prevDiff.text += nextDiff.text;
+ thisDiff.text = thisDiff.text.substring(nextDiff.text.length())
+ + nextDiff.text;
+ pointer.remove(); // Delete nextDiff.
+ nextDiff = pointer.hasNext() ? pointer.next() : null;
+ changes = true;
+ }
+ }
+ prevDiff = thisDiff;
+ thisDiff = nextDiff;
+ nextDiff = pointer.hasNext() ? pointer.next() : null;
+ }
+ // If shifts were made, the diff needs reordering and another shift sweep.
+ if (changes) {
+ diff_cleanupMerge(diffs);
+ }
+ }
+
+ /**
+ * loc is a location in text1, compute and return the equivalent location in
+ * text2.
+ * e.g. "The cat" vs "The big cat", 1->1, 5->8
+ * @param diffs List of Diff objects.
+ * @param loc Location within text1.
+ * @return Location within text2.
+ */
+ public int diff_xIndex(List diffs, int loc) {
+ int chars1 = 0;
+ int chars2 = 0;
+ int last_chars1 = 0;
+ int last_chars2 = 0;
+ Diff lastDiff = null;
+ for (Diff aDiff : diffs) {
+ if (aDiff.operation != Operation.INSERT) {
+ // Equality or deletion.
+ chars1 += aDiff.text.length();
+ }
+ if (aDiff.operation != Operation.DELETE) {
+ // Equality or insertion.
+ chars2 += aDiff.text.length();
+ }
+ if (chars1 > loc) {
+ // Overshot the location.
+ lastDiff = aDiff;
+ break;
+ }
+ last_chars1 = chars1;
+ last_chars2 = chars2;
+ }
+ if (lastDiff != null && lastDiff.operation == Operation.DELETE) {
+ // The location was deleted.
+ return last_chars2;
+ }
+ // Add the remaining character length.
+ return last_chars2 + (loc - last_chars1);
+ }
+
+ /**
+ * Convert a Diff list into a pretty HTML report.
+ * @param diffs List of Diff objects.
+ * @return HTML representation.
+ */
+ public String diff_prettyHtml(List diffs) {
+ StringBuilder html = new StringBuilder();
+ for (Diff aDiff : diffs) {
+ String text = aDiff.text.replace("&", "&").replace("<", "<")
+ .replace(">", ">").replace("\n", "¶
");
+ switch (aDiff.operation) {
+ case INSERT:
+ html.append("").append(text)
+ .append("");
+ break;
+ case DELETE:
+ html.append("").append(text)
+ .append("");
+ break;
+ case EQUAL:
+ html.append("").append(text).append("");
+ break;
+ }
+ }
+ return html.toString();
+ }
+
+ /**
+ * Compute and return the source text (all equalities and deletions).
+ * @param diffs List of Diff objects.
+ * @return Source text.
+ */
+ public String diff_text1(List diffs) {
+ StringBuilder text = new StringBuilder();
+ for (Diff aDiff : diffs) {
+ if (aDiff.operation != Operation.INSERT) {
+ text.append(aDiff.text);
+ }
+ }
+ return text.toString();
+ }
+
+ /**
+ * Compute and return the destination text (all equalities and insertions).
+ * @param diffs List of Diff objects.
+ * @return Destination text.
+ */
+ public String diff_text2(List diffs) {
+ StringBuilder text = new StringBuilder();
+ for (Diff aDiff : diffs) {
+ if (aDiff.operation != Operation.DELETE) {
+ text.append(aDiff.text);
+ }
+ }
+ return text.toString();
+ }
+
+ /**
+ * Compute the Levenshtein distance; the number of inserted, deleted or
+ * substituted characters.
+ * @param diffs List of Diff objects.
+ * @return Number of changes.
+ */
+ public int diff_levenshtein(List diffs) {
+ int levenshtein = 0;
+ int insertions = 0;
+ int deletions = 0;
+ for (Diff aDiff : diffs) {
+ switch (aDiff.operation) {
+ case INSERT:
+ insertions += aDiff.text.length();
+ break;
+ case DELETE:
+ deletions += aDiff.text.length();
+ break;
+ case EQUAL:
+ // A deletion and an insertion is one substitution.
+ levenshtein += Math.max(insertions, deletions);
+ insertions = 0;
+ deletions = 0;
+ break;
+ }
+ }
+ levenshtein += Math.max(insertions, deletions);
+ return levenshtein;
+ }
+
+ /**
+ * Crush the diff into an encoded string which describes the operations
+ * required to transform text1 into text2.
+ * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
+ * Operations are tab-separated. Inserted text is escaped using %xx notation.
+ * @param diffs List of Diff objects.
+ * @return Delta text.
+ */
+ public String diff_toDelta(List diffs) {
+ StringBuilder text = new StringBuilder();
+ for (Diff aDiff : diffs) {
+ switch (aDiff.operation) {
+ case INSERT:
+ try {
+ text.append("+").append(URLEncoder.encode(aDiff.text, "UTF-8")
+ .replace('+', ' ')).append("\t");
+ } catch (UnsupportedEncodingException e) {
+ // Not likely on modern system.
+ throw new Error("This system does not support UTF-8.", e);
+ }
+ break;
+ case DELETE:
+ text.append("-").append(aDiff.text.length()).append("\t");
+ break;
+ case EQUAL:
+ text.append("=").append(aDiff.text.length()).append("\t");
+ break;
+ }
+ }
+ String delta = text.toString();
+ if (delta.length() != 0) {
+ // Strip off trailing tab character.
+ delta = delta.substring(0, delta.length() - 1);
+ delta = unescapeForEncodeUriCompatability(delta);
+ }
+ return delta;
+ }
+
+ /**
+ * Given the original text1, and an encoded string which describes the
+ * operations required to transform text1 into text2, compute the full diff.
+ * @param text1 Source string for the diff.
+ * @param delta Delta text.
+ * @return Array of Diff objects or null if invalid.
+ * @throws IllegalArgumentException If invalid input.
+ */
+ public LinkedList diff_fromDelta(String text1, String delta)
+ throws IllegalArgumentException {
+ LinkedList diffs = new LinkedList();
+ int pointer = 0; // Cursor in text1
+ String[] tokens = delta.split("\t");
+ for (String token : tokens) {
+ if (token.length() == 0) {
+ // Blank tokens are ok (from a trailing \t).
+ continue;
+ }
+ // Each token begins with a one character parameter which specifies the
+ // operation of this token (delete, insert, equality).
+ String param = token.substring(1);
+ switch (token.charAt(0)) {
+ case '+':
+ // decode would change all "+" to " "
+ param = param.replace("+", "%2B");
+ try {
+ param = URLDecoder.decode(param, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // Not likely on modern system.
+ throw new Error("This system does not support UTF-8.", e);
+ } catch (IllegalArgumentException e) {
+ // Malformed URI sequence.
+ throw new IllegalArgumentException(
+ "Illegal escape in diff_fromDelta: " + param, e);
+ }
+ diffs.add(new Diff(Operation.INSERT, param));
+ break;
+ case '-':
+ // Fall through.
+ case '=':
+ int n;
+ try {
+ n = Integer.parseInt(param);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Invalid number in diff_fromDelta: " + param, e);
+ }
+ if (n < 0) {
+ throw new IllegalArgumentException(
+ "Negative number in diff_fromDelta: " + param);
+ }
+ String text;
+ try {
+ text = text1.substring(pointer, pointer += n);
+ } catch (StringIndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("Delta length (" + pointer
+ + ") larger than source text length (" + text1.length()
+ + ").", e);
+ }
+ if (token.charAt(0) == '=') {
+ diffs.add(new Diff(Operation.EQUAL, text));
+ } else {
+ diffs.add(new Diff(Operation.DELETE, text));
+ }
+ break;
+ default:
+ // Anything else is an error.
+ throw new IllegalArgumentException(
+ "Invalid diff operation in diff_fromDelta: " + token.charAt(0));
+ }
+ }
+ if (pointer != text1.length()) {
+ throw new IllegalArgumentException("Delta length (" + pointer
+ + ") smaller than source text length (" + text1.length() + ").");
+ }
+ return diffs;
+ }
+
+
+ // MATCH FUNCTIONS
+
+
+ /**
+ * Locate the best instance of 'pattern' in 'text' near 'loc'.
+ * Returns -1 if no match found.
+ * @param text The text to search.
+ * @param pattern The pattern to search for.
+ * @param loc The location to search around.
+ * @return Best match index or -1.
+ */
+ public int match_main(String text, String pattern, int loc) {
+ // Check for null inputs.
+ if (text == null || pattern == null) {
+ throw new IllegalArgumentException("Null inputs. (match_main)");
+ }
+
+ loc = Math.max(0, Math.min(loc, text.length()));
+ if (text.equals(pattern)) {
+ // Shortcut (potentially not guaranteed by the algorithm)
+ return 0;
+ } else if (text.length() == 0) {
+ // Nothing to match.
+ return -1;
+ } else if (loc + pattern.length() <= text.length()
+ && text.substring(loc, loc + pattern.length()).equals(pattern)) {
+ // Perfect match at the perfect spot! (Includes case of null pattern)
+ return loc;
+ } else {
+ // Do a fuzzy compare.
+ return match_bitap(text, pattern, loc);
+ }
+ }
+
+ /**
+ * Locate the best instance of 'pattern' in 'text' near 'loc' using the
+ * Bitap algorithm. Returns -1 if no match found.
+ * @param text The text to search.
+ * @param pattern The pattern to search for.
+ * @param loc The location to search around.
+ * @return Best match index or -1.
+ */
+ protected int match_bitap(String text, String pattern, int loc) {
+ assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits)
+ : "Pattern too long for this application.";
+
+ // Initialise the alphabet.
+ Map s = match_alphabet(pattern);
+
+ // Highest score beyond which we give up.
+ double score_threshold = Match_Threshold;
+ // Is there a nearby exact match? (speedup)
+ int best_loc = text.indexOf(pattern, loc);
+ if (best_loc != -1) {
+ score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern),
+ score_threshold);
+ // What about in the other direction? (speedup)
+ best_loc = text.lastIndexOf(pattern, loc + pattern.length());
+ if (best_loc != -1) {
+ score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern),
+ score_threshold);
+ }
+ }
+
+ // Initialise the bit arrays.
+ int matchmask = 1 << (pattern.length() - 1);
+ best_loc = -1;
+
+ int bin_min, bin_mid;
+ int bin_max = pattern.length() + text.length();
+ // Empty initialization added to appease Java compiler.
+ int[] last_rd = new int[0];
+ for (int d = 0; d < pattern.length(); d++) {
+ // Scan for the best match; each iteration allows for one more error.
+ // Run a binary search to determine how far from 'loc' we can stray at
+ // this error level.
+ bin_min = 0;
+ bin_mid = bin_max;
+ while (bin_min < bin_mid) {
+ if (match_bitapScore(d, loc + bin_mid, loc, pattern)
+ <= score_threshold) {
+ bin_min = bin_mid;
+ } else {
+ bin_max = bin_mid;
+ }
+ bin_mid = (bin_max - bin_min) / 2 + bin_min;
+ }
+ // Use the result from this iteration as the maximum for the next.
+ bin_max = bin_mid;
+ int start = Math.max(1, loc - bin_mid + 1);
+ int finish = Math.min(loc + bin_mid, text.length()) + pattern.length();
+
+ int[] rd = new int[finish + 2];
+ rd[finish + 1] = (1 << d) - 1;
+ for (int j = finish; j >= start; j--) {
+ int charMatch;
+ if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) {
+ // Out of range.
+ charMatch = 0;
+ } else {
+ charMatch = s.get(text.charAt(j - 1));
+ }
+ if (d == 0) {
+ // First pass: exact match.
+ rd[j] = ((rd[j + 1] << 1) | 1) & charMatch;
+ } else {
+ // Subsequent passes: fuzzy match.
+ rd[j] = (((rd[j + 1] << 1) | 1) & charMatch)
+ | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1];
+ }
+ if ((rd[j] & matchmask) != 0) {
+ double score = match_bitapScore(d, j - 1, loc, pattern);
+ // This match will almost certainly be better than any existing
+ // match. But check anyway.
+ if (score <= score_threshold) {
+ // Told you so.
+ score_threshold = score;
+ best_loc = j - 1;
+ if (best_loc > loc) {
+ // When passing loc, don't exceed our current distance from loc.
+ start = Math.max(1, 2 * loc - best_loc);
+ } else {
+ // Already passed loc, downhill from here on in.
+ break;
+ }
+ }
+ }
+ }
+ if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) {
+ // No hope for a (better) match at greater error levels.
+ break;
+ }
+ last_rd = rd;
+ }
+ return best_loc;
+ }
+
+ /**
+ * Compute and return the score for a match with e errors and x location.
+ * @param e Number of errors in match.
+ * @param x Location of match.
+ * @param loc Expected location of match.
+ * @param pattern Pattern being sought.
+ * @return Overall score for match (0.0 = good, 1.0 = bad).
+ */
+ private double match_bitapScore(int e, int x, int loc, String pattern) {
+ float accuracy = (float) e / pattern.length();
+ int proximity = Math.abs(loc - x);
+ if (Match_Distance == 0) {
+ // Dodge divide by zero error.
+ return proximity == 0 ? accuracy : 1.0;
+ }
+ return accuracy + (proximity / (float) Match_Distance);
+ }
+
+ /**
+ * Initialise the alphabet for the Bitap algorithm.
+ * @param pattern The text to encode.
+ * @return Hash of character locations.
+ */
+ protected Map match_alphabet(String pattern) {
+ Map s = new HashMap();
+ char[] char_pattern = pattern.toCharArray();
+ for (char c : char_pattern) {
+ s.put(c, 0);
+ }
+ int i = 0;
+ for (char c : char_pattern) {
+ s.put(c, s.get(c) | (1 << (pattern.length() - i - 1)));
+ i++;
+ }
+ return s;
+ }
+
+
+ // PATCH FUNCTIONS
+
+
+ /**
+ * Increase the context until it is unique,
+ * but don't let the pattern expand beyond Match_MaxBits.
+ * @param patch The patch to grow.
+ * @param text Source text.
+ */
+ protected void patch_addContext(Patch patch, String text) {
+ if (text.length() == 0) {
+ return;
+ }
+ String pattern = text.substring(patch.start2, patch.start2 + patch.length1);
+ int padding = 0;
+
+ // Look for the first and last matches of pattern in text. If two different
+ // matches are found, increase the pattern length.
+ while (text.indexOf(pattern) != text.lastIndexOf(pattern)
+ && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) {
+ padding += Patch_Margin;
+ pattern = text.substring(Math.max(0, patch.start2 - padding),
+ Math.min(text.length(), patch.start2 + patch.length1 + padding));
+ }
+ // Add one chunk for good luck.
+ padding += Patch_Margin;
+
+ // Add the prefix.
+ String prefix = text.substring(Math.max(0, patch.start2 - padding),
+ patch.start2);
+ if (prefix.length() != 0) {
+ patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix));
+ }
+ // Add the suffix.
+ String suffix = text.substring(patch.start2 + patch.length1,
+ Math.min(text.length(), patch.start2 + patch.length1 + padding));
+ if (suffix.length() != 0) {
+ patch.diffs.addLast(new Diff(Operation.EQUAL, suffix));
+ }
+
+ // Roll back the start points.
+ patch.start1 -= prefix.length();
+ patch.start2 -= prefix.length();
+ // Extend the lengths.
+ patch.length1 += prefix.length() + suffix.length();
+ patch.length2 += prefix.length() + suffix.length();
+ }
+
+ /**
+ * Compute a list of patches to turn text1 into text2.
+ * A set of diffs will be computed.
+ * @param text1 Old text.
+ * @param text2 New text.
+ * @return LinkedList of Patch objects.
+ */
+ public LinkedList patch_make(String text1, String text2) {
+ if (text1 == null || text2 == null) {
+ throw new IllegalArgumentException("Null inputs. (patch_make)");
+ }
+ // No diffs provided, compute our own.
+ LinkedList diffs = diff_main(text1, text2, true);
+ if (diffs.size() > 2) {
+ diff_cleanupSemantic(diffs);
+ diff_cleanupEfficiency(diffs);
+ }
+ return patch_make(text1, diffs);
+ }
+
+ /**
+ * Compute a list of patches to turn text1 into text2.
+ * text1 will be derived from the provided diffs.
+ * @param diffs Array of Diff objects for text1 to text2.
+ * @return LinkedList of Patch objects.
+ */
+ public LinkedList patch_make(LinkedList diffs) {
+ if (diffs == null) {
+ throw new IllegalArgumentException("Null inputs. (patch_make)");
+ }
+ // No origin string provided, compute our own.
+ String text1 = diff_text1(diffs);
+ return patch_make(text1, diffs);
+ }
+
+ /**
+ * Compute a list of patches to turn text1 into text2.
+ * text2 is ignored, diffs are the delta between text1 and text2.
+ * @param text1 Old text
+ * @param text2 Ignored.
+ * @param diffs Array of Diff objects for text1 to text2.
+ * @return LinkedList of Patch objects.
+ * @deprecated Prefer patch_make(String text1, LinkedList diffs).
+ */
+ @Deprecated public LinkedList patch_make(String text1, String text2,
+ LinkedList diffs) {
+ return patch_make(text1, diffs);
+ }
+
+ /**
+ * Compute a list of patches to turn text1 into text2.
+ * text2 is not provided, diffs are the delta between text1 and text2.
+ * @param text1 Old text.
+ * @param diffs Array of Diff objects for text1 to text2.
+ * @return LinkedList of Patch objects.
+ */
+ public LinkedList patch_make(String text1, LinkedList diffs) {
+ if (text1 == null || diffs == null) {
+ throw new IllegalArgumentException("Null inputs. (patch_make)");
+ }
+
+ LinkedList patches = new LinkedList();
+ if (diffs.isEmpty()) {
+ return patches; // Get rid of the null case.
+ }
+ Patch patch = new Patch();
+ int char_count1 = 0; // Number of characters into the text1 string.
+ int char_count2 = 0; // Number of characters into the text2 string.
+ // Start with text1 (prepatch_text) and apply the diffs until we arrive at
+ // text2 (postpatch_text). We recreate the patches one by one to determine
+ // context info.
+ String prepatch_text = text1;
+ String postpatch_text = text1;
+ for (Diff aDiff : diffs) {
+ if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) {
+ // A new patch starts here.
+ patch.start1 = char_count1;
+ patch.start2 = char_count2;
+ }
+
+ switch (aDiff.operation) {
+ case INSERT:
+ patch.diffs.add(aDiff);
+ patch.length2 += aDiff.text.length();
+ postpatch_text = postpatch_text.substring(0, char_count2)
+ + aDiff.text + postpatch_text.substring(char_count2);
+ break;
+ case DELETE:
+ patch.length1 += aDiff.text.length();
+ patch.diffs.add(aDiff);
+ postpatch_text = postpatch_text.substring(0, char_count2)
+ + postpatch_text.substring(char_count2 + aDiff.text.length());
+ break;
+ case EQUAL:
+ if (aDiff.text.length() <= 2 * Patch_Margin
+ && !patch.diffs.isEmpty() && aDiff != diffs.getLast()) {
+ // Small equality inside a patch.
+ patch.diffs.add(aDiff);
+ patch.length1 += aDiff.text.length();
+ patch.length2 += aDiff.text.length();
+ }
+
+ if (aDiff.text.length() >= 2 * Patch_Margin && !patch.diffs.isEmpty()) {
+ // Time for a new patch.
+ if (!patch.diffs.isEmpty()) {
+ patch_addContext(patch, prepatch_text);
+ patches.add(patch);
+ patch = new Patch();
+ // Unlike Unidiff, our patch lists have a rolling context.
+ // https://github.com/google/diff-match-patch/wiki/Unidiff
+ // Update prepatch text & pos to reflect the application of the
+ // just completed patch.
+ prepatch_text = postpatch_text;
+ char_count1 = char_count2;
+ }
+ }
+ break;
+ }
+
+ // Update the current character count.
+ if (aDiff.operation != Operation.INSERT) {
+ char_count1 += aDiff.text.length();
+ }
+ if (aDiff.operation != Operation.DELETE) {
+ char_count2 += aDiff.text.length();
+ }
+ }
+ // Pick up the leftover patch if not empty.
+ if (!patch.diffs.isEmpty()) {
+ patch_addContext(patch, prepatch_text);
+ patches.add(patch);
+ }
+
+ return patches;
+ }
+
+ /**
+ * Given an array of patches, return another array that is identical.
+ * @param patches Array of Patch objects.
+ * @return Array of Patch objects.
+ */
+ public LinkedList patch_deepCopy(LinkedList patches) {
+ LinkedList patchesCopy = new LinkedList();
+ for (Patch aPatch : patches) {
+ Patch patchCopy = new Patch();
+ for (Diff aDiff : aPatch.diffs) {
+ Diff diffCopy = new Diff(aDiff.operation, aDiff.text);
+ patchCopy.diffs.add(diffCopy);
+ }
+ patchCopy.start1 = aPatch.start1;
+ patchCopy.start2 = aPatch.start2;
+ patchCopy.length1 = aPatch.length1;
+ patchCopy.length2 = aPatch.length2;
+ patchesCopy.add(patchCopy);
+ }
+ return patchesCopy;
+ }
+
+ /**
+ * Merge a set of patches onto the text. Return a patched text, as well
+ * as an array of true/false values indicating which patches were applied.
+ * @param patches Array of Patch objects
+ * @param text Old text.
+ * @return Two element Object array, containing the new text and an array of
+ * boolean values.
+ */
+ public Object[] patch_apply(LinkedList patches, String text) {
+ if (patches.isEmpty()) {
+ return new Object[]{text, new boolean[0]};
+ }
+
+ // Deep copy the patches so that no changes are made to originals.
+ patches = patch_deepCopy(patches);
+
+ String nullPadding = patch_addPadding(patches);
+ text = nullPadding + text + nullPadding;
+ patch_splitMax(patches);
+
+ int x = 0;
+ // delta keeps track of the offset between the expected and actual location
+ // of the previous patch. If there are patches expected at positions 10 and
+ // 20, but the first patch was found at 12, delta is 2 and the second patch
+ // has an effective expected position of 22.
+ int delta = 0;
+ boolean[] results = new boolean[patches.size()];
+ for (Patch aPatch : patches) {
+ int expected_loc = aPatch.start2 + delta;
+ String text1 = diff_text1(aPatch.diffs);
+ int start_loc;
+ int end_loc = -1;
+ if (text1.length() > this.Match_MaxBits) {
+ // patch_splitMax will only provide an oversized pattern in the case of
+ // a monster delete.
+ start_loc = match_main(text,
+ text1.substring(0, this.Match_MaxBits), expected_loc);
+ if (start_loc != -1) {
+ end_loc = match_main(text,
+ text1.substring(text1.length() - this.Match_MaxBits),
+ expected_loc + text1.length() - this.Match_MaxBits);
+ if (end_loc == -1 || start_loc >= end_loc) {
+ // Can't find valid trailing context. Drop this patch.
+ start_loc = -1;
+ }
+ }
+ } else {
+ start_loc = match_main(text, text1, expected_loc);
+ }
+ if (start_loc == -1) {
+ // No match found. :(
+ results[x] = false;
+ // Subtract the delta for this failed patch from subsequent patches.
+ delta -= aPatch.length2 - aPatch.length1;
+ } else {
+ // Found a match. :)
+ results[x] = true;
+ delta = start_loc - expected_loc;
+ String text2;
+ if (end_loc == -1) {
+ text2 = text.substring(start_loc,
+ Math.min(start_loc + text1.length(), text.length()));
+ } else {
+ text2 = text.substring(start_loc,
+ Math.min(end_loc + this.Match_MaxBits, text.length()));
+ }
+ if (text1.equals(text2)) {
+ // Perfect match, just shove the replacement text in.
+ text = text.substring(0, start_loc) + diff_text2(aPatch.diffs)
+ + text.substring(start_loc + text1.length());
+ } else {
+ // Imperfect match. Run a diff to get a framework of equivalent
+ // indices.
+ LinkedList diffs = diff_main(text1, text2, false);
+ if (text1.length() > this.Match_MaxBits
+ && diff_levenshtein(diffs) / (float) text1.length()
+ > this.Patch_DeleteThreshold) {
+ // The end points match, but the content is unacceptably bad.
+ results[x] = false;
+ } else {
+ diff_cleanupSemanticLossless(diffs);
+ int index1 = 0;
+ for (Diff aDiff : aPatch.diffs) {
+ if (aDiff.operation != Operation.EQUAL) {
+ int index2 = diff_xIndex(diffs, index1);
+ if (aDiff.operation == Operation.INSERT) {
+ // Insertion
+ text = text.substring(0, start_loc + index2) + aDiff.text
+ + text.substring(start_loc + index2);
+ } else if (aDiff.operation == Operation.DELETE) {
+ // Deletion
+ text = text.substring(0, start_loc + index2)
+ + text.substring(start_loc + diff_xIndex(diffs,
+ index1 + aDiff.text.length()));
+ }
+ }
+ if (aDiff.operation != Operation.DELETE) {
+ index1 += aDiff.text.length();
+ }
+ }
+ }
+ }
+ }
+ x++;
+ }
+ // Strip the padding off.
+ text = text.substring(nullPadding.length(), text.length()
+ - nullPadding.length());
+ return new Object[]{text, results};
+ }
+
+ /**
+ * Add some padding on text start and end so that edges can match something.
+ * Intended to be called only from within patch_apply.
+ * @param patches Array of Patch objects.
+ * @return The padding string added to each side.
+ */
+ public String patch_addPadding(LinkedList patches) {
+ short paddingLength = this.Patch_Margin;
+ String nullPadding = "";
+ for (short x = 1; x <= paddingLength; x++) {
+ nullPadding += String.valueOf((char) x);
+ }
+
+ // Bump all the patches forward.
+ for (Patch aPatch : patches) {
+ aPatch.start1 += paddingLength;
+ aPatch.start2 += paddingLength;
+ }
+
+ // Add some padding on start of first diff.
+ Patch patch = patches.getFirst();
+ LinkedList diffs = patch.diffs;
+ if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) {
+ // Add nullPadding equality.
+ diffs.addFirst(new Diff(Operation.EQUAL, nullPadding));
+ patch.start1 -= paddingLength; // Should be 0.
+ patch.start2 -= paddingLength; // Should be 0.
+ patch.length1 += paddingLength;
+ patch.length2 += paddingLength;
+ } else if (paddingLength > diffs.getFirst().text.length()) {
+ // Grow first equality.
+ Diff firstDiff = diffs.getFirst();
+ int extraLength = paddingLength - firstDiff.text.length();
+ firstDiff.text = nullPadding.substring(firstDiff.text.length())
+ + firstDiff.text;
+ patch.start1 -= extraLength;
+ patch.start2 -= extraLength;
+ patch.length1 += extraLength;
+ patch.length2 += extraLength;
+ }
+
+ // Add some padding on end of last diff.
+ patch = patches.getLast();
+ diffs = patch.diffs;
+ if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) {
+ // Add nullPadding equality.
+ diffs.addLast(new Diff(Operation.EQUAL, nullPadding));
+ patch.length1 += paddingLength;
+ patch.length2 += paddingLength;
+ } else if (paddingLength > diffs.getLast().text.length()) {
+ // Grow last equality.
+ Diff lastDiff = diffs.getLast();
+ int extraLength = paddingLength - lastDiff.text.length();
+ lastDiff.text += nullPadding.substring(0, extraLength);
+ patch.length1 += extraLength;
+ patch.length2 += extraLength;
+ }
+
+ return nullPadding;
+ }
+
+ /**
+ * Look through the patches and break up any which are longer than the
+ * maximum limit of the match algorithm.
+ * Intended to be called only from within patch_apply.
+ * @param patches LinkedList of Patch objects.
+ */
+ public void patch_splitMax(LinkedList patches) {
+ short patch_size = Match_MaxBits;
+ String precontext, postcontext;
+ Patch patch;
+ int start1, start2;
+ boolean empty;
+ Operation diff_type;
+ String diff_text;
+ ListIterator pointer = patches.listIterator();
+ Patch bigpatch = pointer.hasNext() ? pointer.next() : null;
+ while (bigpatch != null) {
+ if (bigpatch.length1 <= Match_MaxBits) {
+ bigpatch = pointer.hasNext() ? pointer.next() : null;
+ continue;
+ }
+ // Remove the big old patch.
+ pointer.remove();
+ start1 = bigpatch.start1;
+ start2 = bigpatch.start2;
+ precontext = "";
+ while (!bigpatch.diffs.isEmpty()) {
+ // Create one of several smaller patches.
+ patch = new Patch();
+ empty = true;
+ patch.start1 = start1 - precontext.length();
+ patch.start2 = start2 - precontext.length();
+ if (precontext.length() != 0) {
+ patch.length1 = patch.length2 = precontext.length();
+ patch.diffs.add(new Diff(Operation.EQUAL, precontext));
+ }
+ while (!bigpatch.diffs.isEmpty()
+ && patch.length1 < patch_size - Patch_Margin) {
+ diff_type = bigpatch.diffs.getFirst().operation;
+ diff_text = bigpatch.diffs.getFirst().text;
+ if (diff_type == Operation.INSERT) {
+ // Insertions are harmless.
+ patch.length2 += diff_text.length();
+ start2 += diff_text.length();
+ patch.diffs.addLast(bigpatch.diffs.removeFirst());
+ empty = false;
+ } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1
+ && patch.diffs.getFirst().operation == Operation.EQUAL
+ && diff_text.length() > 2 * patch_size) {
+ // This is a large deletion. Let it pass in one chunk.
+ patch.length1 += diff_text.length();
+ start1 += diff_text.length();
+ empty = false;
+ patch.diffs.add(new Diff(diff_type, diff_text));
+ bigpatch.diffs.removeFirst();
+ } else {
+ // Deletion or equality. Only take as much as we can stomach.
+ diff_text = diff_text.substring(0, Math.min(diff_text.length(),
+ patch_size - patch.length1 - Patch_Margin));
+ patch.length1 += diff_text.length();
+ start1 += diff_text.length();
+ if (diff_type == Operation.EQUAL) {
+ patch.length2 += diff_text.length();
+ start2 += diff_text.length();
+ } else {
+ empty = false;
+ }
+ patch.diffs.add(new Diff(diff_type, diff_text));
+ if (diff_text.equals(bigpatch.diffs.getFirst().text)) {
+ bigpatch.diffs.removeFirst();
+ } else {
+ bigpatch.diffs.getFirst().text = bigpatch.diffs.getFirst().text
+ .substring(diff_text.length());
+ }
+ }
+ }
+ // Compute the head context for the next patch.
+ precontext = diff_text2(patch.diffs);
+ precontext = precontext.substring(Math.max(0, precontext.length()
+ - Patch_Margin));
+ // Append the end context for this patch.
+ if (diff_text1(bigpatch.diffs).length() > Patch_Margin) {
+ postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin);
+ } else {
+ postcontext = diff_text1(bigpatch.diffs);
+ }
+ if (postcontext.length() != 0) {
+ patch.length1 += postcontext.length();
+ patch.length2 += postcontext.length();
+ if (!patch.diffs.isEmpty()
+ && patch.diffs.getLast().operation == Operation.EQUAL) {
+ patch.diffs.getLast().text += postcontext;
+ } else {
+ patch.diffs.add(new Diff(Operation.EQUAL, postcontext));
+ }
+ }
+ if (!empty) {
+ pointer.add(patch);
+ }
+ }
+ bigpatch = pointer.hasNext() ? pointer.next() : null;
+ }
+ }
+
+ /**
+ * Take a list of patches and return a textual representation.
+ * @param patches List of Patch objects.
+ * @return Text representation of patches.
+ */
+ public String patch_toText(List patches) {
+ StringBuilder text = new StringBuilder();
+ for (Patch aPatch : patches) {
+ text.append(aPatch);
+ }
+ return text.toString();
+ }
+
+ /**
+ * Parse a textual representation of patches and return a List of Patch
+ * objects.
+ * @param textline Text representation of patches.
+ * @return List of Patch objects.
+ * @throws IllegalArgumentException If invalid input.
+ */
+ public List patch_fromText(String textline)
+ throws IllegalArgumentException {
+ List patches = new LinkedList();
+ if (textline.length() == 0) {
+ return patches;
+ }
+ List textList = Arrays.asList(textline.split("\n"));
+ LinkedList text = new LinkedList(textList);
+ Patch patch;
+ Pattern patchHeader
+ = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$");
+ Matcher m;
+ char sign;
+ String line;
+ while (!text.isEmpty()) {
+ m = patchHeader.matcher(text.getFirst());
+ if (!m.matches()) {
+ throw new IllegalArgumentException(
+ "Invalid patch string: " + text.getFirst());
+ }
+ patch = new Patch();
+ patches.add(patch);
+ patch.start1 = Integer.parseInt(m.group(1));
+ if (m.group(2).length() == 0) {
+ patch.start1--;
+ patch.length1 = 1;
+ } else if (m.group(2).equals("0")) {
+ patch.length1 = 0;
+ } else {
+ patch.start1--;
+ patch.length1 = Integer.parseInt(m.group(2));
+ }
+
+ patch.start2 = Integer.parseInt(m.group(3));
+ if (m.group(4).length() == 0) {
+ patch.start2--;
+ patch.length2 = 1;
+ } else if (m.group(4).equals("0")) {
+ patch.length2 = 0;
+ } else {
+ patch.start2--;
+ patch.length2 = Integer.parseInt(m.group(4));
+ }
+ text.removeFirst();
+
+ while (!text.isEmpty()) {
+ try {
+ sign = text.getFirst().charAt(0);
+ } catch (IndexOutOfBoundsException e) {
+ // Blank line? Whatever.
+ text.removeFirst();
+ continue;
+ }
+ line = text.getFirst().substring(1);
+ line = line.replace("+", "%2B"); // decode would change all "+" to " "
+ try {
+ line = URLDecoder.decode(line, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ // Not likely on modern system.
+ throw new Error("This system does not support UTF-8.", e);
+ } catch (IllegalArgumentException e) {
+ // Malformed URI sequence.
+ throw new IllegalArgumentException(
+ "Illegal escape in patch_fromText: " + line, e);
+ }
+ if (sign == '-') {
+ // Deletion.
+ patch.diffs.add(new Diff(Operation.DELETE, line));
+ } else if (sign == '+') {
+ // Insertion.
+ patch.diffs.add(new Diff(Operation.INSERT, line));
+ } else if (sign == ' ') {
+ // Minor equality.
+ patch.diffs.add(new Diff(Operation.EQUAL, line));
+ } else if (sign == '@') {
+ // Start of next patch.
+ break;
+ } else {
+ // WTF?
+ throw new IllegalArgumentException(
+ "Invalid patch mode '" + sign + "' in: " + line);
+ }
+ text.removeFirst();
+ }
+ }
+ return patches;
+ }
+
+
+ /**
+ * Class representing one diff operation.
+ */
+ public static class Diff {
+ /**
+ * One of: INSERT, DELETE or EQUAL.
+ */
+ public Operation operation;
+ /**
+ * The text associated with this diff operation.
+ */
+ public String text;
+
+ /**
+ * Constructor. Initializes the diff with the provided values.
+ * @param operation One of INSERT, DELETE or EQUAL.
+ * @param text The text being applied.
+ */
+ public Diff(Operation operation, String text) {
+ // Construct a diff with the specified operation and text.
+ this.operation = operation;
+ this.text = text;
+ }
+
+ /**
+ * Display a human-readable version of this Diff.
+ * @return text version.
+ */
+ public String toString() {
+ String prettyText = this.text.replace('\n', '\u00b6');
+ return "Diff(" + this.operation + ",\"" + prettyText + "\")";
+ }
+
+ /**
+ * Create a numeric hash value for a Diff.
+ * This function is not used by DMP.
+ * @return Hash value.
+ */
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = (operation == null) ? 0 : operation.hashCode();
+ result += prime * ((text == null) ? 0 : text.hashCode());
+ return result;
+ }
+
+ /**
+ * Is this Diff equivalent to another Diff?
+ * @param obj Another Diff to compare against.
+ * @return true or false.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Diff other = (Diff) obj;
+ if (operation != other.operation) {
+ return false;
+ }
+ if (text == null) {
+ if (other.text != null) {
+ return false;
+ }
+ } else if (!text.equals(other.text)) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+
+ /**
+ * Class representing one patch operation.
+ */
+ public static class Patch {
+ public LinkedList diffs;
+ public int start1;
+ public int start2;
+ public int length1;
+ public int length2;
+
+ /**
+ * Constructor. Initializes with an empty list of diffs.
+ */
+ public Patch() {
+ this.diffs = new LinkedList();
+ }
+
+ /**
+ * Emulate GNU diff's format.
+ * Header: @@ -382,8 +481,9 @@
+ * Indices are printed as 1-based, not 0-based.
+ * @return The GNU diff string.
+ */
+ public String toString() {
+ String coords1, coords2;
+ if (this.length1 == 0) {
+ coords1 = this.start1 + ",0";
+ } else if (this.length1 == 1) {
+ coords1 = Integer.toString(this.start1 + 1);
+ } else {
+ coords1 = (this.start1 + 1) + "," + this.length1;
+ }
+ if (this.length2 == 0) {
+ coords2 = this.start2 + ",0";
+ } else if (this.length2 == 1) {
+ coords2 = Integer.toString(this.start2 + 1);
+ } else {
+ coords2 = (this.start2 + 1) + "," + this.length2;
+ }
+ StringBuilder text = new StringBuilder();
+ text.append("@@ -").append(coords1).append(" +").append(coords2)
+ .append(" @@\n");
+ // Escape the body of the patch with %xx notation.
+ for (Diff aDiff : this.diffs) {
+ switch (aDiff.operation) {
+ case INSERT:
+ text.append('+');
+ break;
+ case DELETE:
+ text.append('-');
+ break;
+ case EQUAL:
+ text.append(' ');
+ break;
+ }
+ try {
+ text.append(URLEncoder.encode(aDiff.text, "UTF-8").replace('+', ' '))
+ .append("\n");
+ } catch (UnsupportedEncodingException e) {
+ // Not likely on modern system.
+ throw new Error("This system does not support UTF-8.", e);
+ }
+ }
+ return unescapeForEncodeUriCompatability(text.toString());
+ }
+ }
+
+ /**
+ * Unescape selected chars for compatability with JavaScript's encodeURI.
+ * In speed critical applications this could be dropped since the
+ * receiving application will certainly decode these fine.
+ * Note that this function is case-sensitive. Thus "%3f" would not be
+ * unescaped. But this is ok because it is only called with the output of
+ * URLEncoder.encode which returns uppercase hex.
+ *
+ * Example: "%3F" -> "?", "%24" -> "$", etc.
+ *
+ * @param str The string to escape.
+ * @return The escaped string.
+ */
+ private static String unescapeForEncodeUriCompatability(String str) {
+ return str.replace("%21", "!").replace("%7E", "~")
+ .replace("%27", "'").replace("%28", "(").replace("%29", ")")
+ .replace("%3B", ";").replace("%2F", "/").replace("%3F", "?")
+ .replace("%3A", ":").replace("%40", "@").replace("%26", "&")
+ .replace("%3D", "=").replace("%2B", "+").replace("%24", "$")
+ .replace("%2C", ",").replace("%23", "#");
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt
index a104f2c031..2d65cac43d 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/data/SessionParams.kt
@@ -22,5 +22,6 @@ package im.vector.matrix.android.api.auth.data
*/
data class SessionParams(
val credentials: Credentials,
- val homeServerConnectionConfig: HomeServerConnectionConfig
+ val homeServerConnectionConfig: HomeServerConnectionConfig,
+ val isTokenValid: Boolean
)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/GlobalError.kt
similarity index 75%
rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt
rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/GlobalError.kt
index 80ee6811bb..b2bc585258 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/ConsentNotGivenError.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/GlobalError.kt
@@ -16,7 +16,8 @@
package im.vector.matrix.android.api.failure
-// This data class will be sent to the bus
-data class ConsentNotGivenError(
- val consentUri: String
-)
+// This class will be sent to the bus
+sealed class GlobalError {
+ data class InvalidToken(val softLogout: Boolean) : GlobalError()
+ data class ConsentNotGivenError(val consentUri: String) : GlobalError()
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
index f3f097bcc5..d7a6954fd5 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
@@ -22,45 +22,112 @@ import com.squareup.moshi.JsonClass
/**
* This data class holds the error defined by the matrix specifications.
* You shouldn't have to instantiate it.
+ * Ref: https://matrix.org/docs/spec/client_server/latest#api-standards
*/
@JsonClass(generateAdapter = true)
data class MatrixError(
+ /** unique string which can be used to handle an error message */
@Json(name = "errcode") val code: String,
+ /** human-readable error message */
@Json(name = "error") val message: String,
+ // For M_CONSENT_NOT_GIVEN
@Json(name = "consent_uri") val consentUri: String? = null,
- // RESOURCE_LIMIT_EXCEEDED data
+ // For M_RESOURCE_LIMIT_EXCEEDED
@Json(name = "limit_type") val limitType: String? = null,
@Json(name = "admin_contact") val adminUri: String? = null,
- // For LIMIT_EXCEEDED
- @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) {
+ // For M_LIMIT_EXCEEDED
+ @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
+ // For M_UNKNOWN_TOKEN
+ @Json(name = "soft_logout") val isSoftLogout: Boolean = false
+) {
companion object {
- const val FORBIDDEN = "M_FORBIDDEN"
- const val UNKNOWN = "M_UNKNOWN"
- const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
- const val MISSING_TOKEN = "M_MISSING_TOKEN"
- const val BAD_JSON = "M_BAD_JSON"
- const val NOT_JSON = "M_NOT_JSON"
- const val NOT_FOUND = "M_NOT_FOUND"
- const val LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
- const val USER_IN_USE = "M_USER_IN_USE"
- const val ROOM_IN_USE = "M_ROOM_IN_USE"
- const val BAD_PAGINATION = "M_BAD_PAGINATION"
- const val UNAUTHORIZED = "M_UNAUTHORIZED"
- const val OLD_VERSION = "M_OLD_VERSION"
- const val UNRECOGNIZED = "M_UNRECOGNIZED"
+ /** Forbidden access, e.g. joining a room without permission, failed login. */
+ const val M_FORBIDDEN = "M_FORBIDDEN"
+ /** An unknown error has occurred. */
+ const val M_UNKNOWN = "M_UNKNOWN"
+ /** The access token specified was not recognised. */
+ const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
+ /** No access token was specified for the request. */
+ const val M_MISSING_TOKEN = "M_MISSING_TOKEN"
+ /** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */
+ const val M_BAD_JSON = "M_BAD_JSON"
+ /** Request did not contain valid JSON. */
+ const val M_NOT_JSON = "M_NOT_JSON"
+ /** No resource was found for this request. */
+ const val M_NOT_FOUND = "M_NOT_FOUND"
+ /** Too many requests have been sent in a short period of time. Wait a while then try again. */
+ const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
- const val LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
- const val THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
- // Error code returned by the server when no account matches the given 3pid
- const val THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
- const val THREEPID_IN_USE = "M_THREEPID_IN_USE"
- const val SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
- const val TOO_LARGE = "M_TOO_LARGE"
+ /* ==========================================================================================
+ * Other error codes the client might encounter are
+ * ========================================================================================== */
+
+ /** Encountered when trying to register a user ID which has been taken. */
+ const val M_USER_IN_USE = "M_USER_IN_USE"
+ /** Sent when the room alias given to the createRoom API is already in use. */
+ const val M_ROOM_IN_USE = "M_ROOM_IN_USE"
+ /** (Not documented yet) */
+ const val M_BAD_PAGINATION = "M_BAD_PAGINATION"
+ /** The request was not correctly authorized. Usually due to login failures. */
+ const val M_UNAUTHORIZED = "M_UNAUTHORIZED"
+ /** (Not documented yet) */
+ const val M_OLD_VERSION = "M_OLD_VERSION"
+ /** The server did not understand the request. */
+ const val M_UNRECOGNIZED = "M_UNRECOGNIZED"
+ /** (Not documented yet) */
+ const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
+ /** Authentication could not be performed on the third party identifier. */
+ const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
+ /** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */
+ const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
+ /** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */
+ const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE"
+ /** The client's request used a third party server, eg. identity server, that this server does not trust. */
+ const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
+ /** The request or entity was too large. */
+ const val M_TOO_LARGE = "M_TOO_LARGE"
+ /** (Not documented yet) */
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
- const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
- const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
+ /** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example,
+ * a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory
+ * or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach
+ * out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account
+ * data, etc) and not routes which only read state (eg: /sync, get account data, etc). */
+ const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
+ /** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */
+ const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED"
+ /** Encountered when trying to register a user ID which is not valid. */
+ const val M_INVALID_USERNAME = "M_INVALID_USERNAME"
+ /** Sent when the initial state given to the createRoom API is invalid. */
+ const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE"
+ /** The server does not permit this third party identifier. This may happen if the server only permits,
+ * for example, email addresses from a particular domain. */
+ const val M_THREEPID_DENIED = "M_THREEPID_DENIED"
+ /** The client's request to create a room used a room version that the server does not support. */
+ const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
+ /** The client attempted to join a room that has a version the server does not support.
+ * Inspect the room_version property of the error response for the room's version. */
+ const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
+ /** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */
+ const val M_BAD_STATE = "M_BAD_STATE"
+ /** The room or resource does not permit guests to access it. */
+ const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
+ /** A Captcha is required to complete the request. */
+ const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
+ /** The Captcha provided did not match what was expected. */
+ const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
+ /** A required parameter was missing from the request. */
+ const val M_MISSING_PARAM = "M_MISSING_PARAM"
+ /** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */
+ const val M_INVALID_PARAM = "M_INVALID_PARAM"
+ /** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */
+ const val M_EXCLUSIVE = "M_EXCLUSIVE"
+ /** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */
+ const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
+ /** (Not documented yet) */
+ const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
index 2440713a40..1c1a3600c8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt
@@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams
-import im.vector.matrix.android.api.failure.ConsentNotGivenError
+import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@@ -62,6 +62,11 @@ interface Session :
*/
val sessionParams: SessionParams
+ /**
+ * The session is valid, i.e. it has a valid token so far
+ */
+ val isOpenable: Boolean
+
/**
* Useful shortcut to get access to the userId
*/
@@ -81,7 +86,7 @@ interface Session :
/**
* Launches infinite periodic background syncs
- * THis does not work in doze mode :/
+ * This does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/
*/
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
@@ -136,13 +141,10 @@ interface Session :
*/
interface Listener {
/**
- * The access token is not valid anymore
+ * Possible cases:
+ * - The access token is not valid anymore,
+ * - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/
- fun onInvalidToken()
-
- /**
- * A M_CONSENT_NOT_GIVEN error has been received from the homeserver
- */
- fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError)
+ fun onGlobalError(globalError: GlobalError)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt
index 2070845f46..fe2c958703 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageVerificationCancelContent.kt
@@ -24,7 +24,7 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoCancel
@JsonClass(generateAdapter = true)
-internal data class MessageVerificationCancelContent(
+data class MessageVerificationCancelContent(
@Json(name = "code") override val code: String? = null,
@Json(name = "reason") override val reason: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt
index 4cd8080dc3..71a422bac8 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/UserMentionSpan.kt
@@ -16,11 +16,12 @@
package im.vector.matrix.android.api.session.room.send
+import im.vector.matrix.android.api.util.MatrixItem
+
/**
* Tag class for spans that should mention a user.
* These Spans will be transformed into pills when detected in message to send
*/
interface UserMentionSpan {
- val displayName: String
- val userId: String
+ val matrixItem: MatrixItem
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt
index 5a0638fb6e..76ca9291ec 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/signout/SignOutService.kt
@@ -17,14 +17,31 @@
package im.vector.matrix.android.api.session.signout
import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.util.Cancelable
/**
- * This interface defines a method to sign out. It's implemented at the session level.
+ * This interface defines a method to sign out, or to renew the token. It's implemented at the session level.
*/
interface SignOutService {
/**
- * Sign out
+ * Ask the homeserver for a new access token.
+ * The same deviceId will be used
*/
- fun signOut(callback: MatrixCallback)
+ fun signInAgain(password: String,
+ callback: MatrixCallback): Cancelable
+
+ /**
+ * Update the session with credentials received after SSO
+ */
+ fun updateCredentials(credentials: Credentials,
+ callback: MatrixCallback): Cancelable
+
+ /**
+ * Sign out, and release the session, clear all the session data, including crypto data
+ * @param sigOutFromHomeserver true if the sign out request has to be done
+ */
+ fun signOut(sigOutFromHomeserver: Boolean,
+ callback: MatrixCallback): Cancelable
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt
index 4db40b2c55..4890c28331 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/sync/SyncState.kt
@@ -17,10 +17,11 @@
package im.vector.matrix.android.api.session.sync
sealed class SyncState {
- object IDLE : SyncState()
- data class RUNNING(val afterPause: Boolean) : SyncState()
- object PAUSED : SyncState()
- object KILLING : SyncState()
- object KILLED : SyncState()
- object NO_NETWORK : SyncState()
+ object Idle : SyncState()
+ data class Running(val afterPause: Boolean) : SyncState()
+ object Paused : SyncState()
+ object Killing : SyncState()
+ object Killed : SyncState()
+ object NoNetwork : SyncState()
+ object InvalidToken : SyncState()
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt
new file mode 100644
index 0000000000..4fed773ae2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/MatrixItem.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.matrix.android.api.util
+
+import im.vector.matrix.android.BuildConfig
+import im.vector.matrix.android.api.session.group.model.GroupSummary
+import im.vector.matrix.android.api.session.room.model.RoomSummary
+import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
+import im.vector.matrix.android.api.session.user.model.User
+import java.util.*
+
+sealed class MatrixItem(
+ open val id: String,
+ open val displayName: String?,
+ open val avatarUrl: String?
+) {
+ data class UserItem(override val id: String,
+ override val displayName: String? = null,
+ override val avatarUrl: String? = null)
+ : MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
+ init {
+ if (BuildConfig.DEBUG) checkId()
+ }
+ }
+
+ data class EventItem(override val id: String,
+ override val displayName: String? = null,
+ override val avatarUrl: String? = null)
+ : MatrixItem(id, displayName, avatarUrl) {
+ init {
+ if (BuildConfig.DEBUG) checkId()
+ }
+ }
+
+ data class RoomItem(override val id: String,
+ override val displayName: String? = null,
+ override val avatarUrl: String? = null)
+ : MatrixItem(id, displayName, avatarUrl) {
+ init {
+ if (BuildConfig.DEBUG) checkId()
+ }
+ }
+
+ data class RoomAliasItem(override val id: String,
+ override val displayName: String? = null,
+ override val avatarUrl: String? = null)
+ : MatrixItem(id, displayName, avatarUrl) {
+ init {
+ if (BuildConfig.DEBUG) checkId()
+ }
+ }
+
+ data class GroupItem(override val id: String,
+ override val displayName: String? = null,
+ override val avatarUrl: String? = null)
+ : MatrixItem(id, displayName, avatarUrl) {
+ init {
+ if (BuildConfig.DEBUG) checkId()
+ }
+ }
+
+ fun getBestName(): String {
+ return displayName?.takeIf { it.isNotBlank() } ?: id
+ }
+
+ protected fun checkId() {
+ if (!id.startsWith(getIdPrefix())) {
+ error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
+ }
+ }
+
+ /**
+ * Return the prefix as defined in the matrix spec (and not extracted from the id)
+ */
+ fun getIdPrefix() = when (this) {
+ is UserItem -> '@'
+ is EventItem -> '$'
+ is RoomItem -> '!'
+ is RoomAliasItem -> '#'
+ is GroupItem -> '+'
+ }
+
+ fun firstLetterOfDisplayName(): String {
+ return getBestName()
+ .let { dn ->
+ var startIndex = 0
+ val initial = dn[startIndex]
+
+ if (initial in listOf('@', '#', '+') && dn.length > 1) {
+ startIndex++
+ }
+
+ var length = 1
+ var first = dn[startIndex]
+
+ // LEFT-TO-RIGHT MARK
+ if (dn.length >= 2 && 0x200e == first.toInt()) {
+ startIndex++
+ first = dn[startIndex]
+ }
+
+ // check if it’s the start of a surrogate pair
+ if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) {
+ val second = dn[startIndex + 1]
+ if (second.toInt() in 0xDC00..0xDFFF) {
+ length++
+ }
+ }
+
+ dn.substring(startIndex, startIndex + length)
+ }
+ .toUpperCase(Locale.ROOT)
+ }
+
+ companion object {
+ private const val ircPattern = " (IRC)"
+ }
+}
+
+/* ==========================================================================================
+ * Extensions to create MatrixItem
+ * ========================================================================================== */
+
+fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
+fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
+fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
+fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt
index 22ed0b9a37..6b6321de36 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthModule.kt
@@ -53,7 +53,7 @@ internal abstract class AuthModule {
.name("matrix-sdk-auth.realm")
.modules(AuthRealmModule())
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
- .migration(AuthRealmMigration())
+ .migration(AuthRealmMigration)
.build()
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt
index f04f262d6e..95a9fbb506 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionCreator.kt
@@ -60,7 +60,8 @@ internal class DefaultSessionCreator @Inject constructor(
?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri
- ))
+ ),
+ isTokenValid = true)
sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt
index 17bcb9dc81..57c22b0053 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/SessionParamsStore.kt
@@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth
+import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
internal interface SessionParamsStore {
@@ -28,6 +29,10 @@ internal interface SessionParamsStore {
suspend fun save(sessionParams: SessionParams)
+ suspend fun setTokenInvalid(userId: String)
+
+ suspend fun updateCredentials(newCredentials: Credentials)
+
suspend fun delete(userId: String)
suspend fun deleteAll()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
index 5f1efb487b..83bf7b7822 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/AuthRealmMigration.kt
@@ -20,12 +20,10 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
-internal class AuthRealmMigration : RealmMigration {
+internal object AuthRealmMigration : RealmMigration {
- companion object {
- // Current schema version
- const val SCHEMA_VERSION = 1L
- }
+ // Current schema version
+ const val SCHEMA_VERSION = 2L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
@@ -46,5 +44,14 @@ internal class AuthRealmMigration : RealmMigration {
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
+
+ if (oldVersion <= 1) {
+ Timber.d("Step 1 -> 2")
+ Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
+
+ realm.schema.get("SessionParamsEntity")
+ ?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
+ ?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
+ }
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
index 1b15995ae6..a4774c632a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/RealmSessionParamsStore.kt
@@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth.db
+import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.database.awaitTransaction
@@ -75,6 +76,53 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
}
}
+ override suspend fun setTokenInvalid(userId: String) {
+ awaitTransaction(realmConfiguration) { realm ->
+ val currentSessionParams = realm
+ .where(SessionParamsEntity::class.java)
+ .equalTo(SessionParamsEntityFields.USER_ID, userId)
+ .findAll()
+ .firstOrNull()
+
+ if (currentSessionParams == null) {
+ // Should not happen
+ "Session param not found for user $userId"
+ .let { Timber.w(it) }
+ .also { error(it) }
+ } else {
+ currentSessionParams.isTokenValid = false
+ }
+ }
+ }
+
+ override suspend fun updateCredentials(newCredentials: Credentials) {
+ awaitTransaction(realmConfiguration) { realm ->
+ val currentSessionParams = realm
+ .where(SessionParamsEntity::class.java)
+ .equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId)
+ .findAll()
+ .map { mapper.map(it) }
+ .firstOrNull()
+
+ if (currentSessionParams == null) {
+ // Should not happen
+ "Session param not found for user ${newCredentials.userId}"
+ .let { Timber.w(it) }
+ .also { error(it) }
+ } else {
+ val newSessionParams = currentSessionParams.copy(
+ credentials = newCredentials,
+ isTokenValid = true
+ )
+
+ val entity = mapper.map(newSessionParams)
+ if (entity != null) {
+ realm.insertOrUpdate(entity)
+ }
+ }
+ }
+ }
+
override suspend fun delete(userId: String) {
awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt
index d0cc41d1ac..92511dccf7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsEntity.kt
@@ -22,5 +22,8 @@ import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "",
var credentialsJson: String = "",
- var homeServerConnectionConfigJson: String = ""
+ var homeServerConnectionConfigJson: String = "",
+ // Set to false when the token is invalid and the user has been soft logged out
+ // In case of hard logout, this object is deleted from DB
+ var isTokenValid: Boolean = true
) : RealmObject()
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt
index 8e64e86582..72e8087f3f 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/db/SessionParamsMapper.kt
@@ -36,7 +36,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentials == null || homeServerConnectionConfig == null) {
return null
}
- return SessionParams(credentials, homeServerConnectionConfig)
+ return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
}
fun map(sessionParams: SessionParams?): SessionParamsEntity? {
@@ -48,6 +48,10 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentialsJson == null || homeServerConnectionConfigJson == null) {
return null
}
- return SessionParamsEntity(sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson)
+ return SessionParamsEntity(
+ sessionParams.credentials.userId,
+ credentialsJson,
+ homeServerConnectionConfigJson,
+ sessionParams.isTokenValid)
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
index 1cc1a8a05a..91b3d6b056 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt
@@ -807,7 +807,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError
- && failure.error.code == MatrixError.NOT_FOUND) {
+ && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null)
} else {
@@ -830,7 +830,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError
- && failure.error.code == MatrixError.NOT_FOUND) {
+ && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null)
} else {
@@ -1209,8 +1209,8 @@ internal class KeysBackup @Inject constructor(
Timber.e(failure, "backupKeys: backupKeys failed.")
when (failure.error.code) {
- MatrixError.NOT_FOUND,
- MatrixError.WRONG_ROOM_KEYS_VERSION -> {
+ MatrixError.M_NOT_FOUND,
+ MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt
index ae8a40f296..00b7d7320a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RequestVerificationDMTask.kt
@@ -29,12 +29,11 @@ import javax.inject.Inject
internal interface RequestVerificationDMTask : Task {
data class Params(
- val roomId: String,
- val from: String,
- val methods: List,
- val to: String,
+ val event: Event,
val cryptoService: CryptoService
)
+
+ fun createParamsAndLocalEcho(roomId: String, from: String, methods: List, to: String, cryptoService: CryptoService): Params
}
internal class DefaultRequestVerificationDMTask @Inject constructor(
@@ -45,8 +44,18 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
private val roomAPI: RoomAPI)
: RequestVerificationDMTask {
+ override fun createParamsAndLocalEcho(roomId: String, from: String, methods: List, to: String, cryptoService: CryptoService)
+ : RequestVerificationDMTask.Params {
+ val event = localEchoEventFactory.createVerificationRequest(roomId, from, to, methods)
+ .also { localEchoEventFactory.saveLocalEcho(monarchy, it) }
+ return RequestVerificationDMTask.Params(
+ event,
+ cryptoService
+ )
+ }
+
override suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse {
- val event = createRequestEvent(params)
+ val event = handleEncryption(params)
val localID = event.eventId!!
try {
@@ -54,7 +63,7 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
val executeRequest = executeRequest {
apiCall = roomAPI.send(
localID,
- roomId = params.roomId,
+ roomId = event.roomId ?: "",
content = event.content,
eventType = event.type // message or room.encrypted
)
@@ -67,14 +76,13 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
}
}
- private suspend fun createRequestEvent(params: RequestVerificationDMTask.Params): Event {
- val event = localEchoEventFactory.createVerificationRequest(params.roomId, params.from, params.to, params.methods)
- .also { localEchoEventFactory.saveLocalEcho(monarchy, it) }
- if (params.cryptoService.isRoomEncrypted(params.roomId)) {
+ private suspend fun handleEncryption(params: RequestVerificationDMTask.Params): Event {
+ val roomId = params.event.roomId ?: ""
+ if (params.cryptoService.isRoomEncrypted(roomId)) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
- params.roomId,
- event,
+ roomId,
+ params.event,
listOf("m.relates_to"),
params.cryptoService
))
@@ -82,6 +90,6 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
// We said it's ok to send verification request in clear
}
}
- return event
+ return params.event
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt
index b850a1a1e6..cf1b2d233a 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendVerificationMessageTask.kt
@@ -34,10 +34,14 @@ import javax.inject.Inject
internal interface SendVerificationMessageTask : Task {
data class Params(
val type: String,
- val roomId: String,
- val content: Content,
+ val event: Event,
val cryptoService: CryptoService?
)
+
+ fun createParamsAndLocalEcho(type: String,
+ roomId: String,
+ content: Content,
+ cryptoService: CryptoService?) : Params
}
internal class DefaultSendVerificationMessageTask @Inject constructor(
@@ -48,8 +52,28 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
@UserId private val userId: String,
private val roomAPI: RoomAPI) : SendVerificationMessageTask {
+ override fun createParamsAndLocalEcho(type: String, roomId: String, content: Content, cryptoService: CryptoService?): SendVerificationMessageTask.Params {
+ val localID = LocalEcho.createLocalEchoId()
+ val event = Event(
+ roomId = roomId,
+ originServerTs = System.currentTimeMillis(),
+ senderId = userId,
+ eventId = localID,
+ type = type,
+ content = content,
+ unsignedData = UnsignedData(age = null, transactionId = localID)
+ ).also {
+ localEchoEventFactory.saveLocalEcho(monarchy, it)
+ }
+ return SendVerificationMessageTask.Params(
+ type,
+ event,
+ cryptoService
+ )
+ }
+
override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse {
- val event = createRequestEvent(params)
+ val event = handleEncryption(params)
val localID = event.eventId!!
try {
@@ -57,7 +81,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
val executeRequest = executeRequest {
apiCall = roomAPI.send(
localID,
- roomId = params.roomId,
+ roomId = event.roomId ?: "",
content = event.content,
eventType = event.type
)
@@ -70,25 +94,12 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
}
}
- private suspend fun createRequestEvent(params: SendVerificationMessageTask.Params): Event {
- val localID = LocalEcho.createLocalEchoId()
- val event = Event(
- roomId = params.roomId,
- originServerTs = System.currentTimeMillis(),
- senderId = userId,
- eventId = localID,
- type = params.type,
- content = params.content,
- unsignedData = UnsignedData(age = null, transactionId = localID)
- ).also {
- localEchoEventFactory.saveLocalEcho(monarchy, it)
- }
-
- if (params.cryptoService?.isRoomEncrypted(params.roomId) == true) {
+ private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
+ if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
- params.roomId,
- event,
+ params.event.roomId ?: "",
+ params.event,
listOf("m.relates_to"),
params.cryptoService
))
@@ -96,6 +107,6 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
// We said it's ok to send verification request in clear
}
}
- return event
+ return params.event
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt
index d54ae3a22a..1d50fc89fe 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt
@@ -38,7 +38,6 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DefaultRequestVerificationDMTask
-import im.vector.matrix.android.internal.crypto.tasks.RequestVerificationDMTask
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.TaskConstraints
@@ -109,6 +108,7 @@ internal class DefaultSasVerificationService @Inject constructor(
onRoomStartRequestReceived(event)
}
EventType.KEY_VERIFICATION_CANCEL -> {
+ // MultiSessions | ignore events if i didn't sent the start from this device, or accepted from this device
onRoomCancelReceived(event)
}
EventType.KEY_VERIFICATION_ACCEPT -> {
@@ -538,7 +538,7 @@ internal class DefaultSasVerificationService @Inject constructor(
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback?) {
requestVerificationDMTask.configureWith(
- RequestVerificationDMTask.Params(
+ requestVerificationDMTask.createParamsAndLocalEcho(
roomId = roomId,
from = credentials.deviceId ?: "",
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt
index e40e8be31f..91adbbd705 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SasTransportRoomMessage.kt
@@ -50,7 +50,7 @@ internal class SasTransportRoomMessage(
Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo")
sendVerificationMessageTask.configureWith(
- SendVerificationMessageTask.Params(
+ sendVerificationMessageTask.createParamsAndLocalEcho(
type,
roomId,
verificationInfo.toEventContent()!!,
@@ -82,7 +82,7 @@ internal class SasTransportRoomMessage(
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code")
sendVerificationMessageTask.configureWith(
- SendVerificationMessageTask.Params(
+ sendVerificationMessageTask.createParamsAndLocalEcho(
EventType.KEY_VERIFICATION_CANCEL,
roomId,
MessageVerificationCancelContent.create(transactionId, code).toContent(),
@@ -108,7 +108,7 @@ internal class SasTransportRoomMessage(
override fun done(transactionId: String) {
sendVerificationMessageTask.configureWith(
- SendVerificationMessageTask.Params(
+ sendVerificationMessageTask.createParamsAndLocalEcho(
EventType.KEY_VERIFICATION_DONE,
roomId,
MessageVerificationDoneContent(
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt
index 2bca40c855..2fee568895 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationMessageLiveObserver.kt
@@ -19,14 +19,15 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toModel
-import im.vector.matrix.android.api.session.room.model.message.MessageContent
-import im.vector.matrix.android.api.session.room.model.message.MessageType
+import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types
+import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor
@@ -36,13 +37,16 @@ import io.realm.RealmResults
import timber.log.Timber
import java.util.*
import javax.inject.Inject
+import kotlin.collections.ArrayList
-internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
- @UserId private val userId: String,
- private val cryptoService: CryptoService,
- private val sasVerificationService: DefaultSasVerificationService,
- private val taskExecutor: TaskExecutor) :
- RealmLiveEntityObserver(realmConfiguration) {
+internal class VerificationMessageLiveObserver @Inject constructor(
+ @SessionDatabase realmConfiguration: RealmConfiguration,
+ @UserId private val userId: String,
+ @DeviceId private val deviceId: String?,
+ private val cryptoService: CryptoService,
+ private val sasVerificationService: DefaultSasVerificationService,
+ private val taskExecutor: TaskExecutor
+) : RealmLiveEntityObserver(realmConfiguration) {
override val query = Monarchy.Query {
EventEntity.types(it, listOf(
@@ -57,6 +61,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
)
}
+ val transactionsHandledByOtherDevice = ArrayList()
+
override fun onChange(results: RealmResults, changeSet: OrderedCollectionChangeSet) {
// TODO do that in a task
// TODO how to ignore when it's an initial sync?
@@ -64,8 +70,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
.asSequence()
.mapNotNull { results[it]?.asDomain() }
.filterNot {
- // ignore mines ^^
- it.senderId == userId
+ // ignore local echos
+ LocalEcho.isLocalEchoId(it.eventId ?: "")
}
.toList()
@@ -77,7 +83,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs
events.forEach { event ->
- Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
+ Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
Timber.v("## SAS Verification live observer: received msgId: $event")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
@@ -111,6 +117,45 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
}
}
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
+
+ if (event.senderId == userId) {
+ // If it's send from me, we need to keep track of Requests or Start
+ // done from another device of mine
+
+ if (EventType.MESSAGE == event.type) {
+ val msgType = event.getClearContent().toModel()?.type
+ if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
+ event.getClearContent().toModel()?.let {
+ if (it.fromDevice != deviceId) {
+ // The verification is requested from another device
+ Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
+ event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
+ }
+ }
+ }
+ } else if (EventType.KEY_VERIFICATION_START == event.type) {
+ event.getClearContent().toModel()?.let {
+ if (it.fromDevice != deviceId) {
+ // The verification is started from another device
+ Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
+ it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
+ }
+ }
+ } else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
+ event.getClearContent().toModel()?.relatesTo?.eventId?.let {
+ transactionsHandledByOtherDevice.remove(it)
+ }
+ }
+
+ return@forEach
+ }
+
+ val relatesTo = event.getClearContent().toModel()?.relatesTo?.eventId
+ if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
+ // Ignore this event, it is directed to another of my devices
+ Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
+ return@forEach
+ }
when (event.getClearType()) {
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT,
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt
index 0e38618590..3444a8fa70 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/StringQualifiers.kt
@@ -25,6 +25,13 @@ import javax.inject.Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class UserId
+/**
+ * Used to inject the userId
+ */
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class DeviceId
+
/**
* Used to inject the md5 of the userId
*/
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt
index 2630560e45..e0257bfc83 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/AccessTokenInterceptor.kt
@@ -16,19 +16,29 @@
package im.vector.matrix.android.internal.network
-import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.internal.auth.SessionParamsStore
+import im.vector.matrix.android.internal.di.UserId
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
-internal class AccessTokenInterceptor @Inject constructor(private val credentials: Credentials) : Interceptor {
+internal class AccessTokenInterceptor @Inject constructor(
+ @UserId private val userId: String,
+ private val sessionParamsStore: SessionParamsStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
- val newRequestBuilder = request.newBuilder()
- // Add the access token to all requests if it is set
- newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken)
- request = newRequestBuilder.build()
+
+ accessToken?.let {
+ val newRequestBuilder = request.newBuilder()
+ // Add the access token to all requests if it is set
+ newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
+ request = newRequestBuilder.build()
+ }
+
return chain.proceed(request)
}
+
+ private val accessToken
+ get() = sessionParamsStore.get(userId)?.credentials?.accessToken
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt
index 0bce924abc..e95c161491 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/RetrofitExtensions.kt
@@ -19,8 +19,8 @@
package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonEncodingException
-import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -31,6 +31,7 @@ import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
import java.io.IOException
+import java.net.HttpURLConnection
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -98,7 +99,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management
- EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri))
+ EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
+ } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
+ && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
+ // Also send this error to the bus, for a global management
+ EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout))
}
return Failure.ServerError(matrixError, httpCode)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
index 4b33e28000..d6a0206eca 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt
@@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
-import im.vector.matrix.android.api.failure.ConsentNotGivenError
+import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
@@ -42,10 +42,14 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService
+import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -72,6 +76,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy,
private val syncThreadProvider: Provider,
private val contentUrlResolver: ContentUrlResolver,
+ private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy,
private val homeServerCapabilitiesService: Lazy)
@@ -94,6 +99,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private var syncThread: SyncThread? = null
+ override val isOpenable: Boolean
+ get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false
+
@MainThread
override fun open() {
assertMainThread()
@@ -170,8 +178,16 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
}
@Subscribe(threadMode = ThreadMode.MAIN)
- fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) {
- sessionListeners.dispatchConsentNotGiven(consentNotGivenError)
+ fun onGlobalError(globalError: GlobalError) {
+ if (globalError is GlobalError.InvalidToken
+ && globalError.softLogout) {
+ // Mark the token has invalid
+ GlobalScope.launch(Dispatchers.IO) {
+ sessionParamsStore.setTokenInvalid(myUserId)
+ }
+ }
+
+ sessionListeners.dispatchGlobalError(globalError)
}
override fun contentUrlResolver() = contentUrlResolver
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
index 25678bef66..ff3bc0b073 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt
@@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session
-import im.vector.matrix.android.api.failure.ConsentNotGivenError
+import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session
import javax.inject.Inject
@@ -36,10 +36,10 @@ internal class SessionListeners @Inject constructor() {
}
}
- fun dispatchConsentNotGiven(consentNotGivenError: ConsentNotGivenError) {
+ fun dispatchGlobalError(globalError: GlobalError) {
synchronized(listeners) {
listeners.forEach {
- it.onConsentNotGivenError(consentNotGivenError)
+ it.onGlobalError(globalError)
}
}
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt
index 883fd37745..6f4136405e 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt
@@ -77,6 +77,13 @@ internal abstract class SessionModule {
return credentials.userId
}
+ @JvmStatic
+ @DeviceId
+ @Provides
+ fun providesDeviceId(credentials: Credentials): String? {
+ return credentials.deviceId
+ }
+
@JvmStatic
@UserMd5
@Provides
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
index a162849fa9..cf7a8a9275 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt
@@ -53,6 +53,9 @@ enum class VerificationState {
DONE
}
+fun VerificationState.isCanceled() : Boolean {
+ return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER
+}
/**
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/
@@ -433,26 +436,27 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
?: ReferencesAggregatedContent(VerificationState.REQUEST.name)
// TODO ignore invalid messages? e.g a START after a CANCEL?
// i.e. never change state if already canceled/done
+ val currentState = VerificationState.values().firstOrNull { data.verificationSummary == it.name }
val newState = when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> {
- VerificationState.WAITING
+ updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_ACCEPT -> {
- VerificationState.WAITING
+ updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_KEY -> {
- VerificationState.WAITING
+ updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_MAC -> {
- VerificationState.WAITING
+ updateVerificationState(currentState, VerificationState.WAITING)
}
EventType.KEY_VERIFICATION_CANCEL -> {
- if (event.senderId == userId) {
+ updateVerificationState(currentState, if (event.senderId == userId) {
VerificationState.CANCELED_BY_ME
- } else VerificationState.CANCELED_BY_OTHER
+ } else VerificationState.CANCELED_BY_OTHER)
}
EventType.KEY_VERIFICATION_DONE -> {
- VerificationState.DONE
+ updateVerificationState(currentState, VerificationState.DONE)
}
else -> VerificationState.REQUEST
}
@@ -468,4 +472,18 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
verifSummary.sourceEvents.add(event.eventId)
}
}
+
+ private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState) : VerificationState {
+ // Cancel is always prioritary ?
+ // Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
+ // consider as canceled
+ if (newState == VerificationState.CANCELED_BY_OTHER || newState == VerificationState.CANCELED_BY_ME) {
+ return newState
+ }
+ // never move out of cancel
+ if (oldState == VerificationState.CANCELED_BY_OTHER || oldState == VerificationState.CANCELED_BY_ME) {
+ return oldState
+ }
+ return newState
+ }
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt
index 183b0ad9b8..6527113054 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendEventWorker.kt
@@ -79,7 +79,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
private fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection
- || (this is Failure.ServerError && this.error.code == MatrixError.LIMIT_EXCEEDED)
+ || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
}
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt
index 580e49b2ce..c079e456c0 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/pills/TextPillsUtils.kt
@@ -65,7 +65,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill
append(text, currIndex, start)
// append the pill
- append(String.format(template, urlSpan.userId, urlSpan.displayName))
+ append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName))
currIndex = end
}
// append text after the last pill
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
index b48ac2c78a..17c91011e7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/DefaultSignOutService.kt
@@ -17,17 +17,43 @@
package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.signout.SignOutService
+import im.vector.matrix.android.api.util.Cancelable
+import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
+import im.vector.matrix.android.internal.task.launchToCallback
+import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
+import kotlinx.coroutines.GlobalScope
import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
+ private val signInAgainTask: SignInAgainTask,
+ private val sessionParamsStore: SessionParamsStore,
+ private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService {
- override fun signOut(callback: MatrixCallback) {
- signOutTask
- .configureWith {
+ override fun signInAgain(password: String,
+ callback: MatrixCallback): Cancelable {
+ return signInAgainTask
+ .configureWith(SignInAgainTask.Params(password)) {
+ this.callback = callback
+ }
+ .executeBy(taskExecutor)
+ }
+
+ override fun updateCredentials(credentials: Credentials,
+ callback: MatrixCallback): Cancelable {
+ return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
+ sessionParamsStore.updateCredentials(credentials)
+ }
+ }
+
+ override fun signOut(sigOutFromHomeserver: Boolean,
+ callback: MatrixCallback): Cancelable {
+ return signOutTask
+ .configureWith(SignOutTask.Params(sigOutFromHomeserver)) {
this.callback = callback
}
.executeBy(taskExecutor)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
new file mode 100644
index 0000000000..666852c988
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignInAgainTask.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.matrix.android.internal.session.signout
+
+import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.api.auth.data.SessionParams
+import im.vector.matrix.android.internal.auth.SessionParamsStore
+import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
+import im.vector.matrix.android.internal.network.executeRequest
+import im.vector.matrix.android.internal.task.Task
+import javax.inject.Inject
+
+internal interface SignInAgainTask : Task {
+ data class Params(
+ val password: String
+ )
+}
+
+internal class DefaultSignInAgainTask @Inject constructor(
+ private val signOutAPI: SignOutAPI,
+ private val sessionParams: SessionParams,
+ private val sessionParamsStore: SessionParamsStore) : SignInAgainTask {
+
+ override suspend fun execute(params: SignInAgainTask.Params) {
+ val newCredentials = executeRequest {
+ apiCall = signOutAPI.loginAgain(
+ PasswordLoginParams.userIdentifier(
+ // Reuse the same userId
+ sessionParams.credentials.userId,
+ params.password,
+ // The spec says the initial device name will be ignored
+ // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
+ // but https://github.com/matrix-org/synapse/issues/6525
+ // Reuse the same deviceId
+ deviceId = sessionParams.credentials.deviceId
+ )
+ )
+ }
+
+ sessionParamsStore.updateCredentials(newCredentials)
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt
index 2f19fee847..9db7c7d915 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutAPI.kt
@@ -16,12 +16,27 @@
package im.vector.matrix.android.internal.session.signout
+import im.vector.matrix.android.api.auth.data.Credentials
+import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
+import retrofit2.http.Body
+import retrofit2.http.Headers
import retrofit2.http.POST
internal interface SignOutAPI {
+ /**
+ * Attempt to login again to the same account.
+ * Set all the timeouts to 1 minute
+ * It is similar to [AuthAPI.login]
+ *
+ * @param loginParams the login parameters
+ */
+ @Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
+ fun loginAgain(@Body loginParams: PasswordLoginParams): Call
+
/**
* Invalidate the access token, so that it can no longer be used for authorization.
*/
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt
index c55c82274d..590729837b 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutModule.kt
@@ -37,8 +37,11 @@ internal abstract class SignOutModule {
}
@Binds
- abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask
+ abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
@Binds
- abstract fun bindSignOutService(signOutService: DefaultSignOutService): SignOutService
+ abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask
+
+ @Binds
+ abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
index 7bff2936fd..51cb22c988 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/signout/SignOutTask.kt
@@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.signout
import android.content.Context
import im.vector.matrix.android.BuildConfig
+import im.vector.matrix.android.api.failure.Failure
+import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
@@ -32,9 +34,14 @@ import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
+import java.net.HttpURLConnection
import javax.inject.Inject
-internal interface SignOutTask : Task
+internal interface SignOutTask : Task {
+ data class Params(
+ val sigOutFromHomeserver: Boolean
+ )
+}
internal class DefaultSignOutTask @Inject constructor(private val context: Context,
@UserId private val userId: String,
@@ -49,10 +56,26 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask {
- override suspend fun execute(params: Unit) {
- Timber.d("SignOut: send request...")
- executeRequest {
- apiCall = signOutAPI.signOut()
+ override suspend fun execute(params: SignOutTask.Params) {
+ // It should be done even after a soft logout, to be sure the deviceId is deleted on the
+ if (params.sigOutFromHomeserver) {
+ Timber.d("SignOut: send request...")
+ try {
+ executeRequest {
+ apiCall = signOutAPI.signOut()
+ }
+ } catch (throwable: Throwable) {
+ // Maybe due to https://github.com/matrix-org/synapse/issues/5755
+ if (throwable is Failure.ServerError
+ && throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
+ && throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) {
+ // Also throwable.error.isSoftLogout should be true
+ // Ignore
+ Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755")
+ } else {
+ throw throwable
+ }
+ }
}
Timber.d("SignOut: release session...")
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
index 51c02456d7..9f9e67bd2e 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncTask.kt
@@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.R
-import im.vector.matrix.android.api.failure.Failure
-import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
@@ -67,17 +65,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
}
- val syncResponse = try {
- executeRequest {
- apiCall = syncAPI.sync(requestParams)
- }
- } catch (throwable: Throwable) {
- // Intercept 401
- if (throwable is Failure.ServerError
- && throwable.error.code == MatrixError.UNKNOWN_TOKEN) {
- sessionParamsStore.delete(userId)
- }
- throw throwable
+ val syncResponse = executeRequest {
+ apiCall = syncAPI.sync(requestParams)
}
syncResponseHandler.handleResponse(syncResponse, token)
syncTokenStore.saveToken(syncResponse.nextBatch)
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
index 4e57aa5be1..71734cdfe7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt
@@ -147,7 +147,7 @@ open class SyncService : Service() {
}
if (failure is Failure.ServerError
- && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
+ && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread
stopSelf()
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt
index d8de292d70..69e03c6269 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt
@@ -44,19 +44,20 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val taskExecutor: TaskExecutor
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
- private var state: SyncState = SyncState.IDLE
+ private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData()
private val lock = Object()
private var cancelableTask: Cancelable? = null
private var isStarted = false
+ private var isTokenValid = true
init {
- updateStateTo(SyncState.IDLE)
+ updateStateTo(SyncState.Idle)
}
fun setInitialForeground(initialForeground: Boolean) {
- val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED
+ val newState = if (initialForeground) SyncState.Idle else SyncState.Paused
updateStateTo(newState)
}
@@ -64,6 +65,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (!isStarted) {
Timber.v("Resume sync...")
isStarted = true
+ // Check again the token validity
+ isTokenValid = true
lock.notify()
}
}
@@ -78,7 +81,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
fun kill() = synchronized(lock) {
Timber.v("Kill sync...")
- updateStateTo(SyncState.KILLING)
+ updateStateTo(SyncState.Killing)
cancelableTask?.cancel()
lock.notify()
}
@@ -100,26 +103,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this)
- while (state != SyncState.KILLING) {
+ while (state != SyncState.Killing) {
Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...")
- updateStateTo(SyncState.NO_NETWORK)
+ updateStateTo(SyncState.NoNetwork)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...")
- updateStateTo(SyncState.PAUSED)
+ updateStateTo(SyncState.Paused)
+ synchronized(lock) { lock.wait() }
+ Timber.v("...unlocked")
+ } else if (!isTokenValid) {
+ Timber.v("Token is invalid. Waiting...")
+ updateStateTo(SyncState.InvalidToken)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else {
- if (state !is SyncState.RUNNING) {
- updateStateTo(SyncState.RUNNING(afterPause = true))
+ if (state !is SyncState.Running) {
+ updateStateTo(SyncState.Running(afterPause = true))
}
// No timeout after a pause
- val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
+ val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
Timber.v("Execute sync request with timeout $timeout")
val latch = CountDownLatch(1)
@@ -141,10 +149,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} else if (failure is Failure.Cancelled) {
Timber.v("Cancelled")
} else if (failure is Failure.ServerError
- && (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
- // No token or invalid token, stop the thread
+ && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
+ // No token or invalid token
Timber.w(failure)
- updateStateTo(SyncState.KILLING)
+ isTokenValid = false
+ isStarted = false
} else {
Timber.e(failure)
@@ -163,8 +172,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
latch.await()
state.let {
- if (it is SyncState.RUNNING && it.afterPause) {
- updateStateTo(SyncState.RUNNING(afterPause = false))
+ if (it is SyncState.Running && it.afterPause) {
+ updateStateTo(SyncState.Running(afterPause = false))
}
}
@@ -172,7 +181,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
}
}
Timber.v("Sync killed")
- updateStateTo(SyncState.KILLED)
+ updateStateTo(SyncState.Killed)
backgroundDetectionObserver.unregister(this)
networkConnectivityChecker.unregister(this)
}
diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt
index 31da372bbe..f19bebe482 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt
@@ -16,9 +16,7 @@
package im.vector.matrix.android.internal.util
-import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber
-import java.util.Locale
/**
* Convert a string to an UTF8 String
@@ -51,10 +49,3 @@ fun convertFromUTF8(s: String): String {
s
}
}
-
-fun String?.firstLetterOfDisplayName(): String {
- if (this.isNullOrEmpty()) return ""
- val isUserId = MatrixPatterns.isUserId(this)
- val firstLetterIndex = if (isUserId) 1 else 0
- return this[firstLetterIndex].toString().toUpperCase(Locale.ROOT)
-}
diff --git a/settings.gradle b/settings.gradle
index 793f7a3426..d020abade4 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx'
+include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
diff --git a/vector/build.gradle b/vector/build.gradle
index 8d35851bbd..8a2df7c120 100644
--- a/vector/build.gradle
+++ b/vector/build.gradle
@@ -229,6 +229,7 @@ dependencies {
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")
+ implementation project(":diff-match-patch")
implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -341,8 +342,6 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
- implementation 'diff_match_patch:diff_match_patch:current'
-
implementation "androidx.emoji:emoji-appcompat:1.0.0"
// TESTS
diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml
index 5f1687c9c9..61eebc99db 100644
--- a/vector/src/main/AndroidManifest.xml
+++ b/vector/src/main/AndroidManifest.xml
@@ -98,6 +98,10 @@
+
+
null
is Failure.NetworkConnection -> {
@@ -41,6 +42,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException ->
// Invalid homeserver?
+ // TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host)
else ->
stringProvider.getString(R.string.error_no_network)
@@ -52,23 +54,23 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
// Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted)
}
- throwable.error.code == MatrixError.FORBIDDEN
+ throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message == "Invalid password" -> {
stringProvider.getString(R.string.auth_invalid_login_param)
}
- throwable.error.code == MatrixError.USER_IN_USE -> {
+ throwable.error.code == MatrixError.M_USER_IN_USE -> {
stringProvider.getString(R.string.login_signup_error_user_in_use)
}
- throwable.error.code == MatrixError.BAD_JSON -> {
+ throwable.error.code == MatrixError.M_BAD_JSON -> {
stringProvider.getString(R.string.login_error_bad_json)
}
- throwable.error.code == MatrixError.NOT_JSON -> {
+ throwable.error.code == MatrixError.M_NOT_JSON -> {
stringProvider.getString(R.string.login_error_not_json)
}
- throwable.error.code == MatrixError.LIMIT_EXCEEDED -> {
+ throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
limitExceededError(throwable.error)
}
- throwable.error.code == MatrixError.THREEPID_NOT_FOUND -> {
+ throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found)
}
else -> {
diff --git a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt b/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt
index dd4257fe1f..614340bd3d 100644
--- a/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt
+++ b/vector/src/main/java/im/vector/riotx/core/error/Extensions.kt
@@ -21,6 +21,6 @@ import im.vector.matrix.android.api.failure.MatrixError
import javax.net.ssl.HttpsURLConnection
fun Throwable.is401(): Boolean {
- return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
- && this.error.code == MatrixError.UNAUTHORIZED)
+ return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
+ && error.code == MatrixError.M_UNAUTHORIZED)
}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
index 0ce4d04497..67e866bb82 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/Session.kt
@@ -19,6 +19,7 @@ package im.vector.riotx.core.extensions
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener
@@ -40,3 +41,11 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener,
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
// @Inject lateinit var keyRequestHandler: KeyRequestHandler
}
+
+/**
+ * Tell is the session has unsaved e2e keys in the backup
+ */
+fun Session.hasUnsavedKeys(): Boolean {
+ return inboundGroupSessionsCount(false) > 0
+ && getKeysBackupService().state != KeysBackupState.ReadyToBackUp
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt b/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt
index 9a26cedf9a..7614fda619 100644
--- a/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt
+++ b/vector/src/main/java/im/vector/riotx/core/extensions/UrlExtensions.kt
@@ -35,3 +35,12 @@ fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder
return this
}
+
+/**
+ * Ex: "https://matrix.org/" -> "matrix.org"
+ */
+fun String?.toReducedUrl(): String {
+ return (this ?: "")
+ .substringAfter("://")
+ .trim { it == '/' }
+}
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
index 79b040cd41..02e28e079c 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseActivity.kt
@@ -38,12 +38,15 @@ import butterknife.Unbinder
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar
+import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.di.*
import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.utils.toast
+import im.vector.riotx.features.MainActivity
+import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.consent.ConsentNotGivenHelper
import im.vector.riotx.features.navigation.Navigator
@@ -89,6 +92,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder
+ // Filter for multiple invalid token error
+ private var mainActivityStarted = false
+
private var unBinder: Unbinder? = null
private var savedInstanceState: Bundle? = null
@@ -153,9 +159,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
})
sessionListener = getVectorComponent().sessionListener()
- sessionListener.consentNotGivenLiveData.observeEvent(this) {
- consentNotGivenHelper.displayDialog(it.consentUri,
- activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
+ sessionListener.globalErrorLiveData.observeEvent(this) {
+ handleGlobalError(it)
}
doBeforeSetContentView()
@@ -180,6 +185,33 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}
}
+ private fun handleGlobalError(globalError: GlobalError) {
+ when (globalError) {
+ is GlobalError.InvalidToken ->
+ handleInvalidToken(globalError)
+ is GlobalError.ConsentNotGivenError ->
+ consentNotGivenHelper.displayDialog(globalError.consentUri,
+ activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
+ }
+ }
+
+ protected open fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
+ Timber.w("Invalid token event received")
+ if (mainActivityStarted) {
+ return
+ }
+
+ mainActivityStarted = true
+
+ MainActivity.restartApp(this,
+ MainActivityArgs(
+ clearCredentials = !globalError.softLogout,
+ isUserLoggedOut = true,
+ isSoftLogout = globalError.softLogout
+ )
+ )
+ }
+
override fun onDestroy() {
super.onDestroy()
unBinder?.unbind()
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
index 924cb6c7bc..efcbdfff39 100644
--- a/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/core/platform/VectorBaseFragment.kt
@@ -34,6 +34,7 @@ import com.bumptech.glide.util.Util.assertMainThread
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent
+import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.Navigator
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
@@ -49,12 +50,14 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
}
/* ==========================================================================================
- * Navigator
+ * Navigator and other common objects
* ========================================================================================== */
- protected lateinit var navigator: Navigator
private lateinit var screenComponent: ScreenComponent
+ protected lateinit var navigator: Navigator
+ protected lateinit var errorFormatter: ErrorFormatter
+
/* ==========================================================================================
* View model
* ========================================================================================== */
@@ -74,6 +77,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = screenComponent.navigator()
+ errorFormatter = screenComponent.errorFormatter()
viewModelFactory = screenComponent.viewModelFactory()
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
injectWith(injector())
diff --git a/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt b/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt
index a2c3e90910..c7fcf85a16 100755
--- a/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt
+++ b/vector/src/main/java/im/vector/riotx/core/preference/UserAvatarPreference.kt
@@ -23,6 +23,8 @@ import android.widget.ProgressBar
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.util.MatrixItem
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.home.AvatarRenderer
@@ -59,9 +61,9 @@ open class UserAvatarPreference : Preference {
val session = mSession ?: return
val view = mAvatarView ?: return
session.getUser(session.myUserId)?.let {
- avatarRenderer.render(it, view)
+ avatarRenderer.render(it.toMatrixItem(), view)
} ?: run {
- avatarRenderer.render(null, session.myUserId, null, view)
+ avatarRenderer.render(MatrixItem.UserItem(session.myUserId), view)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt
index 6e4229908f..c5e2fdf375 100644
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt
+++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadReceiptsView.kt
@@ -26,6 +26,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
+import im.vector.riotx.features.home.room.detail.timeline.item.toMatrixItem
import kotlinx.android.synthetic.main.view_read_receipts.view.*
private const val MAX_RECEIPT_DISPLAYED = 5
@@ -59,7 +60,7 @@ class ReadReceiptsView @JvmOverloads constructor(
receiptAvatars[index].visibility = View.INVISIBLE
} else {
receiptAvatars[index].visibility = View.VISIBLE
- avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index])
+ avatarRenderer.render(receiptData.toMatrixItem(), receiptAvatars[index])
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
index 7064ad0d49..041eb85a11 100644
--- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt
@@ -19,9 +19,11 @@ package im.vector.riotx.features
import android.app.Activity
import android.content.Intent
import android.os.Bundle
+import android.os.Parcelable
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import im.vector.matrix.android.api.MatrixCallback
+import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
@@ -30,6 +32,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.LoginActivity
+import im.vector.riotx.features.notifications.NotificationDrawerManager
+import im.vector.riotx.features.signout.hard.SignedOutActivity
+import im.vector.riotx.features.signout.soft.SoftLogoutActivity
+import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -37,23 +43,37 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
+@Parcelize
+data class MainActivityArgs(
+ val clearCache: Boolean = false,
+ val clearCredentials: Boolean = false,
+ val isUserLoggedOut: Boolean = false,
+ val isSoftLogout: Boolean = false
+) : Parcelable
+
+/**
+ * This is the entry point of RiotX
+ * This Activity, when started with argument, is also doing some cleanup when user disconnects,
+ * clears cache, is logged out, or is soft logged out
+ */
class MainActivity : VectorBaseActivity() {
companion object {
- private const val EXTRA_CLEAR_CACHE = "EXTRA_CLEAR_CACHE"
- private const val EXTRA_CLEAR_CREDENTIALS = "EXTRA_CLEAR_CREDENTIALS"
+ private const val EXTRA_ARGS = "EXTRA_ARGS"
// Special action to clear cache and/or clear credentials
- fun restartApp(activity: Activity, clearCache: Boolean = false, clearCredentials: Boolean = false) {
+ fun restartApp(activity: Activity, args: MainActivityArgs) {
val intent = Intent(activity, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- intent.putExtra(EXTRA_CLEAR_CACHE, clearCache)
- intent.putExtra(EXTRA_CLEAR_CREDENTIALS, clearCredentials)
+ intent.putExtra(EXTRA_ARGS, args)
activity.startActivity(intent)
}
}
+ private lateinit var args: MainActivityArgs
+
+ @Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter
@@ -63,42 +83,71 @@ class MainActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val clearCache = intent.getBooleanExtra(EXTRA_CLEAR_CACHE, false)
- val clearCredentials = intent.getBooleanExtra(EXTRA_CLEAR_CREDENTIALS, false)
+ args = parseArgs()
+
+ if (args.clearCredentials || args.isUserLoggedOut) {
+ clearNotifications()
+ }
// Handle some wanted cleanup
- if (clearCache || clearCredentials) {
- doCleanUp(clearCache, clearCredentials)
+ if (args.clearCache || args.clearCredentials) {
+ doCleanUp()
} else {
- start()
+ startNextActivityAndFinish()
}
}
- private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) {
+ private fun clearNotifications() {
+ // Dismiss all notifications
+ notificationDrawerManager.clearAllEvents()
+ notificationDrawerManager.persistInfo()
+ }
+
+ private fun parseArgs(): MainActivityArgs {
+ val argsFromIntent: MainActivityArgs? = intent.getParcelableExtra(EXTRA_ARGS)
+ Timber.w("Starting MainActivity with $argsFromIntent")
+
+ return MainActivityArgs(
+ clearCache = argsFromIntent?.clearCache ?: false,
+ clearCredentials = argsFromIntent?.clearCredentials ?: false,
+ isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
+ isSoftLogout = argsFromIntent?.isSoftLogout ?: false
+ )
+ }
+
+ private fun doCleanUp() {
when {
- clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback {
- override fun onSuccess(data: Unit) {
- Timber.w("SIGN_OUT: success, start app")
- sessionHolder.clearActiveSession()
- doLocalCleanupAndStart()
- }
+ args.clearCredentials -> sessionHolder.getActiveSession().signOut(
+ !args.isUserLoggedOut,
+ object : MatrixCallback {
+ override fun onSuccess(data: Unit) {
+ Timber.w("SIGN_OUT: success, start app")
+ sessionHolder.clearActiveSession()
+ doLocalCleanupAndStart()
+ }
- override fun onFailure(failure: Throwable) {
- displayError(failure, clearCache, clearCredentials)
- }
- })
- clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback {
- override fun onSuccess(data: Unit) {
- doLocalCleanupAndStart()
- }
+ override fun onFailure(failure: Throwable) {
+ displayError(failure)
+ }
+ })
+ args.clearCache -> sessionHolder.getActiveSession().clearCache(
+ object : MatrixCallback {
+ override fun onSuccess(data: Unit) {
+ doLocalCleanupAndStart()
+ }
- override fun onFailure(failure: Throwable) {
- displayError(failure, clearCache, clearCredentials)
- }
- })
+ override fun onFailure(failure: Throwable) {
+ displayError(failure)
+ }
+ })
}
}
+ override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
+ // No op here
+ Timber.w("Ignoring invalid token global error")
+ }
+
private fun doLocalCleanupAndStart() {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
@@ -112,24 +161,43 @@ class MainActivity : VectorBaseActivity() {
}
}
- start()
+ startNextActivityAndFinish()
}
- private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) {
+ private fun displayError(failure: Throwable) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
- .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) }
- .setNegativeButton(R.string.cancel) { _, _ -> start() }
+ .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
+ .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
.setCancelable(false)
.show()
}
- private fun start() {
- val intent = if (sessionHolder.hasActiveSession()) {
- HomeActivity.newIntent(this)
- } else {
- LoginActivity.newIntent(this, null)
+ private fun startNextActivityAndFinish() {
+ val intent = when {
+ args.clearCredentials
+ && !args.isUserLoggedOut ->
+ // User has explicitly asked to log out
+ LoginActivity.newIntent(this, null)
+ args.isSoftLogout ->
+ // The homeserver has invalidated the token, with a soft logout
+ SoftLogoutActivity.newIntent(this)
+ args.isUserLoggedOut ->
+ // the homeserver has invalidated the token (password changed, device deleted, other security reason
+ SignedOutActivity.newIntent(this)
+ sessionHolder.hasActiveSession() ->
+ // We have a session.
+ // Check it can be opened
+ if (sessionHolder.getActiveSession().isOpenable) {
+ HomeActivity.newIntent(this)
+ } else {
+ // The token is still invalid
+ SoftLogoutActivity.newIntent(this)
+ }
+ else ->
+ // First start, or no active session
+ LoginActivity.newIntent(this, null)
}
startActivity(intent)
finish()
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt
index 01b6bdd41a..8f0090001f 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserController.kt
@@ -18,11 +18,12 @@ package im.vector.riotx.features.autocomplete.user
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
-class AutocompleteUserController @Inject constructor(): TypedEpoxyController>() {
+class AutocompleteUserController @Inject constructor() : TypedEpoxyController>() {
var listener: AutocompleteClickListener? = null
@@ -35,9 +36,7 @@ class AutocompleteUserController @Inject constructor(): TypedEpoxyController
autocompleteUserItem {
id(user.userId)
- userId(user.userId)
- name(user.displayName)
- avatarUrl(user.avatarUrl)
+ matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
listener?.onItemClick(user)
diff --git a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt
index b32562d8e6..8581ba8e2c 100644
--- a/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/autocomplete/user/AutocompleteUserItem.kt
@@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@@ -30,15 +31,13 @@ import im.vector.riotx.features.home.AvatarRenderer
abstract class AutocompleteUserItem : VectorEpoxyModel() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
- @EpoxyAttribute var name: String? = null
- @EpoxyAttribute var userId: String = ""
- @EpoxyAttribute var avatarUrl: String? = null
+ @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
- holder.nameView.text = name
- avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
+ holder.nameView.text = matrixItem.getBestName()
+ avatarRenderer.render(matrixItem, holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt
index 88df53d0f3..61f5c5f9fe 100644
--- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/SASVerificationIncomingFragment.kt
@@ -22,6 +22,8 @@ import androidx.lifecycle.Observer
import butterknife.BindView
import butterknife.OnClick
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
+import im.vector.matrix.android.api.util.MatrixItem
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.AvatarRenderer
@@ -57,10 +59,10 @@ class SASVerificationIncomingFragment @Inject constructor(
otherDeviceTextView.text = viewModel.otherDeviceId
viewModel.otherUser?.let {
- avatarRenderer.render(it, avatarImageView)
+ avatarRenderer.render(it.toMatrixItem(), avatarImageView)
} ?: run {
// Fallback to what we know
- avatarRenderer.render(null, viewModel.otherUserId ?: "", viewModel.otherUserId, avatarImageView)
+ avatarRenderer.render(MatrixItem.UserItem(viewModel.otherUserId ?: "", viewModel.otherUserId), avatarImageView)
}
viewModel.transactionState.observe(viewLifecycleOwner, Observer {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
index 9975ee91cd..4e1808a48a 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/AvatarRenderer.kt
@@ -27,10 +27,7 @@ import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.session.content.ContentUrlResolver
-import im.vector.matrix.android.api.session.room.model.RoomSummary
-import im.vector.matrix.android.api.session.user.model.User
-import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
-import im.vector.riotx.R
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest
@@ -45,76 +42,42 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
companion object {
private const val THUMBNAIL_SIZE = 250
-
- private val AVATAR_COLOR_LIST = listOf(
- R.color.riotx_avatar_fill_1,
- R.color.riotx_avatar_fill_2,
- R.color.riotx_avatar_fill_3
- )
}
@UiThread
- fun render(roomSummary: RoomSummary, imageView: ImageView) {
- render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView)
- }
-
- @UiThread
- fun render(user: User, imageView: ImageView) {
- render(imageView.context, GlideApp.with(imageView), user.avatarUrl, user.userId, user.displayName, DrawableImageViewTarget(imageView))
- }
-
- @UiThread
- fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) {
- render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView))
+ fun render(matrixItem: MatrixItem, imageView: ImageView) {
+ render(imageView.context,
+ GlideApp.with(imageView),
+ matrixItem,
+ DrawableImageViewTarget(imageView))
}
@UiThread
fun render(context: Context,
glideRequest: GlideRequests,
- avatarUrl: String?,
- identifier: String,
- name: String?,
+ matrixItem: MatrixItem,
target: Target) {
- val displayName = if (name.isNullOrBlank()) {
- identifier
- } else {
- name
- }
- val placeholder = getPlaceholderDrawable(context, identifier, displayName)
- buildGlideRequest(glideRequest, avatarUrl)
+ val placeholder = getPlaceholderDrawable(context, matrixItem)
+ buildGlideRequest(glideRequest, matrixItem.avatarUrl)
.placeholder(placeholder)
.into(target)
}
@AnyThread
- fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable {
- val avatarColor = ContextCompat.getColor(context, getColorFromUserId(identifier))
- return if (text.isEmpty()) {
- TextDrawable.builder().buildRound("", avatarColor)
- } else {
- val firstLetter = text.firstLetterOfDisplayName()
- TextDrawable.builder()
- .beginConfig()
- .bold()
- .endConfig()
- .buildRound(firstLetter, avatarColor)
+ fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
+ val avatarColor = when (matrixItem) {
+ is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id))
+ else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id))
}
+ return TextDrawable.builder()
+ .beginConfig()
+ .bold()
+ .endConfig()
+ .buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
}
// PRIVATE API *********************************************************************************
-// private fun getAvatarColor(text: String? = null): Int {
-// var colorIndex: Long = 0
-// if (!text.isNullOrEmpty()) {
-// var sum: Long = 0
-// for (i in 0 until text.length) {
-// sum += text[i].toLong()
-// }
-// colorIndex = sum % AVATAR_COLOR_LIST.size
-// }
-// return AVATAR_COLOR_LIST[colorIndex.toInt()]
-// }
-
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest {
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver()
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
index ac8d429cb1..fc0eeaf92c 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailFragment.kt
@@ -27,6 +27,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.platform.ToolbarConfigurable
@@ -74,12 +75,7 @@ class HomeDetailFragment @Inject constructor(
private fun onGroupChange(groupSummary: GroupSummary?) {
groupSummary?.let {
- avatarRenderer.render(
- it.avatarUrl,
- it.groupId,
- it.displayName,
- groupToolbarAvatarImageView
- )
+ avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView)
}
}
@@ -155,7 +151,7 @@ class HomeDetailFragment @Inject constructor(
bottomNavigationView.selectedItemId = when (displayMode) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
- else -> R.id.bottom_action_home
+ else -> R.id.bottom_action_home
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt
index c7c5e4a233..1777fa03c1 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDetailViewState.kt
@@ -34,5 +34,5 @@ data class HomeDetailViewState(
val notificationHighlightPeople: Boolean = false,
val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false,
- val syncState: SyncState = SyncState.IDLE
+ val syncState: SyncState = SyncState.Idle
) : MvRxState
diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
index 422b59671e..6ff836e8c8 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/HomeDrawerFragment.kt
@@ -19,6 +19,7 @@ package im.vector.riotx.features.home
import android.os.Bundle
import android.view.View
import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.extensions.observeK
import im.vector.riotx.core.extensions.replaceChildFragment
@@ -42,7 +43,7 @@ class HomeDrawerFragment @Inject constructor(
session.liveUser(session.myUserId).observeK(this) { optionalUser ->
val user = optionalUser?.getOrNull()
if (user != null) {
- avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView)
+ avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName
homeDrawerUserIdView.text = user.userId
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/RoomColor.kt b/vector/src/main/java/im/vector/riotx/features/home/RoomColor.kt
new file mode 100644
index 0000000000..0b3fd5396f
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/RoomColor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.home
+
+import androidx.annotation.ColorRes
+import im.vector.riotx.R
+
+@ColorRes
+fun getColorFromRoomId(roomId: String?): Int {
+ return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) {
+ 1 -> R.color.riotx_avatar_fill_2
+ 2 -> R.color.riotx_avatar_fill_3
+ else -> R.color.riotx_avatar_fill_1
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt b/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt
index a88299cc25..d34ca6506a 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/UserColor.kt
@@ -22,28 +22,18 @@ import kotlin.math.abs
@ColorRes
fun getColorFromUserId(userId: String?): Int {
- if (userId.isNullOrBlank()) {
- return R.color.riotx_username_1
- }
-
var hash = 0
- var i = 0
- var chr: Char
- while (i < userId.length) {
- chr = userId[i]
- hash = (hash shl 5) - hash + chr.toInt()
- i++
- }
+ userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() }
- return when (abs(hash) % 8 + 1) {
- 1 -> R.color.riotx_username_1
- 2 -> R.color.riotx_username_2
- 3 -> R.color.riotx_username_3
- 4 -> R.color.riotx_username_4
- 5 -> R.color.riotx_username_5
- 6 -> R.color.riotx_username_6
- 7 -> R.color.riotx_username_7
- else -> R.color.riotx_username_8
+ return when (abs(hash) % 8) {
+ 1 -> R.color.riotx_username_2
+ 2 -> R.color.riotx_username_3
+ 3 -> R.color.riotx_username_4
+ 4 -> R.color.riotx_username_5
+ 5 -> R.color.riotx_username_6
+ 6 -> R.color.riotx_username_7
+ 7 -> R.color.riotx_username_8
+ else -> R.color.riotx_username_1
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt
index 0ff4c5baf8..401d4445fe 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/CreateDirectRoomUserItem.kt
@@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.amulyakhare.textdrawable.TextDrawable
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@@ -34,22 +35,20 @@ import im.vector.riotx.features.home.AvatarRenderer
abstract class CreateDirectRoomUserItem : VectorEpoxyModel() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
- @EpoxyAttribute var name: String? = null
- @EpoxyAttribute var userId: String = ""
- @EpoxyAttribute var avatarUrl: String? = null
+ @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var clickListener: View.OnClickListener? = null
@EpoxyAttribute var selected: Boolean = false
override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener)
// If name is empty, use userId as name and force it being centered
- if (name.isNullOrEmpty()) {
+ if (matrixItem.displayName.isNullOrEmpty()) {
holder.userIdView.visibility = View.GONE
- holder.nameView.text = userId
+ holder.nameView.text = matrixItem.id
} else {
holder.userIdView.visibility = View.VISIBLE
- holder.nameView.text = name
- holder.userIdView.text = userId
+ holder.nameView.text = matrixItem.displayName
+ holder.userIdView.text = matrixItem.id
}
renderSelection(holder, selected)
}
@@ -62,7 +61,7 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel
- users.sortedBy { it.displayName.firstLetterOfDisplayName() }
+ users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
}
}
stream.toAsync {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt
index 265a38b2c9..8d2b3928be 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/DirectoryUsersController.kt
@@ -19,9 +19,13 @@
package im.vector.riotx.features.home.createdirect
import com.airbnb.epoxy.EpoxyController
-import com.airbnb.mvrx.*
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
@@ -94,9 +98,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
createDirectRoomUserItem {
id(user.userId)
selected(isSelected)
- userId(user.userId)
- name(user.displayName)
- avatarUrl(user.avatarUrl)
+ matrixItem(user.toMatrixItem())
avatarRenderer(avatarRenderer)
clickListener { _ ->
callback?.onItemClick(user)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt
index 3d1ee84254..8270683975 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/createdirect/KnownUsersController.kt
@@ -23,7 +23,7 @@ import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
-import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.EmptyItem_
import im.vector.riotx.core.epoxy.loadingItem
@@ -68,9 +68,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
CreateDirectRoomUserItem_()
.id(item.userId)
.selected(isSelected)
- .userId(item.userId)
- .name(item.displayName)
- .avatarUrl(item.avatarUrl)
+ .matrixItem(item.toMatrixItem())
.avatarRenderer(avatarRenderer)
.clickListener { _ ->
callback?.onItemClick(item)
@@ -87,8 +85,8 @@ class KnownUsersController @Inject constructor(private val session: Session,
var lastFirstLetter: String? = null
for (model in models) {
if (model is CreateDirectRoomUserItem) {
- if (model.userId == session.myUserId) continue
- val currentFirstLetter = model.name.firstLetterOfDisplayName()
+ if (model.matrixItem.id == session.myUserId) continue
+ val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName()
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter
diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
index d9a38d5d9b..bbeda127fc 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupListViewModel.kt
@@ -36,7 +36,7 @@ import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
-const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID"
+const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID"
class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState,
private val selectedGroupStore: SelectedGroupDataSource,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt
index 7c3cfd2a94..95054d1689 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryController.kt
@@ -18,6 +18,7 @@ package im.vector.riotx.features.home.group
import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
@@ -49,10 +50,8 @@ class GroupSummaryController @Inject constructor(private val avatarRenderer: Ava
groupSummaryItem {
avatarRenderer(avatarRenderer)
id(groupSummary.groupId)
- groupId(groupSummary.groupId)
- groupName(groupSummary.displayName)
+ matrixItem(groupSummary.toMatrixItem())
selected(isSelected)
- avatarUrl(groupSummary.avatarUrl)
listener { callback?.onGroupSelected(groupSummary) }
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt
index 30c1852f1d..61c589cc00 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/group/GroupSummaryItem.kt
@@ -20,6 +20,7 @@ import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@@ -30,18 +31,16 @@ import im.vector.riotx.features.home.AvatarRenderer
abstract class GroupSummaryItem : VectorEpoxyModel() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
- @EpoxyAttribute lateinit var groupName: CharSequence
- @EpoxyAttribute lateinit var groupId: String
- @EpoxyAttribute var avatarUrl: String? = null
+ @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
- holder.groupNameView.text = groupName
+ holder.groupNameView.text = matrixItem.displayName
holder.rootView.isChecked = selected
- avatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView)
+ avatarRenderer.render(matrixItem, holder.avatarImageView)
}
class Holder : VectorEpoxyHolder() {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt
index 3e400b37ea..bfc91bf5a1 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsController.kt
@@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.breadcrumbs
import android.view.View
import com.airbnb.epoxy.EpoxyController
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject
@@ -52,9 +53,7 @@ class BreadcrumbsController @Inject constructor(
breadcrumbsItem {
id(it.roomId)
avatarRenderer(avatarRenderer)
- roomId(it.roomId)
- roomName(it.displayName)
- avatarUrl(it.avatarUrl)
+ matrixItem(it.toMatrixItem())
unreadNotificationCount(it.notificationCount)
showHighlighted(it.highlightCount > 0)
hasUnreadMessage(it.hasUnreadMessages)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt
index b8e2cf7987..5407c73f35 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsFragment.kt
@@ -48,6 +48,7 @@ class BreadcrumbsFragment @Inject constructor(
override fun onDestroyView() {
breadcrumbsRecyclerView.cleanup()
+ breadcrumbsController.listener = null
super.onDestroyView()
}
@@ -56,6 +57,7 @@ class BreadcrumbsFragment @Inject constructor(
breadcrumbsController.listener = this
}
+ // TODO Use invalidate() ?
private fun renderState(state: BreadcrumbsViewState) {
breadcrumbsController.update(state)
}
@@ -65,4 +67,8 @@ class BreadcrumbsFragment @Inject constructor(
override fun onBreadcrumbClicked(roomId: String) {
sharedActionViewModel.post(RoomDetailSharedAction.SwitchToRoom(roomId))
}
+
+ fun scrollToTop() {
+ breadcrumbsRecyclerView.scrollToPosition(0)
+ }
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt
index 074c35af00..6d18a85b75 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/breadcrumbs/BreadcrumbsItem.kt
@@ -22,6 +22,7 @@ import android.widget.ImageView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
@@ -32,9 +33,7 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
abstract class BreadcrumbsItem : VectorEpoxyModel() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
- @EpoxyAttribute lateinit var roomId: String
- @EpoxyAttribute lateinit var roomName: CharSequence
- @EpoxyAttribute var avatarUrl: String? = null
+ @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var hasUnreadMessage: Boolean = false
@@ -45,7 +44,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel() {
super.bind(holder)
holder.rootView.setOnClickListener(itemClickListener)
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
- avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
+ avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.draftIndentIndicator.isVisible = hasDraft
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
index c1743ae3fc..5d00b09204 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt
@@ -63,4 +63,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ClearSendQueue : RoomDetailAction()
object ResendAll : RoomDetailAction()
+
+ data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
+ data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction()
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
index 431c9e6395..14e9061c36 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActivity.kt
@@ -86,9 +86,19 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
hideKeyboard()
+
+ if (!drawerLayout.isDrawerOpen(GravityCompat.START) && newState == DrawerLayout.STATE_DRAGGING) {
+ // User is starting to open the drawer, scroll the list to op
+ scrollBreadcrumbsToTop()
+ }
}
}
+ private fun scrollBreadcrumbsToTop() {
+ supportFragmentManager.fragments.filterIsInstance()
+ .forEach { it.scrollToTop() }
+ }
+
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index 80f54a9c1f..9af0e946a0 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -66,10 +66,11 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.user.model.User
+import im.vector.matrix.android.api.util.MatrixItem
+import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
-import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
@@ -141,7 +142,6 @@ class RoomDetailFragment @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
val textComposerViewModelFactory: TextComposerViewModel.Factory,
- private val errorFormatter: ErrorFormatter,
private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences
) :
@@ -410,9 +410,7 @@ class RoomDetailFragment @Inject constructor(
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
avatarRenderer.render(
- event.senderAvatar,
- event.root.senderId ?: "",
- event.getDisambiguatedDisplayName(),
+ MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar
)
composerLayout.expand {
@@ -601,20 +599,19 @@ class RoomDetailFragment @Inject constructor(
}
// Replace the word by its completion
- val displayName = item.displayName ?: item.userId
+ val matrixItem = item.toMatrixItem()
+ val displayName = matrixItem.getBestName()
// with a trailing space
editable.replace(startIndex, endIndex, "$displayName ")
// Add the span
- val user = session.getUser(item.userId)
val span = PillImageSpan(
glideRequests,
avatarRenderer,
requireContext(),
- item.userId,
- user?.displayName ?: item.userId,
- user?.avatarUrl)
+ matrixItem
+ )
span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -686,7 +683,7 @@ class RoomDetailFragment @Inject constructor(
inviteView.visibility = View.GONE
val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
- avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
+ avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView)
} else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE)
@@ -713,7 +710,7 @@ class RoomDetailFragment @Inject constructor(
activity?.finish()
} else {
roomToolbarTitleView.text = it.displayName
- avatarRenderer.render(it, roomToolbarAvatarImageView)
+ avatarRenderer.render(it.toMatrixItem(), roomToolbarAvatarImageView)
roomToolbarSubtitleView.setTextOrHide(it.topic)
}
jumpToBottomView.count = it.notificationCount
@@ -1024,6 +1021,10 @@ class RoomDetailFragment @Inject constructor(
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
}
+ override fun onTimelineItemAction(itemAction: RoomDetailAction) {
+ roomDetailViewModel.handle(itemAction)
+ }
+
override fun onRoomCreateLinkClicked(url: String) {
permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor {
override fun navToRoom(roomId: String, eventId: String?): Boolean {
@@ -1197,9 +1198,8 @@ class RoomDetailFragment @Inject constructor(
glideRequests,
avatarRenderer,
requireContext(),
- userId,
- displayName,
- roomMember?.avatarUrl)
+ MatrixItem.UserItem(userId, displayName, roomMember?.avatarUrl)
+ )
.also { it.bind(composerLayout.composerEditText) },
0,
displayName.length,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
index c1d3f4ce4a..3ce27be63a 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt
@@ -48,6 +48,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
+import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
@@ -177,6 +178,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
+ is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
+ is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
}
}
@@ -786,6 +789,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
})
}
+ private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
+ session.getSasVerificationService().beginKeyVerificationInDMs(
+ KeyVerificationStart.VERIF_METHOD_SAS,
+ action.transactionId,
+ room.roomId,
+ action.otherUserId,
+ action.otherdDeviceId,
+ null
+ )
+ }
+
+ private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
+ Timber.e("TODO implement $action")
+ }
+
private fun observeSyncState() {
session.rx()
.liveSyncState()
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
index a0be8fc9dc..b2ad29668e 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt
@@ -58,7 +58,7 @@ data class RoomDetailViewState(
val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async = Uninitialized,
- val syncState: SyncState = SyncState.IDLE,
+ val syncState: SyncState = SyncState.Idle,
val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt
index 2b7d64a80e..6bc93f28dc 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt
@@ -22,6 +22,7 @@ import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.features.home.AvatarRenderer
@@ -29,15 +30,13 @@ import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_display_read_receipt)
abstract class DisplayReadReceiptItem : EpoxyModelWithHolder() {
- @EpoxyAttribute var name: String? = null
- @EpoxyAttribute var userId: String = ""
- @EpoxyAttribute var avatarUrl: String? = null
+ @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var timestamp: CharSequence? = null
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
override fun bind(holder: Holder) {
- avatarRenderer.render(avatarUrl, userId, name, holder.avatarView)
- holder.displayNameView.text = name ?: userId
+ avatarRenderer.render(matrixItem, holder.avatarView)
+ holder.displayNameView.text = matrixItem.getBestName()
timestamp?.let {
holder.timestampView.text = it
holder.timestampView.isVisible = true
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt
index 6affa582bc..3ec60217a0 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptsController.kt
@@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
+import im.vector.riotx.features.home.room.detail.timeline.item.toMatrixItem
import javax.inject.Inject
/**
@@ -36,9 +37,7 @@ class DisplayReadReceiptsController @Inject constructor(private val dateFormatte
val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp)
DisplayReadReceiptItem_()
.id(it.userId)
- .userId(it.userId)
- .avatarUrl(it.avatarUrl)
- .name(it.displayName)
+ .matrixItem(it.toMatrixItem())
.avatarRenderer(avatarRender)
.timestamp(timestamp)
.addIf(session.myUserId != it.userId, this)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
index 576b9fa0ba..fe1a681480 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
@@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.extensions.localDateTime
+import im.vector.riotx.features.home.room.detail.RoomDetailAction
import im.vector.riotx.features.home.room.detail.RoomDetailViewState
import im.vector.riotx.features.home.room.detail.UnreadState
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
@@ -62,6 +63,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEditedDecorationClicked(informationData: MessageInformationData)
+
+ // TODO move all callbacks to this?
+ fun onTimelineItemAction(itemAction: RoomDetailAction)
}
interface ReactionPillCallback {
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
index efbfd3434c..939564e780 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt
@@ -44,9 +44,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
bottomSheetMessagePreviewItem {
id("preview")
avatarRenderer(avatarRenderer)
- avatarUrl(state.informationData.avatarUrl ?: "")
- senderId(state.informationData.senderId)
- senderName(state.senderName())
+ matrixItem(state.informationData.matrixItem)
movementMethod(createLinkMovementMethod(listener))
body(body.linkify(listener))
time(state.time())
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index 102412948b..e8047c2b06 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -23,10 +23,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
-import im.vector.matrix.android.api.session.room.model.message.MessageContent
-import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
-import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
-import im.vector.matrix.android.api.session.room.model.message.MessageType
+import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@@ -172,6 +169,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body)
+ } else if (messageContent is MessageVerificationRequestContent) {
+ stringProvider.getString(R.string.verification_request)
} else {
messageContent?.body
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
index 3f234fcd3e..29b01120d1 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt
@@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail.timeline.factory
import android.view.View
+import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
@@ -34,7 +35,8 @@ import javax.inject.Inject
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
- private val avatarSizeProvider: AvatarSizeProvider) {
+ private val avatarSizeProvider: AvatarSizeProvider,
+ private val session: Session) {
fun create(event: TimelineEvent,
highlight: Boolean,
@@ -46,7 +48,8 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
sendState = event.root.sendState,
avatarUrl = event.senderAvatar,
memberName = event.getDisambiguatedDisplayName(),
- showInformation = false
+ showInformation = false,
+ sentByMe = event.root.senderId == session.myUserId
)
val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 9c96f17022..93d5ab3789 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -24,6 +24,7 @@ import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.view.View
import dagger.Lazy
+import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.*
@@ -64,7 +65,8 @@ class MessageItemFactory @Inject constructor(
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory,
- private val avatarSizeProvider: AvatarSizeProvider) {
+ private val avatarSizeProvider: AvatarSizeProvider,
+ private val session: Session) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
@@ -97,14 +99,15 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent()
// val ev = all.toModel()
return when (messageContent) {
- is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
- is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
- is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
- else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
+ is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
+ is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
+ is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
+ else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
}
}
@@ -128,6 +131,51 @@ class MessageItemFactory @Inject constructor(
}))
}
+ private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent,
+ @Suppress("UNUSED_PARAMETER")
+ informationData: MessageInformationData,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?,
+ attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
+ // If this request is not sent by me or sent to me, we should ignore it in timeline
+ val myUserId = session.myUserId
+ if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
+ return null
+ }
+
+ val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
+ val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName
+ else informationData.memberName
+ return VerificationRequestItem_()
+ .attributes(
+ VerificationRequestItem.Attributes(
+ otherUserId,
+ otherUserName.toString(),
+ messageContent.fromDevice,
+ informationData.eventId,
+ informationData,
+ attributes.avatarRenderer,
+ attributes.colorProvider,
+ attributes.itemLongClickListener,
+ attributes.itemClickListener,
+ attributes.reactionPillCallback,
+ attributes.readReceiptsCallback,
+ attributes.emojiTypeFace
+ )
+ )
+ .callback(callback)
+// .izLocalFile(messageContent.getFileUrl().isLocalFile())
+// .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
+ .highlighted(highlight)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+// .filename(messageContent.body)
+// .iconRes(R.drawable.filetype_audio)
+// .clickListener(
+// DebouncedClickListener(View.OnClickListener {
+// callback?.onAudioMessageClicked(messageContent)
+// }))
+ }
+
private fun buildFileMessageItem(messageContent: MessageFileContent,
informationData: MessageInformationData,
highlight: Boolean,
@@ -193,7 +241,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body,
- url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
+ url = messageContent.videoInfo?.thumbnailFile?.url
+ ?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index a705576234..6d1ce2cf2a 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -28,7 +28,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val encryptedItemFactory: EncryptedItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory,
- private val roomCreateItemFactory: RoomCreateItemFactory) {
+ private val roomCreateItemFactory: RoomCreateItemFactory,
+ private val verificationConclusionItemFactory: VerificationItemFactory) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
@@ -66,13 +67,15 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
}
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
- EventType.KEY_VERIFICATION_DONE,
- EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC -> {
// These events are filtered from timeline in normal case
// Only visible in developer mode
- defaultItemFactory.create(event, highlight, callback)
+ noticeItemFactory.create(event, highlight, callback)
+ }
+ EventType.KEY_VERIFICATION_CANCEL,
+ EventType.KEY_VERIFICATION_DONE -> {
+ verificationConclusionItemFactory.create(event, highlight, callback)
}
// Unhandled event types (yet)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
new file mode 100644
index 0000000000..75305518d2
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotx.features.home.room.detail.timeline.factory
+
+import im.vector.matrix.android.api.session.Session
+import im.vector.matrix.android.api.session.crypto.sas.CancelCode
+import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.events.model.RelationType
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
+import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
+import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.internal.session.room.VerificationState
+import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import im.vector.riotx.core.resources.ColorProvider
+import im.vector.riotx.core.resources.UserPreferencesProvider
+import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
+import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
+import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
+import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem
+import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_
+import javax.inject.Inject
+
+/**
+ * Can creates verification conclusion items
+ * Notice that not all KEY_VERIFICATION_DONE will be displayed in timeline,
+ * several checks are made to see if this conclusion is attached to a known request
+ */
+class VerificationItemFactory @Inject constructor(
+ private val colorProvider: ColorProvider,
+ private val messageInformationDataFactory: MessageInformationDataFactory,
+ private val messageItemAttributesFactory: MessageItemAttributesFactory,
+ private val avatarSizeProvider: AvatarSizeProvider,
+ private val noticeItemFactory: NoticeItemFactory,
+ private val userPreferencesProvider: UserPreferencesProvider,
+ private val session: Session
+) {
+
+ fun create(event: TimelineEvent,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?
+ ): VectorEpoxyModel<*>? {
+ if (event.root.eventId == null) return null
+
+ val relContent: MessageRelationContent = event.root.content.toModel()
+ ?: event.root.getClearContent().toModel()
+ ?: return ignoredConclusion(event, highlight, callback)
+
+ if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback)
+ val refEventId = relContent.relatesTo?.eventId
+ ?: return ignoredConclusion(event, highlight, callback)
+
+ // If we cannot find the referenced request we do not display the done event
+ val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId)
+ ?: return ignoredConclusion(event, highlight, callback)
+
+ // If it's not a request ignore this event
+ if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback)
+
+ val referenceInformationData = messageInformationDataFactory.create(refEvent, null)
+
+ val informationData = messageInformationDataFactory.create(event, null)
+ val attributes = messageItemAttributesFactory.create(null, informationData, callback)
+
+ when (event.root.getClearType()) {
+ EventType.KEY_VERIFICATION_CANCEL -> {
+ // Is the request referenced is actually really cancelled?
+ val cancelContent = event.root.getClearContent().toModel()
+ ?: return ignoredConclusion(event, highlight, callback)
+
+ when (safeValueOf(cancelContent.code)) {
+ CancelCode.MismatchedCommitment,
+ CancelCode.MismatchedKeys,
+ CancelCode.MismatchedSas -> {
+ // We should display these bad conclusions
+ return VerificationRequestConclusionItem_()
+ .attributes(
+ VerificationRequestConclusionItem.Attributes(
+ toUserId = informationData.senderId,
+ toUserName = informationData.memberName.toString(),
+ isPositive = false,
+ informationData = informationData,
+ avatarRenderer = attributes.avatarRenderer,
+ colorProvider = colorProvider,
+ emojiTypeFace = attributes.emojiTypeFace,
+ itemClickListener = attributes.itemClickListener,
+ itemLongClickListener = attributes.itemLongClickListener,
+ reactionPillCallback = attributes.reactionPillCallback,
+ readReceiptsCallback = attributes.readReceiptsCallback
+ )
+ )
+ .highlighted(highlight)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ }
+ else -> ignoredConclusion(event, highlight, callback)
+ }
+ }
+ EventType.KEY_VERIFICATION_DONE -> {
+ // Is the request referenced is actually really completed?
+ if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE)
+ return ignoredConclusion(event, highlight, callback)
+
+ // We only tale the one sent by me
+ if (informationData.sentByMe) {
+ // We only display the done sent by the other user, the done send by me is ignored
+ return ignoredConclusion(event, highlight, callback)
+ }
+ return VerificationRequestConclusionItem_()
+ .attributes(
+ VerificationRequestConclusionItem.Attributes(
+ toUserId = informationData.senderId,
+ toUserName = informationData.memberName.toString(),
+ isPositive = true,
+ informationData = informationData,
+ avatarRenderer = attributes.avatarRenderer,
+ colorProvider = colorProvider,
+ emojiTypeFace = attributes.emojiTypeFace,
+ itemClickListener = attributes.itemClickListener,
+ itemLongClickListener = attributes.itemLongClickListener,
+ reactionPillCallback = attributes.reactionPillCallback,
+ readReceiptsCallback = attributes.readReceiptsCallback
+ )
+ )
+ .highlighted(highlight)
+ .leftGuideline(avatarSizeProvider.leftGuideline)
+ }
+ }
+ return null
+ }
+
+ private fun ignoredConclusion(event: TimelineEvent,
+ highlight: Boolean,
+ callback: TimelineEventController.Callback?
+ ): VectorEpoxyModel<*>? {
+ if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
+ return null
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
new file mode 100644
index 0000000000..ed6bc9df62
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.home.room.detail.timeline.format
+
+import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.room.model.message.MessageType
+import im.vector.matrix.android.api.session.room.model.message.isReply
+import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
+import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
+import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
+import im.vector.riotx.R
+import im.vector.riotx.core.resources.ColorProvider
+import im.vector.riotx.core.resources.StringProvider
+import me.gujun.android.span.span
+import javax.inject.Inject
+
+class DisplayableEventFormatter @Inject constructor(
+// private val sessionHolder: ActiveSessionHolder,
+ private val stringProvider: StringProvider,
+ private val colorProvider: ColorProvider,
+ private val noticeEventFormatter: NoticeEventFormatter
+) {
+
+ fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
+ if (timelineEvent.root.isEncrypted()
+ && timelineEvent.root.mxDecryptionResult == null) {
+ return stringProvider.getString(R.string.encrypted_message)
+ }
+
+ val senderName = timelineEvent.getDisambiguatedDisplayName()
+
+ when (timelineEvent.root.getClearType()) {
+ EventType.MESSAGE -> {
+ timelineEvent.getLastMessageContent()?.let { messageContent ->
+ when (messageContent.type) {
+ MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
+ return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor)
+ }
+ MessageType.MSGTYPE_IMAGE -> {
+ return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
+ }
+ MessageType.MSGTYPE_AUDIO -> {
+ return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
+ }
+ MessageType.MSGTYPE_VIDEO -> {
+ return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
+ }
+ MessageType.MSGTYPE_FILE -> {
+ return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
+ }
+ MessageType.MSGTYPE_TEXT -> {
+ if (messageContent.isReply()) {
+ // Skip reply prefix, and show important
+ // TODO add a reply image span ?
+ return simpleFormat(senderName, timelineEvent.getTextEditableContent()
+ ?: messageContent.body, appendAuthor)
+ } else {
+ return simpleFormat(senderName, messageContent.body, appendAuthor)
+ }
+ }
+ else -> {
+ return simpleFormat(senderName, messageContent.body, appendAuthor)
+ }
+ }
+ }
+ }
+ else -> {
+ return span {
+ text = noticeEventFormatter.format(timelineEvent) ?: ""
+ textStyle = "italic"
+ }
+ }
+ }
+
+ return span { }
+ }
+
+ private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence {
+ return if (appendAuthor) {
+ span {
+ text = senderName
+ textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)
+ }
+ .append(": ")
+ .append(body)
+ } else {
+ body
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index 75100e6c03..f5253a9a28 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -44,6 +44,12 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.MESSAGE,
EventType.REACTION,
+ EventType.KEY_VERIFICATION_START,
+ EventType.KEY_VERIFICATION_CANCEL,
+ EventType.KEY_VERIFICATION_ACCEPT,
+ EventType.KEY_VERIFICATION_MAC,
+ EventType.KEY_VERIFICATION_DONE,
+ EventType.KEY_VERIFICATION_KEY,
EventType.REDACTION -> formatDebug(timelineEvent.root)
else -> {
Timber.v("Type $type not handled by this formatter")
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index 784a180d00..a46d0bb9b9 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -20,15 +20,19 @@ package im.vector.riotx.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
+import im.vector.matrix.android.api.session.events.model.toModel
+import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
+import im.vector.matrix.android.internal.session.room.VerificationState
+import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.home.getColorFromUserId
-import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
+import im.vector.riotx.features.home.room.detail.timeline.item.ReferencesInfoData
import me.gujun.android.span.span
import javax.inject.Inject
@@ -60,7 +64,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
val avatarUrl = event.senderAvatar
val memberName = event.getDisambiguatedDisplayName()
val formattedMemberName = span(memberName) {
- textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
+ textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId))
}
return MessageInformationData(
@@ -86,7 +90,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
- .toList()
+ .toList(),
+ referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
+ val stateStr = referencesAggregatedSummary.content.toModel()?.verificationSummary
+ ReferencesInfoData(
+ VerificationState.values().firstOrNull { stateStr == it.name }
+ ?: VerificationState.REQUEST
+ )
+ },
+ sentByMe = event.root.senderId == session.myUserId
)
}
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
index 033ff68433..343b5ec74c 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt
@@ -37,7 +37,9 @@ object TimelineDisplayableEvents {
EventType.STICKER,
EventType.STATE_ROOM_CREATE,
EventType.STATE_ROOM_TOMBSTONE,
- EventType.STATE_ROOM_JOIN_RULES
+ EventType.STATE_ROOM_JOIN_RULES,
+ EventType.KEY_VERIFICATION_DONE,
+ EventType.KEY_VERIFICATION_CANCEL
)
val DEBUG_DISPLAYABLE_TYPES = DISPLAYABLE_TYPES + listOf(
@@ -45,8 +47,6 @@ object TimelineDisplayableEvents {
EventType.REACTION,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
- EventType.KEY_VERIFICATION_DONE,
- EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_KEY
)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
new file mode 100644
index 0000000000..6d99bb2650
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.features.home.room.detail.timeline.item
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.annotation.IdRes
+import androidx.core.view.isVisible
+import im.vector.matrix.android.api.session.room.send.SendState
+import im.vector.riotx.R
+import im.vector.riotx.core.resources.ColorProvider
+import im.vector.riotx.core.utils.DebouncedClickListener
+import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+import im.vector.riotx.features.reactions.widget.ReactionButton
+import im.vector.riotx.features.ui.getMessageTextColor
+
+/**
+ * Base timeline item with reactions and read receipts.
+ * Manages associated click listeners and send status.
+ * Should not be used as this, use a subclass.
+ */
+abstract class AbsBaseMessageItem : BaseEventItem() {
+
+ abstract val baseAttributes: Attributes
+
+ private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
+ baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts)
+ })
+
+ private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
+ override fun onReacted(reactionButton: ReactionButton) {
+ baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true)
+ }
+
+ override fun onUnReacted(reactionButton: ReactionButton) {
+ baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, false)
+ }
+
+ override fun onLongClick(reactionButton: ReactionButton) {
+ baseAttributes.reactionPillCallback?.onLongClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString)
+ }
+ }
+
+ open fun shouldShowReactionAtBottom(): Boolean {
+ return true
+ }
+
+ override fun getEventIds(): List {
+ return listOf(baseAttributes.informationData.eventId)
+ }
+
+ override fun bind(holder: H) {
+ super.bind(holder)
+ holder.readReceiptsView.render(
+ baseAttributes.informationData.readReceipts,
+ baseAttributes.avatarRenderer,
+ _readReceiptsClickListener
+ )
+
+ val reactions = baseAttributes.informationData.orderedReactionList
+ if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
+ holder.reactionsContainer.isVisible = false
+ } else {
+ holder.reactionsContainer.isVisible = true
+ holder.reactionsContainer.removeAllViews()
+ reactions.take(8).forEach { reaction ->
+ val reactionButton = ReactionButton(holder.view.context)
+ reactionButton.reactedListener = reactionClickListener
+ reactionButton.setTag(R.id.reactionsContainer, reaction.key)
+ reactionButton.reactionString = reaction.key
+ reactionButton.reactionCount = reaction.count
+ reactionButton.setChecked(reaction.addedByMe)
+ reactionButton.isEnabled = reaction.synced
+ holder.reactionsContainer.addView(reactionButton)
+ }
+ holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
+ }
+
+ holder.view.setOnClickListener(baseAttributes.itemClickListener)
+ holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
+ }
+
+ override fun unbind(holder: H) {
+ holder.readReceiptsView.unbind()
+ super.unbind(holder)
+ }
+
+ protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
+ root.isClickable = baseAttributes.informationData.sendState.isSent()
+ val state = if (baseAttributes.informationData.hasPendingEdits) SendState.UNSENT else baseAttributes.informationData.sendState
+ textView?.setTextColor(baseAttributes.colorProvider.getMessageTextColor(state))
+ failureIndicator?.isVisible = baseAttributes.informationData.sendState.hasFailed()
+ }
+
+ abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
+ val reactionsContainer by bind(R.id.reactionsContainer)
+ }
+
+ /**
+ * This class holds all the common attributes for timeline items.
+ */
+ interface Attributes {
+ // val avatarSize: Int,
+ val informationData: MessageInformationData
+ val avatarRenderer: AvatarRenderer
+ val colorProvider: ColorProvider
+ val itemLongClickListener: View.OnLongClickListener?
+ val itemClickListener: View.OnClickListener?
+ // val memberClickListener: View.OnClickListener?
+ val reactionPillCallback: TimelineEventController.ReactionPillCallback?
+ // val avatarCallback: TimelineEventController.AvatarCallback?
+ val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
+// val emojiTypeFace: Typeface?
+ }
+
+// data class AbsAttributes(
+// override val informationData: MessageInformationData,
+// override val avatarRenderer: AvatarRenderer,
+// override val colorProvider: ColorProvider,
+// override val itemLongClickListener: View.OnLongClickListener? = null,
+// override val itemClickListener: View.OnClickListener? = null,
+// override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
+// override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
+// ) : Attributes
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
index 713b60d4d8..ae69164951 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -18,22 +18,24 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.graphics.Typeface
import android.view.View
-import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.IdRes
-import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
-import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
-import im.vector.riotx.features.reactions.widget.ReactionButton
-import im.vector.riotx.features.ui.getMessageTextColor
-abstract class AbsMessageItem : BaseEventItem() {
+/**
+ * Base timeline item that adds an optional information bar with the sender avatar, name and time
+ * Adds associated click listeners (on avatar, displayname)
+ */
+abstract class AbsMessageItem : AbsBaseMessageItem() {
+
+ override val baseAttributes: AbsBaseMessageItem.Attributes
+ get() = attributes
@EpoxyAttribute
lateinit var attributes: Attributes
@@ -45,24 +47,6 @@ abstract class AbsMessageItem : BaseEventItem() {
attributes.avatarCallback?.onMemberNameClicked(attributes.informationData)
})
- private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
- attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
- })
-
- var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
- override fun onReacted(reactionButton: ReactionButton) {
- attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true)
- }
-
- override fun onUnReacted(reactionButton: ReactionButton) {
- attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, false)
- }
-
- override fun onLongClick(reactionButton: ReactionButton) {
- attributes.reactionPillCallback?.onLongClickOnReactionPill(attributes.informationData, reactionButton.reactionString)
- }
- }
-
override fun bind(holder: H) {
super.bind(holder)
if (attributes.informationData.showInformation) {
@@ -77,12 +61,7 @@ abstract class AbsMessageItem : BaseEventItem() {
holder.timeView.visibility = View.VISIBLE
holder.timeView.text = attributes.informationData.time
holder.memberNameView.text = attributes.informationData.memberName
- attributes.avatarRenderer.render(
- attributes.informationData.avatarUrl,
- attributes.informationData.senderId,
- attributes.informationData.memberName?.toString(),
- holder.avatarImageView
- )
+ attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
} else {
@@ -94,60 +73,12 @@ abstract class AbsMessageItem : BaseEventItem() {
holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnLongClickListener(null)
}
- holder.view.setOnClickListener(attributes.itemClickListener)
- holder.view.setOnLongClickListener(attributes.itemLongClickListener)
-
- holder.readReceiptsView.render(
- attributes.informationData.readReceipts,
- attributes.avatarRenderer,
- _readReceiptsClickListener
- )
-
- val reactions = attributes.informationData.orderedReactionList
- if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
- holder.reactionsContainer.isVisible = false
- } else {
- holder.reactionsContainer.isVisible = true
- holder.reactionsContainer.removeAllViews()
- reactions.take(8).forEach { reaction ->
- val reactionButton = ReactionButton(holder.view.context)
- reactionButton.reactedListener = reactionClickListener
- reactionButton.setTag(R.id.reactionsContainer, reaction.key)
- reactionButton.reactionString = reaction.key
- reactionButton.reactionCount = reaction.count
- reactionButton.setChecked(reaction.addedByMe)
- reactionButton.isEnabled = reaction.synced
- holder.reactionsContainer.addView(reactionButton)
- }
- holder.reactionsContainer.setOnLongClickListener(attributes.itemLongClickListener)
- }
}
- override fun unbind(holder: H) {
- holder.readReceiptsView.unbind()
- super.unbind(holder)
- }
-
- open fun shouldShowReactionAtBottom(): Boolean {
- return true
- }
-
- override fun getEventIds(): List {
- return listOf(attributes.informationData.eventId)
- }
-
- protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
- root.isClickable = attributes.informationData.sendState.isSent()
- val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState
- textView?.setTextColor(attributes.colorProvider.getMessageTextColor(state))
- failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed()
- }
-
- abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
+ abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
val avatarImageView by bind(R.id.messageAvatarImageView)
val memberNameView by bind(R.id.messageMemberNameView)
val timeView by bind(R.id.messageTimeView)
- val reactionsContainer by bind(R.id.reactionsContainer)
}
/**
@@ -155,15 +86,15 @@ abstract class AbsMessageItem : BaseEventItem() {
*/
data class Attributes(
val avatarSize: Int,
- val informationData: MessageInformationData,
- val avatarRenderer: AvatarRenderer,
- val colorProvider: ColorProvider,
- val itemLongClickListener: View.OnLongClickListener? = null,
- val itemClickListener: View.OnClickListener? = null,
+ override val informationData: MessageInformationData,
+ override val avatarRenderer: AvatarRenderer,
+ override val colorProvider: ColorProvider,
+ override val itemLongClickListener: View.OnLongClickListener? = null,
+ override val itemClickListener: View.OnClickListener? = null,
val memberClickListener: View.OnClickListener? = null,
- val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
+ override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
val avatarCallback: TimelineEventController.AvatarCallback? = null,
- val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
+ override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null
- )
+ ) : AbsBaseMessageItem.Attributes
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt
index a2a3c9ad3b..93f7dc271d 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt
@@ -24,6 +24,7 @@ import androidx.core.view.children
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@@ -54,7 +55,7 @@ abstract class MergedHeaderItem : BaseEventItem() {
val data = distinctMergeData.getOrNull(index)
if (data != null && view is ImageView) {
view.visibility = View.VISIBLE
- attributes.avatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view)
+ attributes.avatarRenderer.render(data.toMatrixItem(), view)
} else {
view.visibility = View.GONE
}
@@ -87,6 +88,8 @@ abstract class MergedHeaderItem : BaseEventItem() {
val avatarUrl: String?
)
+ fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)
+
data class Attributes(
val isCollapsed: Boolean,
val mergeData: List,
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
index 2dd581ce6f..835a789107 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt
@@ -18,6 +18,8 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.os.Parcelable
import im.vector.matrix.android.api.session.room.send.SendState
+import im.vector.matrix.android.api.util.MatrixItem
+import im.vector.matrix.android.internal.session.room.VerificationState
import kotlinx.android.parcel.Parcelize
@Parcelize
@@ -33,7 +35,18 @@ data class MessageInformationData(
val orderedReactionList: List? = null,
val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false,
- val readReceipts: List = emptyList()
+ val readReceipts: List = emptyList(),
+ val referencesInfoData: ReferencesInfoData? = null,
+ val sentByMe : Boolean
+) : Parcelable {
+
+ val matrixItem: MatrixItem
+ get() = MatrixItem.UserItem(senderId, memberName?.toString(), avatarUrl)
+}
+
+@Parcelize
+data class ReferencesInfoData(
+ val verificationStatus: VerificationState
) : Parcelable
@Parcelize
@@ -51,3 +64,5 @@ data class ReadReceiptData(
val displayName: String?,
val timestamp: Long
) : Parcelable
+
+fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt
index 05dedcfa22..189c358b48 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt
@@ -39,13 +39,7 @@ abstract class NoticeItem : BaseEventItem() {
override fun bind(holder: Holder) {
super.bind(holder)
holder.noticeTextView.text = attributes.noticeText
- attributes.avatarRenderer.render(
- attributes.informationData.avatarUrl,
- attributes.informationData.senderId,
- attributes.informationData.memberName?.toString()
- ?: attributes.informationData.senderId,
- holder.avatarImageView
- )
+ attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt
new file mode 100644
index 0000000000..036bf2b036
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotx.features.home.room.detail.timeline.item
+
+import android.annotation.SuppressLint
+import android.graphics.Typeface
+import android.view.View
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.content.ContextCompat
+import androidx.core.view.updateLayoutParams
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.riotx.R
+import im.vector.riotx.core.resources.ColorProvider
+import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+
+@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
+abstract class VerificationRequestConclusionItem : AbsBaseMessageItem() {
+
+ override val baseAttributes: AbsBaseMessageItem.Attributes
+ get() = attributes
+
+ @EpoxyAttribute
+ lateinit var attributes: Attributes
+
+ override fun getViewType() = STUB_ID
+
+ @SuppressLint("SetTextI18n")
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+ holder.endGuideline.updateLayoutParams {
+ this.marginEnd = leftGuideline
+ }
+ val title = if (attributes.isPositive) R.string.sas_verified else R.string.verification_conclusion_warning
+ holder.titleView.text = holder.view.context.getString(title)
+ holder.descriptionView.text = "${attributes.informationData.memberName} (${attributes.informationData.senderId})"
+
+ val startDrawable = if (attributes.isPositive) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
+ holder.titleView.setCompoundDrawablesWithIntrinsicBounds(
+ ContextCompat.getDrawable(holder.view.context, startDrawable),
+ null, null, null
+ )
+
+ renderSendState(holder.view, null, holder.failedToSendIndicator)
+ }
+
+ class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
+ val titleView by bind(R.id.itemVerificationDoneTitleTextView)
+ val descriptionView by bind(R.id.itemVerificationDoneDetailTextView)
+ val endGuideline by bind(R.id.messageEndGuideline)
+ val failedToSendIndicator by bind(R.id.messageFailToSendIndicator)
+ }
+
+ companion object {
+ private const val STUB_ID = R.id.messageVerificationDoneStub
+ }
+
+ /**
+ * This class holds all the common attributes for timeline items.
+ */
+ data class Attributes(
+ val toUserId: String,
+ val toUserName: String,
+ val isPositive: Boolean,
+ override val informationData: MessageInformationData,
+ override val avatarRenderer: AvatarRenderer,
+ override val colorProvider: ColorProvider,
+ override val itemLongClickListener: View.OnLongClickListener? = null,
+ override val itemClickListener: View.OnClickListener? = null,
+ override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
+ override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
+ val emojiTypeFace: Typeface? = null
+ ) : AbsBaseMessageItem.Attributes
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
new file mode 100644
index 0000000000..7964707d3c
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package im.vector.riotx.features.home.room.detail.timeline.item
+
+import android.annotation.SuppressLint
+import android.graphics.Typeface
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import com.airbnb.epoxy.EpoxyAttribute
+import com.airbnb.epoxy.EpoxyModelClass
+import im.vector.matrix.android.internal.session.room.VerificationState
+import im.vector.riotx.R
+import im.vector.riotx.core.resources.ColorProvider
+import im.vector.riotx.core.utils.DebouncedClickListener
+import im.vector.riotx.features.home.AvatarRenderer
+import im.vector.riotx.features.home.room.detail.RoomDetailAction
+import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
+
+@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
+abstract class VerificationRequestItem : AbsBaseMessageItem() {
+
+ override val baseAttributes: AbsBaseMessageItem.Attributes
+ get() = attributes
+
+ @EpoxyAttribute
+ lateinit var attributes: Attributes
+
+ @EpoxyAttribute
+ var callback: TimelineEventController.Callback? = null
+
+ override fun getViewType() = STUB_ID
+
+ @SuppressLint("SetTextI18n")
+ override fun bind(holder: Holder) {
+ super.bind(holder)
+
+ holder.endGuideline.updateLayoutParams {
+ this.marginEnd = leftGuideline
+ }
+
+ holder.titleView.text = if (attributes.informationData.sentByMe)
+ holder.view.context.getString(R.string.verification_sent)
+// + "\n ${attributes.informationData.referencesInfoData?.verificationStatus?.name
+// ?: "??"}"
+ else
+ holder.view.context.getString(R.string.verification_request)
+// + "\n ${attributes.informationData.referencesInfoData?.verificationStatus?.name
+// ?: "??"}"
+
+ holder.descriptionView.text = if (!attributes.informationData.sentByMe)
+ "${attributes.informationData.memberName} (${attributes.informationData.senderId})"
+ else
+ "${attributes.otherUserName} (${attributes.otherUserId})"
+
+ when (attributes.informationData.referencesInfoData?.verificationStatus) {
+ VerificationState.REQUEST,
+ null -> {
+ holder.buttonBar.isVisible = !attributes.informationData.sentByMe
+ holder.statusTextView.text = null
+ holder.statusTextView.isVisible = false
+ }
+ VerificationState.CANCELED_BY_OTHER -> {
+ holder.buttonBar.isVisible = false
+ holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName)
+ holder.statusTextView.isVisible = true
+ }
+ VerificationState.CANCELED_BY_ME -> {
+ holder.buttonBar.isVisible = false
+ holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_you_cancelled)
+ holder.statusTextView.isVisible = true
+ }
+ VerificationState.WAITING -> {
+ holder.buttonBar.isVisible = false
+ holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_waiting)
+ holder.statusTextView.isVisible = true
+ }
+ VerificationState.DONE -> {
+ holder.buttonBar.isVisible = false
+ holder.statusTextView.text = if (attributes.informationData.sentByMe)
+ holder.view.context.getString(R.string.verification_request_other_accepted, attributes.otherUserName)
+ else
+ holder.view.context.getString(R.string.verification_request_you_accepted)
+ holder.statusTextView.isVisible = true
+ }
+ else -> {
+ holder.buttonBar.isVisible = false
+ holder.statusTextView.text = null
+ holder.statusTextView.isVisible = false
+ }
+ }
+
+ holder.callback = callback
+ holder.attributes = attributes
+
+ renderSendState(holder.view, null, holder.failedToSendIndicator)
+ }
+
+ override fun unbind(holder: Holder) {
+ super.unbind(holder)
+ holder.callback = null
+ holder.attributes = null
+ }
+
+ class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
+
+ var callback: TimelineEventController.Callback? = null
+ var attributes: Attributes? = null
+
+ private val _clickListener = DebouncedClickListener(View.OnClickListener {
+ val att = attributes ?: return@OnClickListener
+ if (it == acceptButton) {
+ callback?.onTimelineItemAction(RoomDetailAction.AcceptVerificationRequest(
+ att.referenceId,
+ att.otherUserId,
+ att.fromDevide))
+ } else if (it == declineButton) {
+ callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId))
+ }
+ })
+
+ val titleView by bind(R.id.itemVerificationTitleTextView)
+ val descriptionView by bind(R.id.itemVerificationDetailTextView)
+ val buttonBar by bind(R.id.itemVerificationButtonBar)
+ val statusTextView by bind(R.id.itemVerificationStatusText)
+ val endGuideline by bind(R.id.messageEndGuideline)
+ private val declineButton by bind