homogenize ChooserTargetService and Shortcuts

Daniel Gultsch created

Change summary

build.gradle                                                                   |   6 
src/main/AndroidManifest.xml                                                   |  14 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java          | 899 
src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java | 117 
src/main/java/eu/siacs/conversations/services/NotificationService.java         |  41 
src/main/java/eu/siacs/conversations/services/ShortcutService.java             | 154 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java       | 179 
src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java             | 174 
src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java                 |  54 
src/main/res/xml/shortcuts.xml                                                 |   7 
10 files changed, 1,019 insertions(+), 626 deletions(-)

Detailed changes

build.gradle 🔗

@@ -42,13 +42,13 @@ spotless {
 
 
 dependencies {
-    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3'
 
     implementation project(':libs:annotation')
     annotationProcessor project(':libs:annotation-processor')
 
 
-    implementation 'androidx.viewpager:viewpager:1.0.0'
+    implementation 'androidx.viewpager:viewpager:1.1.0'
 
     playstoreImplementation('com.google.firebase:firebase-messaging:24.1.0') {
         exclude group: 'com.google.firebase', module: 'firebase-core'
@@ -59,6 +59,8 @@ dependencies {
     quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.1.0'
     implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1'
     implementation("com.github.CanHub:Android-Image-Cropper:2.0.0")
+    implementation "androidx.sharetarget:sharetarget:1.2.0"
+
     implementation 'androidx.appcompat:appcompat:1.7.0'
     implementation 'androidx.exifinterface:exifinterface:1.3.7'
     implementation 'androidx.cardview:cardview:1.0.0'

src/main/AndroidManifest.xml 🔗

@@ -124,14 +124,6 @@
             android:name=".services.ImportBackupService"
             android:exported="false"
             android:foregroundServiceType="dataSync" />
-        <service
-            android:name=".services.ContactChooserTargetService"
-            android:exported="true"
-            android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
-            <intent-filter>
-                <action android:name="android.service.chooser.ChooserTargetService" />
-            </intent-filter>
-        </service>
 
         <service
             android:name=".services.CallIntegrationConnectionService"
@@ -194,6 +186,9 @@
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+            <meta-data
+                android:name="android.app.shortcuts"
+                android:resource="@xml/shortcuts" />
         </activity>
         <activity
             android:name=".ui.ConversationsActivity"
@@ -330,10 +325,9 @@
                 <data android:mimeType="*/*" />
             </intent-filter>
 
-            <!-- the value here needs to be the full class name; independent of the configured applicationId -->
             <meta-data
                 android:name="android.service.chooser.chooser_target_service"
-                android:value="eu.siacs.conversations.services.ContactChooserTargetService" />
+                android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
         </activity>
         <activity
             android:name=".ui.TrustKeysActivity"

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -10,35 +10,7 @@ import android.os.Environment;
 import android.os.SystemClock;
 import android.util.Base64;
 import android.util.Log;
-
 import com.google.common.base.Stopwatch;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.IdentityKeyPair;
-import org.whispersystems.libsignal.InvalidKeyException;
-import org.whispersystems.libsignal.SignalProtocolAddress;
-import org.whispersystems.libsignal.state.PreKeyRecord;
-import org.whispersystems.libsignal.state.SessionRecord;
-import org.whispersystems.libsignal.state.SignedPreKeyRecord;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArrayList;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -60,6 +32,30 @@ import eu.siacs.conversations.utils.Resolver;
 import eu.siacs.conversations.xmpp.InvalidJid;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.mam.MamReference;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.whispersystems.libsignal.state.PreKeyRecord;
+import org.whispersystems.libsignal.state.SessionRecord;
+import org.whispersystems.libsignal.state.SignedPreKeyRecord;
 
 public class DatabaseBackend extends SQLiteOpenHelper {
 
@@ -68,123 +64,273 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 
     private static boolean requiresMessageIndexRebuild = false;
     private static DatabaseBackend instance = null;
-    private static final String CREATE_CONTATCS_STATEMENT = "create table "
-            + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
-            + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT,"
-            + Contact.PRESENCE_NAME + " TEXT,"
-            + Contact.JID + " TEXT," + Contact.KEYS + " TEXT,"
-            + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER,"
-            + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, "
-            + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, "
-            + Contact.RTP_CAPABILITY + " TEXT,"
-            + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
-            + Account.TABLENAME + "(" + Account.UUID
-            + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
-            + Contact.JID + ") ON CONFLICT REPLACE);";
-
-    private static final String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table "
-            + ServiceDiscoveryResult.TABLENAME + "("
-            + ServiceDiscoveryResult.HASH + " TEXT, "
-            + ServiceDiscoveryResult.VER + " TEXT, "
-            + ServiceDiscoveryResult.RESULT + " TEXT, "
-            + "UNIQUE(" + ServiceDiscoveryResult.HASH + ", "
-            + ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);";
-
-    private static final String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE "
-            + PresenceTemplate.TABELNAME + "("
-            + PresenceTemplate.UUID + " TEXT, "
-            + PresenceTemplate.LAST_USED + " NUMBER,"
-            + PresenceTemplate.MESSAGE + " TEXT,"
-            + PresenceTemplate.STATUS + " TEXT,"
-            + "UNIQUE(" + PresenceTemplate.MESSAGE + "," + PresenceTemplate.STATUS + ") ON CONFLICT REPLACE);";
-
-    private static final String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
-            + SQLiteAxolotlStore.PREKEY_TABLENAME + "("
-            + SQLiteAxolotlStore.ACCOUNT + " TEXT,  "
-            + SQLiteAxolotlStore.ID + " INTEGER, "
-            + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
-            + SQLiteAxolotlStore.ACCOUNT
-            + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
-            + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
-            + SQLiteAxolotlStore.ID
-            + ") ON CONFLICT REPLACE"
-            + ");";
-
-    private static final String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
-            + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
-            + SQLiteAxolotlStore.ACCOUNT + " TEXT,  "
-            + SQLiteAxolotlStore.ID + " INTEGER, "
-            + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
-            + SQLiteAxolotlStore.ACCOUNT
-            + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
-            + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
-            + SQLiteAxolotlStore.ID
-            + ") ON CONFLICT REPLACE" +
-            ");";
-
-    private static final String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
-            + SQLiteAxolotlStore.SESSION_TABLENAME + "("
-            + SQLiteAxolotlStore.ACCOUNT + " TEXT,  "
-            + SQLiteAxolotlStore.NAME + " TEXT, "
-            + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
-            + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
-            + SQLiteAxolotlStore.ACCOUNT
-            + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
-            + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
-            + SQLiteAxolotlStore.NAME + ", "
-            + SQLiteAxolotlStore.DEVICE_ID
-            + ") ON CONFLICT REPLACE"
-            + ");";
-
-    private static final String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
-            + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
-            + SQLiteAxolotlStore.ACCOUNT + " TEXT,  "
-            + SQLiteAxolotlStore.NAME + " TEXT, "
-            + SQLiteAxolotlStore.OWN + " INTEGER, "
-            + SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
-            + SQLiteAxolotlStore.CERTIFICATE + " BLOB, "
-            + SQLiteAxolotlStore.TRUST + " TEXT, "
-            + SQLiteAxolotlStore.ACTIVE + " NUMBER, "
-            + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER,"
-            + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
-            + SQLiteAxolotlStore.ACCOUNT
-            + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
-            + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
-            + SQLiteAxolotlStore.NAME + ", "
-            + SQLiteAxolotlStore.FINGERPRINT
-            + ") ON CONFLICT IGNORE"
-            + ");";
+    private static final String CREATE_CONTATCS_STATEMENT =
+            "create table "
+                    + Contact.TABLENAME
+                    + "("
+                    + Contact.ACCOUNT
+                    + " TEXT, "
+                    + Contact.SERVERNAME
+                    + " TEXT, "
+                    + Contact.SYSTEMNAME
+                    + " TEXT,"
+                    + Contact.PRESENCE_NAME
+                    + " TEXT,"
+                    + Contact.JID
+                    + " TEXT,"
+                    + Contact.KEYS
+                    + " TEXT,"
+                    + Contact.PHOTOURI
+                    + " TEXT,"
+                    + Contact.OPTIONS
+                    + " NUMBER,"
+                    + Contact.SYSTEMACCOUNT
+                    + " NUMBER, "
+                    + Contact.AVATAR
+                    + " TEXT, "
+                    + Contact.LAST_PRESENCE
+                    + " TEXT, "
+                    + Contact.LAST_TIME
+                    + " NUMBER, "
+                    + Contact.RTP_CAPABILITY
+                    + " TEXT,"
+                    + Contact.GROUPS
+                    + " TEXT, FOREIGN KEY("
+                    + Contact.ACCOUNT
+                    + ") REFERENCES "
+                    + Account.TABLENAME
+                    + "("
+                    + Account.UUID
+                    + ") ON DELETE CASCADE, UNIQUE("
+                    + Contact.ACCOUNT
+                    + ", "
+                    + Contact.JID
+                    + ") ON CONFLICT REPLACE);";
+
+    private static final String CREATE_DISCOVERY_RESULTS_STATEMENT =
+            "create table "
+                    + ServiceDiscoveryResult.TABLENAME
+                    + "("
+                    + ServiceDiscoveryResult.HASH
+                    + " TEXT, "
+                    + ServiceDiscoveryResult.VER
+                    + " TEXT, "
+                    + ServiceDiscoveryResult.RESULT
+                    + " TEXT, "
+                    + "UNIQUE("
+                    + ServiceDiscoveryResult.HASH
+                    + ", "
+                    + ServiceDiscoveryResult.VER
+                    + ") ON CONFLICT REPLACE);";
+
+    private static final String CREATE_PRESENCE_TEMPLATES_STATEMENT =
+            "CREATE TABLE "
+                    + PresenceTemplate.TABELNAME
+                    + "("
+                    + PresenceTemplate.UUID
+                    + " TEXT, "
+                    + PresenceTemplate.LAST_USED
+                    + " NUMBER,"
+                    + PresenceTemplate.MESSAGE
+                    + " TEXT,"
+                    + PresenceTemplate.STATUS
+                    + " TEXT,"
+                    + "UNIQUE("
+                    + PresenceTemplate.MESSAGE
+                    + ","
+                    + PresenceTemplate.STATUS
+                    + ") ON CONFLICT REPLACE);";
+
+    private static final String CREATE_PREKEYS_STATEMENT =
+            "CREATE TABLE "
+                    + SQLiteAxolotlStore.PREKEY_TABLENAME
+                    + "("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + " TEXT,  "
+                    + SQLiteAxolotlStore.ID
+                    + " INTEGER, "
+                    + SQLiteAxolotlStore.KEY
+                    + " TEXT, FOREIGN KEY("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ") REFERENCES "
+                    + Account.TABLENAME
+                    + "("
+                    + Account.UUID
+                    + ") ON DELETE CASCADE, "
+                    + "UNIQUE( "
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ", "
+                    + SQLiteAxolotlStore.ID
+                    + ") ON CONFLICT REPLACE"
+                    + ");";
+
+    private static final String CREATE_SIGNED_PREKEYS_STATEMENT =
+            "CREATE TABLE "
+                    + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME
+                    + "("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + " TEXT,  "
+                    + SQLiteAxolotlStore.ID
+                    + " INTEGER, "
+                    + SQLiteAxolotlStore.KEY
+                    + " TEXT, FOREIGN KEY("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ") REFERENCES "
+                    + Account.TABLENAME
+                    + "("
+                    + Account.UUID
+                    + ") ON DELETE CASCADE, "
+                    + "UNIQUE( "
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ", "
+                    + SQLiteAxolotlStore.ID
+                    + ") ON CONFLICT REPLACE"
+                    + ");";
+
+    private static final String CREATE_SESSIONS_STATEMENT =
+            "CREATE TABLE "
+                    + SQLiteAxolotlStore.SESSION_TABLENAME
+                    + "("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + " TEXT,  "
+                    + SQLiteAxolotlStore.NAME
+                    + " TEXT, "
+                    + SQLiteAxolotlStore.DEVICE_ID
+                    + " INTEGER, "
+                    + SQLiteAxolotlStore.KEY
+                    + " TEXT, FOREIGN KEY("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ") REFERENCES "
+                    + Account.TABLENAME
+                    + "("
+                    + Account.UUID
+                    + ") ON DELETE CASCADE, "
+                    + "UNIQUE( "
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ", "
+                    + SQLiteAxolotlStore.NAME
+                    + ", "
+                    + SQLiteAxolotlStore.DEVICE_ID
+                    + ") ON CONFLICT REPLACE"
+                    + ");";
+
+    private static final String CREATE_IDENTITIES_STATEMENT =
+            "CREATE TABLE "
+                    + SQLiteAxolotlStore.IDENTITIES_TABLENAME
+                    + "("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + " TEXT,  "
+                    + SQLiteAxolotlStore.NAME
+                    + " TEXT, "
+                    + SQLiteAxolotlStore.OWN
+                    + " INTEGER, "
+                    + SQLiteAxolotlStore.FINGERPRINT
+                    + " TEXT, "
+                    + SQLiteAxolotlStore.CERTIFICATE
+                    + " BLOB, "
+                    + SQLiteAxolotlStore.TRUST
+                    + " TEXT, "
+                    + SQLiteAxolotlStore.ACTIVE
+                    + " NUMBER, "
+                    + SQLiteAxolotlStore.LAST_ACTIVATION
+                    + " NUMBER,"
+                    + SQLiteAxolotlStore.KEY
+                    + " TEXT, FOREIGN KEY("
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ") REFERENCES "
+                    + Account.TABLENAME
+                    + "("
+                    + Account.UUID
+                    + ") ON DELETE CASCADE, "
+                    + "UNIQUE( "
+                    + SQLiteAxolotlStore.ACCOUNT
+                    + ", "
+                    + SQLiteAxolotlStore.NAME
+                    + ", "
+                    + SQLiteAxolotlStore.FINGERPRINT
+                    + ") ON CONFLICT IGNORE"
+                    + ");";
 
     private static final String RESOLVER_RESULTS_TABLENAME = "resolver_results";
 
-    private static final String CREATE_RESOLVER_RESULTS_TABLE = "create table " + RESOLVER_RESULTS_TABLENAME + "("
-            + Resolver.Result.DOMAIN + " TEXT,"
-            + Resolver.Result.HOSTNAME + " TEXT,"
-            + Resolver.Result.IP + " BLOB,"
-            + Resolver.Result.PRIORITY + " NUMBER,"
-            + Resolver.Result.DIRECT_TLS + " NUMBER,"
-            + Resolver.Result.AUTHENTICATED + " NUMBER,"
-            + Resolver.Result.PORT + " NUMBER,"
-            + "UNIQUE(" + Resolver.Result.DOMAIN + ") ON CONFLICT REPLACE"
-            + ");";
-
-    private static final String CREATE_MESSAGE_TIME_INDEX = "CREATE INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")";
-    private static final String CREATE_MESSAGE_CONVERSATION_INDEX = "CREATE INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")";
-    private static final String CREATE_MESSAGE_DELETED_INDEX = "CREATE INDEX message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")";
-    private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX = "CREATE INDEX message_file_path_index ON " + Message.TABLENAME + "(" + Message.RELATIVE_FILE_PATH + ")";
-    private static final String CREATE_MESSAGE_TYPE_INDEX = "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")";
-
-    private static final String CREATE_MESSAGE_INDEX_TABLE = "CREATE VIRTUAL TABLE messages_index USING fts4 (uuid,body,notindexed=\"uuid\",content=\"" + Message.TABLENAME + "\",tokenize='unicode61')";
-    private static final String CREATE_MESSAGE_INSERT_TRIGGER = "CREATE TRIGGER after_message_insert AFTER INSERT ON " + Message.TABLENAME + " BEGIN INSERT INTO messages_index(rowid,uuid,body) VALUES(NEW.rowid,NEW.uuid,NEW.body); END;";
-    private static final String CREATE_MESSAGE_UPDATE_TRIGGER = "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON " + Message.TABLENAME + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE rowid=OLD.rowid; END;";
-    private static final String CREATE_MESSAGE_DELETE_TRIGGER = "CREATE TRIGGER after_message_delete AFTER DELETE ON " + Message.TABLENAME + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;";
-    private static final String COPY_PREEXISTING_ENTRIES = "INSERT INTO messages_index(messages_index) VALUES('rebuild');";
+    private static final String CREATE_RESOLVER_RESULTS_TABLE =
+            "create table "
+                    + RESOLVER_RESULTS_TABLENAME
+                    + "("
+                    + Resolver.Result.DOMAIN
+                    + " TEXT,"
+                    + Resolver.Result.HOSTNAME
+                    + " TEXT,"
+                    + Resolver.Result.IP
+                    + " BLOB,"
+                    + Resolver.Result.PRIORITY
+                    + " NUMBER,"
+                    + Resolver.Result.DIRECT_TLS
+                    + " NUMBER,"
+                    + Resolver.Result.AUTHENTICATED
+                    + " NUMBER,"
+                    + Resolver.Result.PORT
+                    + " NUMBER,"
+                    + "UNIQUE("
+                    + Resolver.Result.DOMAIN
+                    + ") ON CONFLICT REPLACE"
+                    + ");";
+
+    private static final String CREATE_MESSAGE_TIME_INDEX =
+            "CREATE INDEX message_time_index ON "
+                    + Message.TABLENAME
+                    + "("
+                    + Message.TIME_SENT
+                    + ")";
+    private static final String CREATE_MESSAGE_CONVERSATION_INDEX =
+            "CREATE INDEX message_conversation_index ON "
+                    + Message.TABLENAME
+                    + "("
+                    + Message.CONVERSATION
+                    + ")";
+    private static final String CREATE_MESSAGE_DELETED_INDEX =
+            "CREATE INDEX message_deleted_index ON "
+                    + Message.TABLENAME
+                    + "("
+                    + Message.DELETED
+                    + ")";
+    private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX =
+            "CREATE INDEX message_file_path_index ON "
+                    + Message.TABLENAME
+                    + "("
+                    + Message.RELATIVE_FILE_PATH
+                    + ")";
+    private static final String CREATE_MESSAGE_TYPE_INDEX =
+            "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")";
+
+    private static final String CREATE_MESSAGE_INDEX_TABLE =
+            "CREATE VIRTUAL TABLE messages_index USING fts4"
+                    + " (uuid,body,notindexed=\"uuid\",content=\""
+                    + Message.TABLENAME
+                    + "\",tokenize='unicode61')";
+    private static final String CREATE_MESSAGE_INSERT_TRIGGER =
+            "CREATE TRIGGER after_message_insert AFTER INSERT ON "
+                    + Message.TABLENAME
+                    + " BEGIN INSERT INTO messages_index(rowid,uuid,body)"
+                    + " VALUES(NEW.rowid,NEW.uuid,NEW.body); END;";
+    private static final String CREATE_MESSAGE_UPDATE_TRIGGER =
+            "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON "
+                    + Message.TABLENAME
+                    + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE"
+                    + " rowid=OLD.rowid; END;";
+    private static final String CREATE_MESSAGE_DELETE_TRIGGER =
+            "CREATE TRIGGER after_message_delete AFTER DELETE ON "
+                    + Message.TABLENAME
+                    + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;";
+    private static final String COPY_PREEXISTING_ENTRIES =
+            "INSERT INTO messages_index(messages_index) VALUES('rebuild');";
 
     private DatabaseBackend(Context context) {
         super(context, DATABASE_NAME, null, DATABASE_VERSION);
     }
 
-    private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
+    private static ContentValues createFingerprintStatusContentValues(
+            FingerprintStatus.Trust trust, boolean active) {
         ContentValues values = new ContentValues();
         values.put(SQLiteAxolotlStore.TRUST, trust.toString());
         values.put(SQLiteAxolotlStore.ACTIVE, active ? 1 : 0);
@@ -199,7 +345,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         final SQLiteDatabase db = getWritableDatabase();
         final Stopwatch stopwatch = Stopwatch.createStarted();
         db.execSQL(COPY_PREEXISTING_ENTRIES);
-        Log.d(Config.LOGTAG,"rebuilt message index in "+ stopwatch.stop().toString());
+        Log.d(Config.LOGTAG, "rebuilt message index in " + stopwatch.stop().toString());
     }
 
     public static synchronized DatabaseBackend getInstance(Context context) {
@@ -217,57 +363,132 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 
     @Override
     public void onCreate(SQLiteDatabase db) {
-        db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID + " TEXT PRIMARY KEY,"
-                + Account.USERNAME + " TEXT,"
-                + Account.SERVER + " TEXT,"
-                + Account.PASSWORD + " TEXT,"
-                + Account.DISPLAY_NAME + " TEXT, "
-                + Account.STATUS + " TEXT,"
-                + Account.STATUS_MESSAGE + " TEXT,"
-                + Account.ROSTERVERSION + " TEXT,"
-                + Account.OPTIONS + " NUMBER, "
-                + Account.AVATAR + " TEXT, "
-                + Account.KEYS + " TEXT, "
-                + Account.HOSTNAME + " TEXT, "
-                + Account.RESOURCE + " TEXT,"
-                + Account.PINNED_MECHANISM + " TEXT,"
-                + Account.PINNED_CHANNEL_BINDING + " TEXT,"
-                + Account.FAST_MECHANISM + " TEXT,"
-                + Account.FAST_TOKEN + " TEXT,"
-                + Account.PORT + " NUMBER DEFAULT 5222)");
-        db.execSQL("create table " + Conversation.TABLENAME + " ("
-                + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
-                + " TEXT, " + Conversation.CONTACT + " TEXT, "
-                + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID
-                + " TEXT, " + Conversation.CREATED + " NUMBER, "
-                + Conversation.STATUS + " NUMBER, " + Conversation.MODE
-                + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY("
-                + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME
-                + "(" + Account.UUID + ") ON DELETE CASCADE);");
-        db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID
-                + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, "
-                + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART
-                + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
-                + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
-                + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
-                + Message.RELATIVE_FILE_PATH + " TEXT, "
-                + Message.SERVER_MSG_ID + " TEXT, "
-                + Message.FINGERPRINT + " TEXT, "
-                + Message.CARBON + " INTEGER, "
-                + Message.EDITED + " TEXT, "
-                + Message.READ + " NUMBER DEFAULT 1, "
-                + Message.OOB + " INTEGER, "
-                + Message.ERROR_MESSAGE + " TEXT,"
-                + Message.READ_BY_MARKERS + " TEXT,"
-                + Message.MARKABLE + " NUMBER DEFAULT 0,"
-                + Message.DELETED + " NUMBER DEFAULT 0,"
-                + Message.BODY_LANGUAGE + " TEXT,"
-                + Message.OCCUPANT_ID + " TEXT,"
-                + Message.REACTIONS + " TEXT,"
-                + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
-                + Message.CONVERSATION + ") REFERENCES "
-                + Conversation.TABLENAME + "(" + Conversation.UUID
-                + ") ON DELETE CASCADE);");
+        db.execSQL(
+                "create table "
+                        + Account.TABLENAME
+                        + "("
+                        + Account.UUID
+                        + " TEXT PRIMARY KEY,"
+                        + Account.USERNAME
+                        + " TEXT,"
+                        + Account.SERVER
+                        + " TEXT,"
+                        + Account.PASSWORD
+                        + " TEXT,"
+                        + Account.DISPLAY_NAME
+                        + " TEXT, "
+                        + Account.STATUS
+                        + " TEXT,"
+                        + Account.STATUS_MESSAGE
+                        + " TEXT,"
+                        + Account.ROSTERVERSION
+                        + " TEXT,"
+                        + Account.OPTIONS
+                        + " NUMBER, "
+                        + Account.AVATAR
+                        + " TEXT, "
+                        + Account.KEYS
+                        + " TEXT, "
+                        + Account.HOSTNAME
+                        + " TEXT, "
+                        + Account.RESOURCE
+                        + " TEXT,"
+                        + Account.PINNED_MECHANISM
+                        + " TEXT,"
+                        + Account.PINNED_CHANNEL_BINDING
+                        + " TEXT,"
+                        + Account.FAST_MECHANISM
+                        + " TEXT,"
+                        + Account.FAST_TOKEN
+                        + " TEXT,"
+                        + Account.PORT
+                        + " NUMBER DEFAULT 5222)");
+        db.execSQL(
+                "create table "
+                        + Conversation.TABLENAME
+                        + " ("
+                        + Conversation.UUID
+                        + " TEXT PRIMARY KEY, "
+                        + Conversation.NAME
+                        + " TEXT, "
+                        + Conversation.CONTACT
+                        + " TEXT, "
+                        + Conversation.ACCOUNT
+                        + " TEXT, "
+                        + Conversation.CONTACTJID
+                        + " TEXT, "
+                        + Conversation.CREATED
+                        + " NUMBER, "
+                        + Conversation.STATUS
+                        + " NUMBER, "
+                        + Conversation.MODE
+                        + " NUMBER, "
+                        + Conversation.ATTRIBUTES
+                        + " TEXT, FOREIGN KEY("
+                        + Conversation.ACCOUNT
+                        + ") REFERENCES "
+                        + Account.TABLENAME
+                        + "("
+                        + Account.UUID
+                        + ") ON DELETE CASCADE);");
+        db.execSQL(
+                "create table "
+                        + Message.TABLENAME
+                        + "( "
+                        + Message.UUID
+                        + " TEXT PRIMARY KEY, "
+                        + Message.CONVERSATION
+                        + " TEXT, "
+                        + Message.TIME_SENT
+                        + " NUMBER, "
+                        + Message.COUNTERPART
+                        + " TEXT, "
+                        + Message.TRUE_COUNTERPART
+                        + " TEXT,"
+                        + Message.BODY
+                        + " TEXT, "
+                        + Message.ENCRYPTION
+                        + " NUMBER, "
+                        + Message.STATUS
+                        + " NUMBER,"
+                        + Message.TYPE
+                        + " NUMBER, "
+                        + Message.RELATIVE_FILE_PATH
+                        + " TEXT, "
+                        + Message.SERVER_MSG_ID
+                        + " TEXT, "
+                        + Message.FINGERPRINT
+                        + " TEXT, "
+                        + Message.CARBON
+                        + " INTEGER, "
+                        + Message.EDITED
+                        + " TEXT, "
+                        + Message.READ
+                        + " NUMBER DEFAULT 1, "
+                        + Message.OOB
+                        + " INTEGER, "
+                        + Message.ERROR_MESSAGE
+                        + " TEXT,"
+                        + Message.READ_BY_MARKERS
+                        + " TEXT,"
+                        + Message.MARKABLE
+                        + " NUMBER DEFAULT 0,"
+                        + Message.DELETED
+                        + " NUMBER DEFAULT 0,"
+                        + Message.BODY_LANGUAGE
+                        + " TEXT,"
+                        + Message.OCCUPANT_ID
+                        + " TEXT,"
+                        + Message.REACTIONS
+                        + " TEXT,"
+                        + Message.REMOTE_MSG_ID
+                        + " TEXT, FOREIGN KEY("
+                        + Message.CONVERSATION
+                        + ") REFERENCES "
+                        + Conversation.TABLENAME
+                        + "("
+                        + Conversation.UUID
+                        + ") ON DELETE CASCADE);");
         db.execSQL(CREATE_MESSAGE_TIME_INDEX);
         db.execSQL(CREATE_MESSAGE_CONVERSATION_INDEX);
         db.execSQL(CREATE_MESSAGE_DELETED_INDEX);
@@ -290,54 +511,87 @@ public class DatabaseBackend extends SQLiteOpenHelper {
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         if (oldVersion < 2 && newVersion >= 2) {
-            db.execSQL("update " + Account.TABLENAME + " set "
-                    + Account.OPTIONS + " = " + Account.OPTIONS + " | 8");
+            db.execSQL(
+                    "update "
+                            + Account.TABLENAME
+                            + " set "
+                            + Account.OPTIONS
+                            + " = "
+                            + Account.OPTIONS
+                            + " | 8");
         }
         if (oldVersion < 3 && newVersion >= 3) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
-                    + Message.TYPE + " NUMBER");
+            db.execSQL(
+                    "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.TYPE + " NUMBER");
         }
         if (oldVersion < 5 && newVersion >= 5) {
             db.execSQL("DROP TABLE " + Contact.TABLENAME);
             db.execSQL(CREATE_CONTATCS_STATEMENT);
-            db.execSQL("UPDATE " + Account.TABLENAME + " SET "
-                    + Account.ROSTERVERSION + " = NULL");
+            db.execSQL("UPDATE " + Account.TABLENAME + " SET " + Account.ROSTERVERSION + " = NULL");
         }
         if (oldVersion < 6 && newVersion >= 6) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
-                    + Message.TRUE_COUNTERPART + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.TRUE_COUNTERPART
+                            + " TEXT");
         }
         if (oldVersion < 7 && newVersion >= 7) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
-                    + Message.REMOTE_MSG_ID + " TEXT");
-            db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
-                    + Contact.AVATAR + " TEXT");
-            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN "
-                    + Account.AVATAR + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.REMOTE_MSG_ID
+                            + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.AVATAR + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.AVATAR + " TEXT");
         }
         if (oldVersion < 8 && newVersion >= 8) {
-            db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN "
-                    + Conversation.ATTRIBUTES + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Conversation.TABLENAME
+                            + " ADD COLUMN "
+                            + Conversation.ATTRIBUTES
+                            + " TEXT");
         }
         if (oldVersion < 9 && newVersion >= 9) {
-            db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
-                    + Contact.LAST_TIME + " NUMBER");
-            db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
-                    + Contact.LAST_PRESENCE + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Contact.TABLENAME
+                            + " ADD COLUMN "
+                            + Contact.LAST_TIME
+                            + " NUMBER");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Contact.TABLENAME
+                            + " ADD COLUMN "
+                            + Contact.LAST_PRESENCE
+                            + " TEXT");
         }
         if (oldVersion < 10 && newVersion >= 10) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
-                    + Message.RELATIVE_FILE_PATH + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.RELATIVE_FILE_PATH
+                            + " TEXT");
         }
         if (oldVersion < 11 && newVersion >= 11) {
-            db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
-                    + Contact.GROUPS + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.GROUPS + " TEXT");
             db.execSQL("delete from " + Contact.TABLENAME);
             db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
         }
         if (oldVersion < 12 && newVersion >= 12) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
-                    + Message.SERVER_MSG_ID + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.SERVER_MSG_ID
+                            + " TEXT");
         }
         if (oldVersion < 13 && newVersion >= 13) {
             db.execSQL("delete from " + Contact.TABLENAME);
@@ -348,26 +602,60 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         }
         if (oldVersion < 15 && newVersion >= 15) {
             recreateAxolotlDb(db);
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
-                    + Message.FINGERPRINT + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.FINGERPRINT
+                            + " TEXT");
         }
         if (oldVersion < 16 && newVersion >= 16) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
-                    + Message.CARBON + " INTEGER");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.CARBON
+                            + " INTEGER");
         }
         if (oldVersion < 19 && newVersion >= 19) {
-            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.DISPLAY_NAME + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Account.TABLENAME
+                            + " ADD COLUMN "
+                            + Account.DISPLAY_NAME
+                            + " TEXT");
         }
         if (oldVersion < 20 && newVersion >= 20) {
-            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT");
-            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Account.TABLENAME
+                            + " ADD COLUMN "
+                            + Account.HOSTNAME
+                            + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Account.TABLENAME
+                            + " ADD COLUMN "
+                            + Account.PORT
+                            + " NUMBER DEFAULT 5222");
         }
         if (oldVersion < 26 && newVersion >= 26) {
-            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT");
-            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Account.TABLENAME
+                            + " ADD COLUMN "
+                            + Account.STATUS_MESSAGE
+                            + " TEXT");
         }
         if (oldVersion < 40 && newVersion >= 40) {
-            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.RESOURCE + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Account.TABLENAME
+                            + " ADD COLUMN "
+                            + Account.RESOURCE
+                            + " TEXT");
         }
         /* Any migrations that alter the Account table need to happen BEFORE this migration, as it
          * depends on account de-serialization.
@@ -375,45 +663,67 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         if (oldVersion < 17 && newVersion >= 17 && newVersion < 31) {
             List<Account> accounts = getAccounts(db);
             for (Account account : accounts) {
-                String ownDeviceIdString = account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID);
+                String ownDeviceIdString =
+                        account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID);
                 if (ownDeviceIdString == null) {
                     continue;
                 }
                 int ownDeviceId = Integer.valueOf(ownDeviceIdString);
-                SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownDeviceId);
+                SignalProtocolAddress ownAddress =
+                        new SignalProtocolAddress(
+                                account.getJid().asBareJid().toString(), ownDeviceId);
                 deleteSession(db, account, ownAddress);
                 IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
                 if (identityKeyPair != null) {
                     String[] selectionArgs = {
-                            account.getUuid(),
-                            CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize())
+                        account.getUuid(),
+                        CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize())
                     };
                     ContentValues values = new ContentValues();
                     values.put(SQLiteAxolotlStore.TRUSTED, 2);
-                    db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
-                            SQLiteAxolotlStore.ACCOUNT + " = ? AND "
-                                    + SQLiteAxolotlStore.FINGERPRINT + " = ? ",
+                    db.update(
+                            SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+                            values,
+                            SQLiteAxolotlStore.ACCOUNT
+                                    + " = ? AND "
+                                    + SQLiteAxolotlStore.FINGERPRINT
+                                    + " = ? ",
                             selectionArgs);
                 } else {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not load own identity key pair");
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid()
+                                    + ": could not load own identity key pair");
                 }
             }
         }
         if (oldVersion < 18 && newVersion >= 18) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ + " NUMBER DEFAULT 1");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.READ
+                            + " NUMBER DEFAULT 1");
         }
 
         if (oldVersion < 21 && newVersion >= 21) {
             List<Account> accounts = getAccounts(db);
             for (Account account : accounts) {
                 account.unsetPgpSignature();
-                db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
-                        + "=?", new String[]{account.getUuid()});
+                db.update(
+                        Account.TABLENAME,
+                        account.getContentValues(),
+                        Account.UUID + "=?",
+                        new String[] {account.getUuid()});
             }
         }
 
         if (oldVersion >= 15 && oldVersion < 22 && newVersion >= 22) {
-            db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE);
+            db.execSQL(
+                    "ALTER TABLE "
+                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
+                            + " ADD COLUMN "
+                            + SQLiteAxolotlStore.CERTIFICATE);
         }
 
         if (oldVersion < 23 && newVersion >= 23) {
@@ -421,11 +731,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         }
 
         if (oldVersion < 24 && newVersion >= 24) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
         }
 
         if (oldVersion < 25 && newVersion >= 25) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
+            db.execSQL(
+                    "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
         }
 
         if (oldVersion < 26 && newVersion >= 26) {
@@ -441,51 +753,116 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         }
 
         if (oldVersion < 29 && newVersion >= 29) {
-            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + Message.TABLENAME
+                            + " ADD COLUMN "
+                            + Message.ERROR_MESSAGE
+                            + " TEXT");
         }
         if (oldVersion >= 15 && oldVersion < 31 && newVersion >= 31) {
-            db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.TRUST + " TEXT");
-            db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.ACTIVE + " NUMBER");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
+                            + " ADD COLUMN "
+                            + SQLiteAxolotlStore.TRUST
+                            + " TEXT");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
+                            + " ADD COLUMN "
+                            + SQLiteAxolotlStore.ACTIVE
+                            + " NUMBER");
             HashMap<Integer, ContentValues> migration = new HashMap<>();
-            migration.put(0, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
-            migration.put(1, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
-            migration.put(2, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
-            migration.put(3, createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false));
-            migration.put(4, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
-            migration.put(5, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
-            migration.put(6, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
-            migration.put(7, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true));
-            migration.put(8, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false));
+            migration.put(
+                    0, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
+            migration.put(
+                    1, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
+            migration.put(
+                    2,
+                    createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
+            migration.put(
+                    3,
+                    createFingerprintStatusContentValues(
+                            FingerprintStatus.Trust.COMPROMISED, false));
+            migration.put(
+                    4,
+                    createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
+            migration.put(
+                    5,
+                    createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
+            migration.put(
+                    6,
+                    createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
+            migration.put(
+                    7,
+                    createFingerprintStatusContentValues(
+                            FingerprintStatus.Trust.VERIFIED_X509, true));
+            migration.put(
+                    8,
+                    createFingerprintStatusContentValues(
+                            FingerprintStatus.Trust.VERIFIED_X509, false));
             for (Map.Entry<Integer, ContentValues> entry : migration.entrySet()) {
                 String whereClause = SQLiteAxolotlStore.TRUSTED + "=?";
                 String[] where = {String.valueOf(entry.getKey())};
-                db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, entry.getValue(), whereClause, where);
+                db.update(
+                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+                        entry.getValue(),
+                        whereClause,
+                        where);
             }
-
         }
         if (oldVersion >= 15 && oldVersion < 32 && newVersion >= 32) {
-            db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER");
+            db.execSQL(
+                    "ALTER TABLE "
+                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
+                            + " ADD COLUMN "
+                            + SQLiteAxolotlStore.LAST_ACTIVATION
+                            + " NUMBER");
             ContentValues defaults = new ContentValues();
             defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION, System.currentTimeMillis());
             db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, defaults, null, null);
         }
         if (oldVersion >= 15 && oldVersion < 33 && newVersion >= 33) {
             String whereClause = SQLiteAxolotlStore.OWN + "=1";
-            db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED, true), whereClause, null);
+            db.update(
+                    SQLiteAxolotlStore.IDENTITIES_TABLENAME,
+                    createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED, true),
+                    whereClause,
+                    null);
         }
 
         if (oldVersion < 34 && newVersion >= 34) {
             db.execSQL(CREATE_MESSAGE_TIME_INDEX);
 
-            final File oldPicturesDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/Conversations/");
-            final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/");
-            final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Files/");
-            final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Videos/");
+            final File oldPicturesDirectory =
+                    new File(
+                            Environment.getExternalStoragePublicDirectory(
+                                            Environment.DIRECTORY_PICTURES)
+                                    + "/Conversations/");
+            final File oldFilesDirectory =
+                    new File(Environment.getExternalStorageDirectory() + "/Conversations/");
+            final File newFilesDirectory =
+                    new File(
+                            Environment.getExternalStorageDirectory()
+                                    + "/Conversations/Media/Conversations Files/");
+            final File newVideosDirectory =
+                    new File(
+                            Environment.getExternalStorageDirectory()
+                                    + "/Conversations/Media/Conversations Videos/");
             if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) {
-                final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Images/");
+                final File newPicturesDirectory =
+                        new File(
+                                Environment.getExternalStorageDirectory()
+                                        + "/Conversations/Media/Conversations Images/");
                 newPicturesDirectory.getParentFile().mkdirs();
                 if (oldPicturesDirectory.renameTo(newPicturesDirectory)) {
-                    Log.d(Config.LOGTAG, "moved " + oldPicturesDirectory.getAbsolutePath() + " to " + newPicturesDirectory.getAbsolutePath());
+                    Log.d(
+                            Config.LOGTAG,
+                            "moved "
+                                    + oldPicturesDirectory.getAbsolutePath()
+                                    + " to "
+                                    + newPicturesDirectory.getAbsolutePath());
                 }
             }
             if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) {

src/main/java/eu/siacs/conversations/services/ContactChooserTargetService.java 🔗

@@ -1,117 +0,0 @@
-package eu.siacs.conversations.services;
-
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.graphics.drawable.Icon;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.service.chooser.ChooserTarget;
-import android.service.chooser.ChooserTargetService;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.receiver.SystemEventReceiver;
-import eu.siacs.conversations.ui.ConversationsActivity;
-import eu.siacs.conversations.utils.Compatibility;
-
-@SuppressLint("Deprecated")
-@TargetApi(Build.VERSION_CODES.M)
-public class ContactChooserTargetService extends ChooserTargetService implements ServiceConnection {
-
-    private final Object lock = new Object();
-    private static final int MAX_TARGETS = 5;
-    private XmppConnectionService mXmppConnectionService;
-
-    private static boolean textOnly(IntentFilter filter) {
-        for (int i = 0; i < filter.countDataTypes(); ++i) {
-            if (!"text/plain".equals(filter.getDataType(i))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public List<ChooserTarget> onGetChooserTargets(
-            final ComponentName targetActivityName, final IntentFilter matchedFilter) {
-        if (!SystemEventReceiver.hasEnabledAccounts(this)) {
-            return Collections.emptyList();
-        }
-        final Intent intent = new Intent(this, XmppConnectionService.class);
-        intent.setAction("contact_chooser");
-        Compatibility.startService(this, intent);
-        bindService(intent, this, Context.BIND_AUTO_CREATE);
-        try {
-            waitForService();
-            if (!mXmppConnectionService.areMessagesInitialized()) {
-                return Collections.emptyList();
-            }
-            final ArrayList<Conversation> conversations = new ArrayList<>();
-            mXmppConnectionService.populateWithOrderedConversations(
-                    conversations, textOnly(matchedFilter));
-            final ComponentName componentName =
-                    new ComponentName(this, ConversationsActivity.class);
-            final int pixel = AvatarService.getSystemUiAvatarSize(this);
-            final ArrayList<ChooserTarget> chooserTargets = new ArrayList<>();
-            for (final Conversation conversation : conversations) {
-                if (conversation.sentMessagesCount() == 0) {
-                    continue;
-                }
-                final String name = conversation.getName().toString();
-                final Icon icon =
-                        Icon.createWithBitmap(
-                                mXmppConnectionService.getAvatarService().get(conversation, pixel));
-                final float score = 1 - (1.0f / MAX_TARGETS) * chooserTargets.size();
-                final Bundle extras = new Bundle();
-                extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
-                chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras));
-                if (chooserTargets.size() >= MAX_TARGETS) {
-                    return chooserTargets;
-                }
-            }
-            return chooserTargets;
-        } catch (final InterruptedException e) {
-            Log.d(
-                    Config.LOGTAG,
-                    "Thread got interrupted before binding to XmppConnectionService",
-                    e);
-        } finally {
-            unbindService(this);
-        }
-        return Collections.emptyList();
-    }
-
-    @Override
-    public void onServiceConnected(final ComponentName name, final IBinder service) {
-        XmppConnectionService.XmppConnectionBinder binder =
-                (XmppConnectionService.XmppConnectionBinder) service;
-        mXmppConnectionService = binder.getService();
-        synchronized (this.lock) {
-            lock.notifyAll();
-        }
-    }
-
-    @Override
-    public void onServiceDisconnected(ComponentName name) {
-        mXmppConnectionService = null;
-    }
-
-    private void waitForService() throws InterruptedException {
-        if (mXmppConnectionService == null) {
-            synchronized (this.lock) {
-                lock.wait();
-            }
-        }
-    }
-}

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -28,7 +28,6 @@ import android.text.SpannableString;
 import android.text.style.StyleSpan;
 import android.util.DisplayMetrics;
 import android.util.Log;
-
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.core.app.ActivityCompat;
@@ -41,7 +40,6 @@ import androidx.core.app.RemoteInput;
 import androidx.core.content.ContextCompat;
 import androidx.core.content.pm.ShortcutInfoCompat;
 import androidx.core.graphics.drawable.IconCompat;
-
 import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
@@ -49,7 +47,6 @@ import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.primitives.Ints;
-
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -70,7 +67,6 @@ import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
-
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -505,7 +501,8 @@ public class NotificationService {
             Log.d(
                     Config.LOGTAG,
                     message.getConversation().getAccount().getJid().asBareJid()
-                            + ": suppressing failed delivery notification because conversation is open");
+                            + ": suppressing failed delivery notification because conversation is"
+                            + " open");
             return;
         }
         final PendingIntent pendingIntent = createContentIntent(conversation);
@@ -631,10 +628,11 @@ public class NotificationService {
                         .build());
         modifyIncomingCall(builder);
         final Notification notification = builder.build();
-        notification.audioAttributes = new AudioAttributes.Builder()
-                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
-                .build();
+        notification.audioAttributes =
+                new AudioAttributes.Builder()
+                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                        .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                        .build();
         notification.flags = notification.flags | Notification.FLAG_INSISTENT;
         notify(INCOMING_CALL_NOTIFICATION_ID, notification);
     }
@@ -708,7 +706,8 @@ public class NotificationService {
         if (jingleRtpConnection == null) {
             return false;
         }
-        final var notificationManager = mXmppConnectionService.getSystemService(NotificationManager.class);
+        final var notificationManager =
+                mXmppConnectionService.getSystemService(NotificationManager.class);
         if (Iterables.any(
                 Arrays.asList(notificationManager.getActiveNotifications()),
                 n -> n.getId() == INCOMING_CALL_NOTIFICATION_ID)) {
@@ -820,7 +819,8 @@ public class NotificationService {
                         Log.d(
                                 Config.LOGTAG,
                                 conversational.getAccount().getJid().asBareJid()
-                                        + ": dismissed missed call because call was picked up on other device");
+                                        + ": dismissed missed call because call was picked up on"
+                                        + " other device");
                         iterator.remove();
                     }
                 }
@@ -1345,12 +1345,15 @@ public class NotificationService {
             if (systemAccount != null) {
                 notificationBuilder.addPerson(systemAccount.toString());
             }
-            info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
+            info =
+                    mXmppConnectionService
+                            .getShortcutService()
+                            .getShortcutInfo(contact, conversation.getUuid());
         } else {
             info =
                     mXmppConnectionService
                             .getShortcutService()
-                            .getShortcutInfoCompat(conversation.getMucOptions());
+                            .getShortcutInfo(conversation.getMucOptions());
         }
         notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
         notificationBuilder.setSmallIcon(R.drawable.ic_app_icon_notification);
@@ -1384,16 +1387,16 @@ public class NotificationService {
             }
             final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
             bigPictureStyle.bigPicture(bitmap);
-            if (tmp.size() > 0) {
-                CharSequence text = getMergedBodies(tmp);
-                bigPictureStyle.setSummaryText(text);
-                builder.setContentText(text);
-                builder.setTicker(text);
-            } else {
+            if (tmp.isEmpty()) {
                 final String description =
                         UIHelper.getFileDescriptionString(mXmppConnectionService, message);
                 builder.setContentText(description);
                 builder.setTicker(description);
+            } else {
+                final CharSequence text = getMergedBodies(tmp);
+                bigPictureStyle.setSummaryText(text);
+                builder.setContentText(text);
+                builder.setTicker(text);
             }
             builder.setStyle(bigPictureStyle);
         } catch (final IOException e) {

src/main/java/eu/siacs/conversations/services/ShortcutService.java 🔗

@@ -2,37 +2,38 @@ package eu.siacs.conversations.services;
 
 import android.annotation.TargetApi;
 import android.content.Intent;
-import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
 import android.graphics.Bitmap;
-import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
+import android.os.PersistableBundle;
 import android.util.Log;
-
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.content.pm.ShortcutManagerCompat;
 import androidx.core.graphics.drawable.IconCompat;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.MucOptions;
+import eu.siacs.conversations.ui.ConversationsActivity;
 import eu.siacs.conversations.ui.StartConversationActivity;
 import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 import eu.siacs.conversations.xmpp.Jid;
+import java.util.Collection;
+import java.util.List;
 
 public class ShortcutService {
 
     private final XmppConnectionService xmppConnectionService;
-    private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName());
+    private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor =
+            new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName());
 
-    public ShortcutService(XmppConnectionService xmppConnectionService) {
+    public ShortcutService(final XmppConnectionService xmppConnectionService) {
         this.xmppConnectionService = xmppConnectionService;
     }
 
@@ -42,12 +43,7 @@ public class ShortcutService {
 
     public void refresh(final boolean forceUpdate) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
-            final Runnable r = new Runnable() {
-                @Override
-                public void run() {
-                    refreshImpl(forceUpdate);
-                }
-            };
+            final Runnable r = () -> refreshImpl(forceUpdate);
             replacingSerialSingleThreadExecutor.execute(r);
         }
     }
@@ -55,87 +51,88 @@ public class ShortcutService {
     @TargetApi(25)
     public void report(Contact contact) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
-            ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class);
+            ShortcutManager shortcutManager =
+                    xmppConnectionService.getSystemService(ShortcutManager.class);
             shortcutManager.reportShortcutUsed(getShortcutId(contact));
         }
     }
 
     @TargetApi(25)
-    private void refreshImpl(boolean forceUpdate) {
-        List<FrequentContact> frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30);
-        HashMap<String,Account> accounts = new HashMap<>();
-        for(Account account : xmppConnectionService.getAccounts()) {
-            accounts.put(account.getUuid(),account);
-        }
-        List<Contact> contacts = new ArrayList<>();
-        for(FrequentContact frequentContact : frequentContacts) {
-            Account account = accounts.get(frequentContact.account);
+    private void refreshImpl(final boolean forceUpdate) {
+        final var frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30);
+        final var accounts =
+                ImmutableMap.copyOf(
+                        Maps.uniqueIndex(xmppConnectionService.getAccounts(), Account::getUuid));
+        final var contactBuilder = new ImmutableMap.Builder<FrequentContact, Contact>();
+        for (final var frequentContact : frequentContacts) {
+            final Account account = accounts.get(frequentContact.account);
             if (account != null) {
-                contacts.add(account.getRoster().getContact(frequentContact.contact));
+                final var contact = account.getRoster().getContact(frequentContact.contact);
+                contactBuilder.put(frequentContact, contact);
             }
         }
-        ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class);
-        boolean needsUpdate = forceUpdate || contactsChanged(contacts,shortcutManager.getDynamicShortcuts());
+        final var contacts = contactBuilder.build();
+        final var current = ShortcutManagerCompat.getDynamicShortcuts(xmppConnectionService);
+        boolean needsUpdate = forceUpdate || contactsChanged(contacts.values(), current);
         if (!needsUpdate) {
-            Log.d(Config.LOGTAG,"skipping shortcut update");
+            Log.d(Config.LOGTAG, "skipping shortcut update");
             return;
         }
-        List<ShortcutInfo> newDynamicShortCuts = new ArrayList<>();
-        for (Contact contact : contacts) {
-            ShortcutInfo shortcut = getShortcutInfo(contact);
-            newDynamicShortCuts.add(shortcut);
+        final var newDynamicShortcuts = new ImmutableList.Builder<ShortcutInfoCompat>();
+        for (final var entry : contacts.entrySet()) {
+            final var contact = entry.getValue();
+            final var conversation = entry.getKey().conversation;
+            final var shortcut = getShortcutInfo(contact, conversation);
+            newDynamicShortcuts.add(shortcut);
         }
-        if (shortcutManager.setDynamicShortcuts(newDynamicShortCuts)) {
-            Log.d(Config.LOGTAG,"updated dynamic shortcuts");
+        if (ShortcutManagerCompat.setDynamicShortcuts(
+                xmppConnectionService, newDynamicShortcuts.build())) {
+            Log.d(Config.LOGTAG, "updated dynamic shortcuts");
         } else {
             Log.d(Config.LOGTAG, "unable to update dynamic shortcuts");
         }
     }
 
-    public ShortcutInfoCompat getShortcutInfoCompat(final Contact contact) {
+    public ShortcutInfoCompat getShortcutInfo(final Contact contact, final String conversation) {
         final ShortcutInfoCompat.Builder builder =
                 new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact))
                         .setShortLabel(contact.getDisplayName())
                         .setIntent(getShortcutIntent(contact))
                         .setIsConversation();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            builder.setIcon(
-                    IconCompat.createFromIcon(
-                            xmppConnectionService,
-                            Icon.createWithBitmap(
-                                    xmppConnectionService
-                                            .getAvatarService()
-                                            .getRoundedShortcut(contact))));
+        builder.setIcon(
+                IconCompat.createWithBitmap(
+                        xmppConnectionService.getAvatarService().getRoundedShortcut(contact)));
+        if (conversation != null) {
+            setConversation(builder, conversation);
         }
         return builder.build();
     }
 
-    public ShortcutInfoCompat getShortcutInfoCompat(final MucOptions mucOptions) {
+    public ShortcutInfoCompat getShortcutInfo(final MucOptions mucOptions) {
         final ShortcutInfoCompat.Builder builder =
                 new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions))
                         .setShortLabel(mucOptions.getConversation().getName())
                         .setIntent(getShortcutIntent(mucOptions))
                         .setIsConversation();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            builder.setIcon(
-                    IconCompat.createFromIcon(
-                            xmppConnectionService,
-                            Icon.createWithBitmap(
-                                    xmppConnectionService
-                                            .getAvatarService()
-                                            .getRoundedShortcut(mucOptions))));
-        }
+        builder.setIcon(
+                IconCompat.createWithBitmap(
+                        xmppConnectionService.getAvatarService().getRoundedShortcut(mucOptions)));
+        setConversation(builder, mucOptions.getConversation().getUuid());
         return builder.build();
     }
 
-    @TargetApi(Build.VERSION_CODES.N_MR1)
-    private ShortcutInfo getShortcutInfo(final Contact contact) {
-        return getShortcutInfoCompat(contact).toShortcutInfo();
+    private static void setConversation(
+            final ShortcutInfoCompat.Builder builder, @NonNull final String conversation) {
+        builder.setCategories(ImmutableSet.of("eu.siacs.conversations.category.SHARE_TARGET"));
+        final var extras = new PersistableBundle();
+        extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation);
+        builder.setExtras(extras);
     }
 
-    private static boolean contactsChanged(List<Contact> needles, List<ShortcutInfo> haystack) {
-        for(Contact needle : needles) {
-            if(!contactExists(needle,haystack)) {
+    private static boolean contactsChanged(
+            final Collection<Contact> needles, final List<ShortcutInfoCompat> haystack) {
+        for (final Contact needle : needles) {
+            if (!contactExists(needle, haystack)) {
                 return true;
             }
         }
@@ -143,17 +140,22 @@ public class ShortcutService {
     }
 
     @TargetApi(25)
-    private static boolean contactExists(Contact needle, List<ShortcutInfo> haystack) {
-        for(ShortcutInfo shortcutInfo : haystack) {
-            if (getShortcutId(needle).equals(shortcutInfo.getId()) && needle.getDisplayName().equals(shortcutInfo.getShortLabel())) {
+    private static boolean contactExists(
+            final Contact needle, final List<ShortcutInfoCompat> haystack) {
+        for (final ShortcutInfoCompat shortcutInfo : haystack) {
+            final var label = shortcutInfo.getShortLabel();
+            if (getShortcutId(needle).equals(shortcutInfo.getId())
+                    && needle.getDisplayName().equals(label.toString())) {
                 return true;
             }
         }
         return false;
     }
 
-    private static String getShortcutId(Contact contact) {
-        return contact.getAccount().getJid().asBareJid().toEscapedString()+"#"+contact.getJid().asBareJid().toEscapedString();
+    private static String getShortcutId(final Contact contact) {
+        return contact.getAccount().getJid().asBareJid().toEscapedString()
+                + "#"
+                + contact.getJid().asBareJid().toEscapedString();
     }
 
     private static String getShortcutId(final MucOptions mucOptions) {
@@ -194,12 +196,15 @@ public class ShortcutService {
     }
 
     @NonNull
-    public Intent createShortcut(Contact contact, boolean legacy) {
+    public Intent createShortcut(final Contact contact, final boolean legacy) {
         Intent intent;
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !legacy) {
-            ShortcutInfo shortcut = getShortcutInfo(contact);
-            ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class);
-            intent = shortcutManager.createShortcutResultIntent(shortcut);
+            final var conversation = xmppConnectionService.find(contact);
+            final var uuid = conversation == null ? null : conversation.getUuid();
+            final var shortcut = getShortcutInfo(contact, uuid);
+            intent =
+                    ShortcutManagerCompat.createShortcutResultIntent(
+                            xmppConnectionService, shortcut);
         } else {
             intent = createShortcutResultIntent(contact);
         }
@@ -207,7 +212,7 @@ public class ShortcutService {
     }
 
     @NonNull
-    private Intent createShortcutResultIntent(Contact contact) {
+    private Intent createShortcutResultIntent(final Contact contact) {
         AvatarService avatarService = xmppConnectionService.getAvatarService();
         Bitmap icon = avatarService.getRoundedShortcutWithIcon(contact);
         Intent intent = new Intent();
@@ -218,13 +223,14 @@ public class ShortcutService {
     }
 
     public static class FrequentContact {
+        private final String conversation;
         private final String account;
         private final Jid contact;
 
-        public FrequentContact(String account, Jid contact) {
+        public FrequentContact(final String conversation, final String account, final Jid contact) {
+            this.conversation = conversation;
             this.account = account;
             this.contact = contact;
         }
     }
-
 }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -55,8 +55,10 @@ import com.google.common.base.Objects;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -147,7 +149,6 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
@@ -253,12 +254,13 @@ public class XmppConnectionService extends Service {
     private final MessageGenerator mMessageGenerator = new MessageGenerator(this);
     public OnContactStatusChanged onContactStatusChanged =
             (contact, online) -> {
-                Conversation conversation = find(getConversations(), contact);
-                if (conversation != null) {
-                    if (online) {
-                        if (contact.getPresences().size() == 1) {
-                            sendUnsentMessages(conversation);
-                        }
+                final var conversation = find(contact);
+                if (conversation == null) {
+                    return;
+                }
+                if (online) {
+                    if (contact.getPresences().size() == 1) {
+                        sendUnsentMessages(conversation);
                     }
                 }
             };
@@ -998,16 +1000,17 @@ public class XmppConnectionService extends Service {
         }
         if (pingNow) {
             for (final Account account : pingCandidates) {
+                final var connection = account.getXmppConnection();
                 final boolean lowTimeout = isInLowPingTimeoutMode(account);
-                account.getXmppConnection().sendPing();
+                final var delta =
+                        (SystemClock.elapsedRealtime() - connection.getLastPacketReceived())
+                                / 1000L;
+                connection.sendPing();
                 Log.d(
                         Config.LOGTAG,
-                        account.getJid().asBareJid()
-                                + " send ping (action="
-                                + action
-                                + ",lowTimeout="
-                                + lowTimeout
-                                + ")");
+                        String.format(
+                                "%s: send ping (action=%s,lowTimeout=%s,interval=%s)",
+                                account.getJid().asBareJid(), action, lowTimeout, delta));
                 scheduleWakeUpCall(
                         lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT,
                         account.getUuid().hashCode());
@@ -1485,7 +1488,7 @@ public class XmppConnectionService extends Service {
                 ContextCompat.RECEIVER_EXPORTED);
         mForceDuringOnCreate.set(false);
         toggleForegroundService();
-        internalPingExecutor.scheduleAtFixedRate(
+        internalPingExecutor.scheduleWithFixedDelay(
                 this::manageAccountConnectionStatesInternal, 10, 10, TimeUnit.SECONDS);
         final SharedPreferences sharedPreferences =
                 androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
@@ -2428,10 +2431,8 @@ public class XmppConnectionService extends Service {
 
     private void restoreFromDatabase() {
         synchronized (this.conversations) {
-            final Map<String, Account> accountLookupTable = new Hashtable<>();
-            for (Account account : this.accounts) {
-                accountLookupTable.put(account.getUuid(), account);
-            }
+            final Map<String, Account> accountLookupTable =
+                    ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
             Log.d(Config.LOGTAG, "restoring conversations...");
             final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
             this.conversations.addAll(
@@ -2735,8 +2736,8 @@ public class XmppConnectionService extends Service {
         return results;
     }
 
-    public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
-        for (final Conversation conversation : haystack) {
+    public Conversation find(final Contact contact) {
+        for (final Conversation conversation : this.conversations) {
             if (conversation.getContact() == contact) {
                 return conversation;
             }
@@ -2798,27 +2799,19 @@ public class XmppConnectionService extends Service {
             final MessageArchiveService.Query query,
             final boolean async) {
         synchronized (this.conversations) {
-            Conversation conversation = find(account, jid);
-            if (conversation != null) {
-                return conversation;
+            final var cached = find(account, jid);
+            if (cached != null) {
+                return cached;
             }
-            conversation = databaseBackend.findConversation(account, jid);
+            final var existing = databaseBackend.findConversation(account, jid);
+            final Conversation conversation;
             final boolean loadMessagesFromDb;
-            if (conversation != null) {
-                conversation.setStatus(Conversation.STATUS_AVAILABLE);
-                conversation.setAccount(account);
-                if (muc) {
-                    conversation.setMode(Conversation.MODE_MULTI);
-                    conversation.setContactJid(jid);
-                } else {
-                    conversation.setMode(Conversation.MODE_SINGLE);
-                    conversation.setContactJid(jid.asBareJid());
-                }
-                databaseBackend.updateConversation(conversation);
-                loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false);
+            if (existing != null) {
+                conversation = existing;
+                loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
             } else {
                 String conversationName;
-                Contact contact = account.getRoster().getContact(jid);
+                final Contact contact = account.getRoster().getContact(jid);
                 if (contact != null) {
                     conversationName = contact.getDisplayName();
                 } else {
@@ -2839,35 +2832,13 @@ public class XmppConnectionService extends Service {
                 this.databaseBackend.createConversation(conversation);
                 loadMessagesFromDb = false;
             }
-            final Conversation c = conversation;
-            final Runnable runnable =
-                    () -> {
-                        if (loadMessagesFromDb) {
-                            c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
-                            updateConversationUi();
-                            c.messagesLoaded.set(true);
-                        }
-                        if (account.getXmppConnection() != null
-                                && !c.getContact().isBlocked()
-                                && account.getXmppConnection().getFeatures().mam()
-                                && !muc) {
-                            if (query == null) {
-                                mMessageArchiveService.query(c);
-                            } else {
-                                if (query.getConversation() == null) {
-                                    mMessageArchiveService.query(
-                                            c, query.getStart(), query.isCatchup());
-                                }
-                            }
-                        }
-                        if (joinAfterCreate) {
-                            joinMuc(c);
-                        }
-                    };
             if (async) {
-                mDatabaseReaderExecutor.execute(runnable);
+                mDatabaseReaderExecutor.execute(
+                        () ->
+                                postProcessConversation(
+                                        conversation, loadMessagesFromDb, joinAfterCreate, query));
             } else {
-                runnable.run();
+                postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
             }
             this.conversations.add(conversation);
             updateConversationUi();
@@ -2875,6 +2846,84 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    public Conversation findConversationByUuidReliable(final String uuid) {
+        final var cached = findConversationByUuid(uuid);
+        if (cached != null) {
+            return cached;
+        }
+        final var existing = databaseBackend.findConversation(uuid);
+        if (existing == null) {
+            return null;
+        }
+        Log.d(
+                Config.LOGTAG,
+                existing.getJid().asBareJid()
+                        + ": restoring conversation with "
+                        + existing.getJid()
+                        + " from DB");
+        final Map<String, Account> accounts =
+                ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
+        existing.setAccount(accounts.get(existing.getAccountUuid()));
+        final var loadMessagesFromDb = restoreFromArchive(existing);
+        mDatabaseReaderExecutor.execute(
+                () ->
+                        postProcessConversation(
+                                existing,
+                                loadMessagesFromDb,
+                                existing.getMode() == Conversational.MODE_MULTI,
+                                null));
+        this.conversations.add(existing);
+        updateConversationUi();
+        return existing;
+    }
+
+    private boolean restoreFromArchive(
+            final Conversation conversation, final Jid jid, final boolean muc) {
+        if (muc) {
+            conversation.setMode(Conversation.MODE_MULTI);
+            conversation.setContactJid(jid);
+        } else {
+            conversation.setMode(Conversation.MODE_SINGLE);
+            conversation.setContactJid(jid.asBareJid());
+        }
+        return restoreFromArchive(conversation);
+    }
+
+    private boolean restoreFromArchive(final Conversation conversation) {
+        conversation.setStatus(Conversation.STATUS_AVAILABLE);
+        databaseBackend.updateConversation(conversation);
+        return conversation.messagesLoaded.compareAndSet(true, false);
+    }
+
+    private void postProcessConversation(
+            final Conversation c,
+            final boolean loadMessagesFromDb,
+            final boolean joinAfterCreate,
+            final MessageArchiveService.Query query) {
+        final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
+        final var account = c.getAccount();
+        if (loadMessagesFromDb) {
+            c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
+            updateConversationUi();
+            c.messagesLoaded.set(true);
+        }
+        if (account.getXmppConnection() != null
+                && !c.getContact().isBlocked()
+                && account.getXmppConnection().getFeatures().mam()
+                && singleMode) {
+            if (query == null) {
+                mMessageArchiveService.query(c);
+            } else {
+                if (query.getConversation() == null) {
+                    mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
+                }
+            }
+        }
+        if (joinAfterCreate) {
+            joinMuc(c);
+        }
+    }
+
     public void archiveConversation(Conversation conversation) {
         archiveConversation(conversation, true);
     }

src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java 🔗

@@ -29,7 +29,6 @@
 
 package eu.siacs.conversations.ui;
 
-
 import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
 
 import android.Manifest;
@@ -51,22 +50,13 @@ import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.widget.Toast;
-
 import androidx.annotation.IdRes;
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AlertDialog;
 import androidx.core.app.ActivityCompat;
 import androidx.databinding.DataBindingUtil;
-
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.openintents.openpgp.util.OpenPgpApi;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.OmemoSetting;
@@ -90,8 +80,22 @@ import eu.siacs.conversations.utils.SignupUtils;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.openintents.openpgp.util.OpenPgpApi;
 
-public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {
+public class ConversationsActivity extends XmppActivity
+        implements OnConversationSelected,
+                OnConversationArchived,
+                OnConversationsListItemUpdated,
+                OnConversationRead,
+                XmppConnectionService.OnAccountUpdate,
+                XmppConnectionService.OnConversationUpdate,
+                XmppConnectionService.OnRosterUpdate,
+                OnUpdateBlocklist,
+                XmppConnectionService.OnShowErrorToast,
+                XmppConnectionService.OnAffiliationChanged {
 
     public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
     public static final String EXTRA_CONVERSATION = "conversationUuid";
@@ -104,19 +108,18 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     public static final String POST_ACTION_RECORD_VOICE = "record_voice";
     public static final String EXTRA_TYPE = "type";
 
-    private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
-            ACTION_VIEW_CONVERSATION,
-            Intent.ACTION_SEND,
-            Intent.ACTION_SEND_MULTIPLE
-    );
+    private static final List<String> VIEW_AND_SHARE_ACTIONS =
+            Arrays.asList(
+                    ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE);
 
     public static final int REQUEST_OPEN_MESSAGE = 0x9876;
     public static final int REQUEST_PLAY_PAUSE = 0x5432;
 
-
-    //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment
-    private static final @IdRes
-    int[] FRAGMENT_ID_NOTIFICATION_ORDER = {R.id.secondary_fragment, R.id.main_fragment};
+    // secondary fragment (when holding the conversation, must be initialized before refreshing the
+    // overview fragment
+    private static final @IdRes int[] FRAGMENT_ID_NOTIFICATION_ORDER = {
+        R.id.secondary_fragment, R.id.main_fragment
+    };
     private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
     private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
     private ActivityConversationsBinding binding;
@@ -125,7 +128,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
 
     private static boolean isViewOrShareIntent(Intent i) {
         Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction()));
-        return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION);
+        return i != null
+                && VIEW_AND_SHARE_ACTIONS.contains(i.getAction())
+                && i.hasExtra(EXTRA_CONVERSATION);
     }
 
     private static Intent createLauncherIntent(Context context) {
@@ -169,7 +174,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         }
 
         invalidateActionBarTitle();
-        if (binding.secondaryFragment != null && ConversationFragment.getConversation(this) == null) {
+        if (binding.secondaryFragment != null
+                && ConversationFragment.getConversation(this) == null) {
             Conversation conversation = ConversationsOverviewFragment.getSuggestion(this);
             if (conversation != null) {
                 openConversation(conversation, null);
@@ -182,7 +188,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         return performRedirectIfNecessary(null, noAnimation);
     }
 
-    private boolean performRedirectIfNecessary(final Conversation ignore, final boolean noAnimation) {
+    private boolean performRedirectIfNecessary(
+            final Conversation ignore, final boolean noAnimation) {
         if (xmppConnectionService == null) {
             return false;
         }
@@ -192,12 +199,13 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
             if (noAnimation) {
                 intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
             }
-            runOnUiThread(() -> {
-                startActivity(intent);
-                if (noAnimation) {
-                    overridePendingTransition(0, 0);
-                }
-            });
+            runOnUiThread(
+                    () -> {
+                        startActivity(intent);
+                        if (noAnimation) {
+                            overridePendingTransition(0, 0);
+                        }
+                    });
         }
         return mRedirectInProcess.get();
     }
@@ -219,7 +227,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     private String getBatteryOptimizationPreferenceKey() {
-        @SuppressLint("HardwareIds") String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
+        @SuppressLint("HardwareIds")
+        String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
         return "show_battery_optimization" + (device == null ? "" : device);
     }
 
@@ -228,20 +237,31 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     private boolean openBatteryOptimizationDialogIfNeeded() {
-        if (isOptimizingBattery() && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
+        if (isOptimizingBattery()
+                && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
             final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
             builder.setTitle(R.string.battery_optimizations_enabled);
-            builder.setMessage(getString(R.string.battery_optimizations_enabled_dialog, getString(R.string.app_name)));
-            builder.setPositiveButton(R.string.next, (dialog, which) -> {
-                final Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
-                final Uri uri = Uri.parse("package:" + getPackageName());
-                intent.setData(uri);
-                try {
-                    startActivityForResult(intent, REQUEST_BATTERY_OP);
-                } catch (final ActivityNotFoundException e) {
-                    Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
-                }
-            });
+            builder.setMessage(
+                    getString(
+                            R.string.battery_optimizations_enabled_dialog,
+                            getString(R.string.app_name)));
+            builder.setPositiveButton(
+                    R.string.next,
+                    (dialog, which) -> {
+                        final Intent intent =
+                                new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
+                        final Uri uri = Uri.parse("package:" + getPackageName());
+                        intent.setData(uri);
+                        try {
+                            startActivityForResult(intent, REQUEST_BATTERY_OP);
+                        } catch (final ActivityNotFoundException e) {
+                            Toast.makeText(
+                                            this,
+                                            R.string.device_does_not_support_battery_op,
+                                            Toast.LENGTH_SHORT)
+                                    .show();
+                        }
+                    });
             builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
             final AlertDialog dialog = builder.create();
             dialog.setCanceledOnTouchOutside(false);
@@ -252,8 +272,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     private void requestNotificationPermissionIfNeeded() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
-            requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_POST_NOTIFICATION);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+                && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+                        != PackageManager.PERMISSION_GRANTED) {
+            requestPermissions(
+                    new String[] {Manifest.permission.POST_NOTIFICATIONS},
+                    REQUEST_POST_NOTIFICATION);
         }
     }
 
@@ -271,9 +295,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         }
     }
 
-    private boolean processViewIntent(Intent intent) {
+    private boolean processViewIntent(final Intent intent) {
         final String uuid = intent.getStringExtra(EXTRA_CONVERSATION);
-        final Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null;
+        final Conversation conversation =
+                uuid != null ? xmppConnectionService.findConversationByUuidReliable(uuid) : null;
         if (conversation == null) {
             Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid);
             return false;
@@ -283,7 +308,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+    public void onRequestPermissionsResult(
+            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
         if (grantResults.length > 0) {
@@ -397,8 +423,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         if (qrCodeScanMenuItem != null) {
             if (isCameraFeatureAvailable()) {
                 Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
-                boolean visible = getResources().getBoolean(R.bool.show_qr_code_scan)
-                        && fragment instanceof ConversationsOverviewFragment;
+                boolean visible =
+                        getResources().getBoolean(R.bool.show_qr_code_scan)
+                                && fragment instanceof ConversationsOverviewFragment;
                 qrCodeScanMenuItem.setVisible(visible);
             } else {
                 qrCodeScanMenuItem.setVisible(false);
@@ -411,7 +438,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     public void onConversationSelected(Conversation conversation) {
         clearPendingViewIntent();
         if (ConversationFragment.getConversation(this) == conversation) {
-            Log.d(Config.LOGTAG, "ignore onConversationSelected() because conversation is already open");
+            Log.d(
+                    Config.LOGTAG,
+                    "ignore onConversationSelected() because conversation is already open");
             return;
         }
         openConversation(conversation, null);
@@ -424,13 +453,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     private void displayToast(final String msg) {
-        runOnUiThread(() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
+        runOnUiThread(
+                () -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
     }
 
     @Override
-    public void onAffiliationChangedSuccessful(Jid jid) {
-
-    }
+    public void onAffiliationChangedSuccessful(Jid jid) {}
 
     @Override
     public void onAffiliationChangeFailed(Jid jid, int resId) {
@@ -440,7 +468,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     private void openConversation(Conversation conversation, Bundle extras) {
         final FragmentManager fragmentManager = getFragmentManager();
         executePendingTransactions(fragmentManager);
-        ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
+        ConversationFragment conversationFragment =
+                (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
         final boolean mainNeedsRefresh;
         if (conversationFragment == null) {
             mainNeedsRefresh = false;
@@ -456,7 +485,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                     fragmentTransaction.commit();
                 } catch (IllegalStateException e) {
                     Log.w(Config.LOGTAG, "sate loss while opening conversation", e);
-                    //allowing state loss is probably fine since view intents et all are already stored and a click can probably be 'ignored'
+                    // allowing state loss is probably fine since view intents et all are already
+                    // stored and a click can probably be 'ignored'
                     return;
                 }
             }
@@ -474,14 +504,15 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         try {
             fragmentManager.executePendingTransactions();
         } catch (final Exception e) {
-            Log.e(Config.LOGTAG,"unable to execute pending fragment transactions");
+            Log.e(Config.LOGTAG, "unable to execute pending fragment transactions");
         }
     }
 
     public boolean onXmppUriClicked(Uri uri) {
         XmppUri xmppUri = new XmppUri(uri);
         if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
-            final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri);
+            final Conversation conversation =
+                    xmppConnectionService.findUniqueConversationByJid(xmppUri);
             if (conversation != null) {
                 openConversation(conversation, null);
                 return true;
@@ -540,7 +571,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     @Override
     public void onSaveInstanceState(final Bundle savedInstanceState) {
         final Intent pendingIntent = pendingViewIntent.peek();
-        savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
+        savedInstanceState.putParcelable(
+                "intent", pendingIntent != null ? pendingIntent : getIntent());
         super.onSaveInstanceState(savedInstanceState);
     }
 
@@ -580,7 +612,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
         final FragmentManager fragmentManager = getFragmentManager();
         FragmentTransaction transaction = fragmentManager.beginTransaction();
         final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
-        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
+        final Fragment secondaryFragment =
+                fragmentManager.findFragmentById(R.id.secondary_fragment);
         if (mainFragment != null) {
             if (binding.secondaryFragment != null) {
                 if (mainFragment instanceof ConversationFragment) {
@@ -628,13 +661,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                 actionBar.setTitle(conversation.getName());
                 actionBar.setDisplayHomeAsUpEnabled(true);
                 ToolbarUtils.setActionBarOnClickListener(
-                        binding.toolbar,
-                        (v) -> openConversationDetails(conversation)
-                );
+                        binding.toolbar, (v) -> openConversationDetails(conversation));
                 return;
             }
         }
-        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
+        final Fragment secondaryFragment =
+                fragmentManager.findFragmentById(R.id.secondary_fragment);
         if (secondaryFragment instanceof ConversationFragment conversationFragment) {
             final Conversation conversation = conversationFragment.getConversation();
             if (conversation != null) {
@@ -673,15 +705,21 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
             try {
                 fragmentManager.popBackStack();
             } catch (final IllegalStateException e) {
-                Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e);
-                //this usually means activity is no longer active; meaning on the next open we will run through this again
+                Log.w(
+                        Config.LOGTAG,
+                        "state loss while popping back state after archiving conversation",
+                        e);
+                // this usually means activity is no longer active; meaning on the next open we will
+                // run through this again
             }
             return;
         }
-        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
+        final Fragment secondaryFragment =
+                fragmentManager.findFragmentById(R.id.secondary_fragment);
         if (secondaryFragment instanceof ConversationFragment) {
             if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
-                Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);
+                Conversation suggestion =
+                        ConversationsOverviewFragment.getSuggestion(this, conversation);
                 if (suggestion != null) {
                     openConversation(suggestion, null);
                 }

src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java 🔗

@@ -8,11 +8,11 @@ import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.widget.Toast;
-
 import androidx.annotation.NonNull;
+import androidx.core.content.pm.ShortcutManagerCompat;
 import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.LinearLayoutManager;
-
+import com.google.common.collect.Iterables;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityShareWithBinding;
@@ -21,7 +21,6 @@ import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.adapter.ConversationAdapter;
 import eu.siacs.conversations.xmpp.Jid;
-
 import java.util.ArrayList;
 import java.util.List;
 
@@ -112,7 +111,34 @@ public class ShareWithActivity extends XmppActivity
                 new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
         binding.chooseConversationList.setAdapter(mAdapter);
         mAdapter.setConversationClickListener((view, conversation) -> share(conversation));
+        final var intent = getIntent();
+        final var shortcutId = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID);
         this.share = new Share();
+        if (shortcutId != null) {
+            final var conversation = shortcutIdToConversation(shortcutId);
+            if (conversation != null) {
+                // we have everything we need. Jump into chat
+                populateShare(intent);
+                share(conversation);
+            }
+        }
+    }
+
+    private String shortcutIdToConversation(final String shortcutId) {
+        final var shortcut =
+                Iterables.tryFind(
+                        ShortcutManagerCompat.getDynamicShortcuts(this),
+                        si -> si.getId().equals(shortcutId));
+        if (shortcut.isPresent()) {
+            final var extras = shortcut.get().getExtras();
+            if (extras == null) {
+                return null;
+            } else {
+                return extras.getString(ConversationsActivity.EXTRA_CONVERSATION);
+            }
+        } else {
+            return null;
+        }
     }
 
     @Override
@@ -137,10 +163,18 @@ public class ShareWithActivity extends XmppActivity
     @Override
     public void onStart() {
         super.onStart();
-        Intent intent = getIntent();
+        final Intent intent = getIntent();
         if (intent == null) {
             return;
         }
+        populateShare(intent);
+        if (xmppConnectionServiceBound) {
+            xmppConnectionService.populateWithOrderedConversations(
+                    mConversations, this.share.uris.isEmpty(), false);
+        }
+    }
+
+    private void populateShare(final Intent intent) {
         final String type = intent.getType();
         final String action = intent.getAction();
         final Uri data = intent.getData();
@@ -165,10 +199,6 @@ public class ShareWithActivity extends XmppActivity
             final ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
             this.share.uris = uris == null ? new ArrayList<>() : uris;
         }
-        if (xmppConnectionServiceBound) {
-            xmppConnectionService.populateWithOrderedConversations(
-                    mConversations, this.share.uris.isEmpty(), false);
-        }
     }
 
     @Override
@@ -209,8 +239,12 @@ public class ShareWithActivity extends XmppActivity
             mPendingConversation = conversation;
             return;
         }
+        share(conversation.getUuid());
+    }
+
+    private void share(final String conversation) {
         final Intent intent = new Intent(this, ConversationsActivity.class);
-        intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
+        intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation);
         if (!share.uris.isEmpty()) {
             intent.setAction(Intent.ACTION_SEND_MULTIPLE);
             intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris);
@@ -225,7 +259,7 @@ public class ShareWithActivity extends XmppActivity
         }
         try {
             startActivity(intent);
-        } catch (SecurityException e) {
+        } catch (final SecurityException e) {
             Toast.makeText(
                             this,
                             R.string.sharing_application_not_grant_permission,

src/main/res/xml/shortcuts.xml 🔗

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+    <share-target android:targetClass="eu.siacs.conversations.ui.ShareWithActivity">
+        <data android:mimeType="*/*" />
+        <category android:name="eu.siacs.conversations.category.SHARE_TARGET" />
+    </share-target>
+</shortcuts>