DatabaseBackend.java

   1package eu.siacs.conversations.persistance;
   2
   3import android.content.ContentValues;
   4import android.content.Context;
   5import android.database.Cursor;
   6import android.database.DatabaseUtils;
   7import android.database.sqlite.SQLiteDatabase;
   8import android.database.sqlite.SQLiteOpenHelper;
   9import android.text.TextUtils;
  10import android.os.Build;
  11import android.os.Environment;
  12import android.os.SystemClock;
  13import android.util.Base64;
  14import android.util.Log;
  15
  16import com.cheogram.android.WebxdcUpdate;
  17
  18import com.google.common.base.Stopwatch;
  19import com.google.common.collect.Multimap;
  20import com.google.common.collect.HashMultimap;
  21
  22import org.json.JSONException;
  23import org.json.JSONObject;
  24import org.whispersystems.libsignal.IdentityKey;
  25import org.whispersystems.libsignal.IdentityKeyPair;
  26import org.whispersystems.libsignal.InvalidKeyException;
  27import org.whispersystems.libsignal.SignalProtocolAddress;
  28import org.whispersystems.libsignal.state.PreKeyRecord;
  29import org.whispersystems.libsignal.state.SessionRecord;
  30import org.whispersystems.libsignal.state.SignedPreKeyRecord;
  31
  32import java.io.ByteArrayInputStream;
  33import java.io.File;
  34import java.io.IOException;
  35import java.security.cert.CertificateEncodingException;
  36import java.security.cert.CertificateException;
  37import java.security.cert.CertificateFactory;
  38import java.security.cert.X509Certificate;
  39import java.util.ArrayList;
  40import java.util.Collection;
  41import java.util.HashMap;
  42import java.util.Hashtable;
  43import java.util.HashSet;
  44import java.util.List;
  45import java.util.Map;
  46import java.util.Set;
  47import java.util.UUID;
  48import java.util.concurrent.CopyOnWriteArrayList;
  49
  50import io.ipfs.cid.Cid;
  51
  52import eu.siacs.conversations.Config;
  53import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  54import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  55import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
  56import eu.siacs.conversations.entities.Account;
  57import eu.siacs.conversations.entities.Contact;
  58import eu.siacs.conversations.entities.Conversation;
  59import eu.siacs.conversations.entities.DownloadableFile;
  60import eu.siacs.conversations.entities.Message;
  61import eu.siacs.conversations.entities.MucOptions;
  62import eu.siacs.conversations.entities.PresenceTemplate;
  63import eu.siacs.conversations.entities.Roster;
  64import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  65import eu.siacs.conversations.services.QuickConversationsService;
  66import eu.siacs.conversations.services.ShortcutService;
  67import eu.siacs.conversations.utils.CryptoHelper;
  68import eu.siacs.conversations.utils.CursorUtils;
  69import eu.siacs.conversations.utils.FtsUtils;
  70import eu.siacs.conversations.utils.MimeUtils;
  71import eu.siacs.conversations.utils.Resolver;
  72import eu.siacs.conversations.xmpp.Jid;
  73import eu.siacs.conversations.xmpp.mam.MamReference;
  74import java.io.ByteArrayInputStream;
  75import java.io.File;
  76import java.io.IOException;
  77import java.security.cert.CertificateEncodingException;
  78import java.security.cert.CertificateException;
  79import java.security.cert.CertificateFactory;
  80import java.security.cert.X509Certificate;
  81import java.util.ArrayList;
  82import java.util.HashMap;
  83import java.util.HashSet;
  84import java.util.List;
  85import java.util.Map;
  86import java.util.Set;
  87import java.util.UUID;
  88import java.util.concurrent.CopyOnWriteArrayList;
  89import org.json.JSONException;
  90import org.json.JSONObject;
  91import org.whispersystems.libsignal.IdentityKey;
  92import org.whispersystems.libsignal.IdentityKeyPair;
  93import org.whispersystems.libsignal.InvalidKeyException;
  94import org.whispersystems.libsignal.SignalProtocolAddress;
  95import org.whispersystems.libsignal.state.PreKeyRecord;
  96import org.whispersystems.libsignal.state.SessionRecord;
  97import org.whispersystems.libsignal.state.SignedPreKeyRecord;
  98
  99public class DatabaseBackend extends SQLiteOpenHelper {
 100
 101    private static final String DATABASE_NAME = "history";
 102    private static final int DATABASE_VERSION = 52;
 103
 104    private static boolean requiresMessageIndexRebuild = false;
 105    private static DatabaseBackend instance = null;
 106    private static final String CREATE_CONTATCS_STATEMENT =
 107            "create table "
 108                    + Contact.TABLENAME
 109                    + "("
 110                    + Contact.ACCOUNT
 111                    + " TEXT, "
 112                    + Contact.SERVERNAME
 113                    + " TEXT, "
 114                    + Contact.SYSTEMNAME
 115                    + " TEXT,"
 116                    + Contact.PRESENCE_NAME
 117                    + " TEXT,"
 118                    + Contact.JID
 119                    + " TEXT,"
 120                    + Contact.KEYS
 121                    + " TEXT,"
 122                    + Contact.PHOTOURI
 123                    + " TEXT,"
 124                    + Contact.OPTIONS
 125                    + " NUMBER,"
 126                    + Contact.SYSTEMACCOUNT
 127                    + " NUMBER, "
 128                    + Contact.AVATAR
 129                    + " TEXT, "
 130                    + Contact.LAST_PRESENCE
 131                    + " TEXT, "
 132                    + Contact.LAST_TIME
 133                    + " NUMBER, "
 134                    + Contact.RTP_CAPABILITY
 135                    + " TEXT,"
 136                    + Contact.GROUPS
 137                    + " TEXT, FOREIGN KEY("
 138                    + Contact.ACCOUNT
 139                    + ") REFERENCES "
 140                    + Account.TABLENAME
 141                    + "("
 142                    + Account.UUID
 143                    + ") ON DELETE CASCADE, UNIQUE("
 144                    + Contact.ACCOUNT
 145                    + ", "
 146                    + Contact.JID
 147                    + ") ON CONFLICT REPLACE);";
 148
 149    private static final String CREATE_DISCOVERY_RESULTS_STATEMENT =
 150            "create table "
 151                    + ServiceDiscoveryResult.TABLENAME
 152                    + "("
 153                    + ServiceDiscoveryResult.HASH
 154                    + " TEXT, "
 155                    + ServiceDiscoveryResult.VER
 156                    + " TEXT, "
 157                    + ServiceDiscoveryResult.RESULT
 158                    + " TEXT, "
 159                    + "UNIQUE("
 160                    + ServiceDiscoveryResult.HASH
 161                    + ", "
 162                    + ServiceDiscoveryResult.VER
 163                    + ") ON CONFLICT REPLACE);";
 164
 165    private static final String CREATE_PRESENCE_TEMPLATES_STATEMENT =
 166            "CREATE TABLE "
 167                    + PresenceTemplate.TABELNAME
 168                    + "("
 169                    + PresenceTemplate.UUID
 170                    + " TEXT, "
 171                    + PresenceTemplate.LAST_USED
 172                    + " NUMBER,"
 173                    + PresenceTemplate.MESSAGE
 174                    + " TEXT,"
 175                    + PresenceTemplate.STATUS
 176                    + " TEXT,"
 177                    + "UNIQUE("
 178                    + PresenceTemplate.MESSAGE
 179                    + ","
 180                    + PresenceTemplate.STATUS
 181                    + ") ON CONFLICT REPLACE);";
 182
 183    private static final String CREATE_PREKEYS_STATEMENT =
 184            "CREATE TABLE "
 185                    + SQLiteAxolotlStore.PREKEY_TABLENAME
 186                    + "("
 187                    + SQLiteAxolotlStore.ACCOUNT
 188                    + " TEXT,  "
 189                    + SQLiteAxolotlStore.ID
 190                    + " INTEGER, "
 191                    + SQLiteAxolotlStore.KEY
 192                    + " TEXT, FOREIGN KEY("
 193                    + SQLiteAxolotlStore.ACCOUNT
 194                    + ") REFERENCES "
 195                    + Account.TABLENAME
 196                    + "("
 197                    + Account.UUID
 198                    + ") ON DELETE CASCADE, "
 199                    + "UNIQUE( "
 200                    + SQLiteAxolotlStore.ACCOUNT
 201                    + ", "
 202                    + SQLiteAxolotlStore.ID
 203                    + ") ON CONFLICT REPLACE"
 204                    + ");";
 205
 206    private static final String CREATE_SIGNED_PREKEYS_STATEMENT =
 207            "CREATE TABLE "
 208                    + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME
 209                    + "("
 210                    + SQLiteAxolotlStore.ACCOUNT
 211                    + " TEXT,  "
 212                    + SQLiteAxolotlStore.ID
 213                    + " INTEGER, "
 214                    + SQLiteAxolotlStore.KEY
 215                    + " TEXT, FOREIGN KEY("
 216                    + SQLiteAxolotlStore.ACCOUNT
 217                    + ") REFERENCES "
 218                    + Account.TABLENAME
 219                    + "("
 220                    + Account.UUID
 221                    + ") ON DELETE CASCADE, "
 222                    + "UNIQUE( "
 223                    + SQLiteAxolotlStore.ACCOUNT
 224                    + ", "
 225                    + SQLiteAxolotlStore.ID
 226                    + ") ON CONFLICT REPLACE"
 227                    + ");";
 228
 229    private static final String CREATE_SESSIONS_STATEMENT =
 230            "CREATE TABLE "
 231                    + SQLiteAxolotlStore.SESSION_TABLENAME
 232                    + "("
 233                    + SQLiteAxolotlStore.ACCOUNT
 234                    + " TEXT,  "
 235                    + SQLiteAxolotlStore.NAME
 236                    + " TEXT, "
 237                    + SQLiteAxolotlStore.DEVICE_ID
 238                    + " INTEGER, "
 239                    + SQLiteAxolotlStore.KEY
 240                    + " TEXT, FOREIGN KEY("
 241                    + SQLiteAxolotlStore.ACCOUNT
 242                    + ") REFERENCES "
 243                    + Account.TABLENAME
 244                    + "("
 245                    + Account.UUID
 246                    + ") ON DELETE CASCADE, "
 247                    + "UNIQUE( "
 248                    + SQLiteAxolotlStore.ACCOUNT
 249                    + ", "
 250                    + SQLiteAxolotlStore.NAME
 251                    + ", "
 252                    + SQLiteAxolotlStore.DEVICE_ID
 253                    + ") ON CONFLICT REPLACE"
 254                    + ");";
 255
 256    private static final String CREATE_IDENTITIES_STATEMENT =
 257            "CREATE TABLE "
 258                    + SQLiteAxolotlStore.IDENTITIES_TABLENAME
 259                    + "("
 260                    + SQLiteAxolotlStore.ACCOUNT
 261                    + " TEXT,  "
 262                    + SQLiteAxolotlStore.NAME
 263                    + " TEXT, "
 264                    + SQLiteAxolotlStore.OWN
 265                    + " INTEGER, "
 266                    + SQLiteAxolotlStore.FINGERPRINT
 267                    + " TEXT, "
 268                    + SQLiteAxolotlStore.CERTIFICATE
 269                    + " BLOB, "
 270                    + SQLiteAxolotlStore.TRUST
 271                    + " TEXT, "
 272                    + SQLiteAxolotlStore.ACTIVE
 273                    + " NUMBER, "
 274                    + SQLiteAxolotlStore.LAST_ACTIVATION
 275                    + " NUMBER,"
 276                    + SQLiteAxolotlStore.KEY
 277                    + " TEXT, FOREIGN KEY("
 278                    + SQLiteAxolotlStore.ACCOUNT
 279                    + ") REFERENCES "
 280                    + Account.TABLENAME
 281                    + "("
 282                    + Account.UUID
 283                    + ") ON DELETE CASCADE, "
 284                    + "UNIQUE( "
 285                    + SQLiteAxolotlStore.ACCOUNT
 286                    + ", "
 287                    + SQLiteAxolotlStore.NAME
 288                    + ", "
 289                    + SQLiteAxolotlStore.FINGERPRINT
 290                    + ") ON CONFLICT IGNORE"
 291                    + ");";
 292
 293    private static final String RESOLVER_RESULTS_TABLENAME = "resolver_results";
 294
 295    private static final String CREATE_RESOLVER_RESULTS_TABLE =
 296            "create table "
 297                    + RESOLVER_RESULTS_TABLENAME
 298                    + "("
 299                    + Resolver.Result.DOMAIN
 300                    + " TEXT,"
 301                    + Resolver.Result.HOSTNAME
 302                    + " TEXT,"
 303                    + Resolver.Result.IP
 304                    + " BLOB,"
 305                    + Resolver.Result.PRIORITY
 306                    + " NUMBER,"
 307                    + Resolver.Result.DIRECT_TLS
 308                    + " NUMBER,"
 309                    + Resolver.Result.AUTHENTICATED
 310                    + " NUMBER,"
 311                    + Resolver.Result.PORT
 312                    + " NUMBER,"
 313                    + "UNIQUE("
 314                    + Resolver.Result.DOMAIN
 315                    + ") ON CONFLICT REPLACE"
 316                    + ");";
 317
 318    private static final String CREATE_MESSAGE_TIME_INDEX =
 319            "CREATE INDEX message_time_index ON "
 320                    + Message.TABLENAME
 321                    + "("
 322                    + Message.TIME_SENT
 323                    + ")";
 324    private static final String CREATE_MESSAGE_CONVERSATION_INDEX =
 325            "CREATE INDEX message_conversation_index ON "
 326                    + Message.TABLENAME
 327                    + "("
 328                    + Message.CONVERSATION
 329                    + ")";
 330    private static final String CREATE_MESSAGE_DELETED_INDEX =
 331            "CREATE INDEX message_deleted_index ON "
 332                    + Message.TABLENAME
 333                    + "("
 334                    + Message.DELETED
 335                    + ")";
 336    private static final String CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX =
 337            "CREATE INDEX message_file_path_index ON "
 338                    + Message.TABLENAME
 339                    + "("
 340                    + Message.RELATIVE_FILE_PATH
 341                    + ")";
 342    private static final String CREATE_MESSAGE_TYPE_INDEX =
 343            "CREATE INDEX message_type_index ON " + Message.TABLENAME + "(" + Message.TYPE + ")";
 344
 345    private static final String CREATE_MESSAGE_INDEX_TABLE =
 346            "CREATE VIRTUAL TABLE messages_index USING fts4"
 347                    + " (uuid,body,notindexed=\"uuid\",content=\""
 348                    + Message.TABLENAME
 349                    + "\",tokenize='unicode61')";
 350    private static final String CREATE_MESSAGE_INSERT_TRIGGER =
 351            "CREATE TRIGGER after_message_insert AFTER INSERT ON "
 352                    + Message.TABLENAME
 353                    + " BEGIN INSERT INTO messages_index(rowid,uuid,body)"
 354                    + " VALUES(NEW.rowid,NEW.uuid,NEW.body); END;";
 355    private static final String CREATE_MESSAGE_UPDATE_TRIGGER =
 356            "CREATE TRIGGER after_message_update UPDATE OF uuid,body ON "
 357                    + Message.TABLENAME
 358                    + " BEGIN UPDATE messages_index SET body=NEW.body,uuid=NEW.uuid WHERE"
 359                    + " rowid=OLD.rowid; END;";
 360    private static final String CREATE_MESSAGE_DELETE_TRIGGER =
 361            "CREATE TRIGGER after_message_delete AFTER DELETE ON "
 362                    + Message.TABLENAME
 363                    + " BEGIN DELETE FROM messages_index WHERE rowid=OLD.rowid; END;";
 364    private static final String COPY_PREEXISTING_ENTRIES =
 365            "INSERT INTO messages_index(messages_index) VALUES('rebuild');";
 366
 367    protected Context context;
 368
 369    private DatabaseBackend(Context context) {
 370        super(context, DATABASE_NAME, null, DATABASE_VERSION);
 371        this.context = context;
 372        setWriteAheadLoggingEnabled(true);
 373    }
 374
 375    private static ContentValues createFingerprintStatusContentValues(
 376            FingerprintStatus.Trust trust, boolean active) {
 377        ContentValues values = new ContentValues();
 378        values.put(SQLiteAxolotlStore.TRUST, trust.toString());
 379        values.put(SQLiteAxolotlStore.ACTIVE, active ? 1 : 0);
 380        return values;
 381    }
 382
 383    public static boolean requiresMessageIndexRebuild() {
 384        return requiresMessageIndexRebuild;
 385    }
 386
 387    public void rebuildMessagesIndex() {
 388        final SQLiteDatabase db = getWritableDatabase();
 389        final Stopwatch stopwatch = Stopwatch.createStarted();
 390        db.execSQL(COPY_PREEXISTING_ENTRIES);
 391        Log.d(Config.LOGTAG, "rebuilt message index in " + stopwatch.stop().toString());
 392    }
 393
 394    public static synchronized DatabaseBackend getInstance(Context context) {
 395        if (instance == null) {
 396            instance = new DatabaseBackend(context);
 397        }
 398        return instance;
 399    }
 400
 401    protected void cheogramMigrate(SQLiteDatabase db) {
 402        db.beginTransaction();
 403
 404        try {
 405            Cursor cursor = db.rawQuery("PRAGMA cheogram.user_version", null);
 406            cursor.moveToNext();
 407            int cheogramVersion = cursor.getInt(0);
 408            cursor.close();
 409
 410            if(cheogramVersion < 1) {
 411                // No cross-DB foreign keys unfortunately
 412                db.execSQL(
 413                    "CREATE TABLE cheogram." + Message.TABLENAME + "(" +
 414                    Message.UUID + " TEXT PRIMARY KEY, " +
 415                    "subject TEXT" +
 416                    ")"
 417                );
 418                db.execSQL("PRAGMA cheogram.user_version = 1");
 419            }
 420
 421            if(cheogramVersion < 2) {
 422                db.execSQL(
 423                    "ALTER TABLE cheogram." + Message.TABLENAME + " " +
 424                    "ADD COLUMN oobUri TEXT"
 425                );
 426                db.execSQL(
 427                    "ALTER TABLE cheogram." + Message.TABLENAME + " " +
 428                    "ADD COLUMN fileParams TEXT"
 429                );
 430                db.execSQL("PRAGMA cheogram.user_version = 2");
 431            }
 432
 433            if(cheogramVersion < 3) {
 434                db.execSQL(
 435                    "ALTER TABLE cheogram." + Message.TABLENAME + " " +
 436                    "ADD COLUMN payloads TEXT"
 437                );
 438                db.execSQL("PRAGMA cheogram.user_version = 3");
 439            }
 440
 441            if(cheogramVersion < 4) {
 442                db.execSQL(
 443                    "CREATE TABLE cheogram.cids (" +
 444                    "cid TEXT NOT NULL PRIMARY KEY," +
 445                    "path TEXT NOT NULL" +
 446                    ")"
 447                );
 448                db.execSQL("PRAGMA cheogram.user_version = 4");
 449            }
 450
 451            if(cheogramVersion < 5) {
 452                db.execSQL(
 453                    "ALTER TABLE cheogram." + Message.TABLENAME + " " +
 454                    "ADD COLUMN timeReceived NUMBER"
 455                );
 456                db.execSQL("CREATE INDEX cheogram.message_time_received_index ON " + Message.TABLENAME + " (timeReceived)");
 457                db.execSQL("PRAGMA cheogram.user_version = 5");
 458            }
 459
 460            if(cheogramVersion < 6) {
 461                db.execSQL(
 462                    "CREATE TABLE cheogram.blocked_media (" +
 463                    "cid TEXT NOT NULL PRIMARY KEY" +
 464                    ")"
 465                );
 466                db.execSQL("PRAGMA cheogram.user_version = 6");
 467            }
 468
 469            if(cheogramVersion < 7) {
 470                db.execSQL(
 471                    "ALTER TABLE cheogram.cids " +
 472                    "ADD COLUMN url TEXT"
 473                );
 474                db.execSQL("PRAGMA cheogram.user_version = 7");
 475            }
 476
 477            if(cheogramVersion < 8) {
 478                db.execSQL(
 479                    "CREATE TABLE cheogram.webxdc_updates (" +
 480                    "serial INTEGER PRIMARY KEY AUTOINCREMENT, " +
 481                    Message.CONVERSATION + " TEXT NOT NULL, " +
 482                    "sender TEXT NOT NULL, " +
 483                    "thread TEXT NOT NULL, " +
 484                    "threadParent TEXT, " +
 485                    "info TEXT, " +
 486                    "document TEXT, " +
 487                    "summary TEXT, " +
 488                    "payload TEXT" +
 489                    ")"
 490                );
 491                db.execSQL("CREATE INDEX cheogram.webxdc_index ON webxdc_updates (" + Message.CONVERSATION + ", thread)");
 492                db.execSQL("PRAGMA cheogram.user_version = 8");
 493				}
 494
 495            if(cheogramVersion < 9) {
 496                db.execSQL(
 497                    "ALTER TABLE cheogram.webxdc_updates " +
 498                    "ADD COLUMN message_id TEXT"
 499                );
 500                db.execSQL("CREATE UNIQUE INDEX cheogram.webxdc_message_id_index ON webxdc_updates (" + Message.CONVERSATION + ", message_id)");
 501                db.execSQL("PRAGMA cheogram.user_version = 9");
 502            }
 503
 504            if(cheogramVersion < 10) {
 505                db.execSQL(
 506                    "CREATE TABLE cheogram.muted_participants (" +
 507                    "muc_jid TEXT NOT NULL, " +
 508                    "occupant_id TEXT NOT NULL, " +
 509                    "nick TEXT NOT NULL," +
 510                    "PRIMARY KEY (muc_jid, occupant_id)" +
 511                    ")"
 512                );
 513                db.execSQL(
 514                    "ALTER TABLE cheogram." + Message.TABLENAME + " " +
 515                    "ADD COLUMN occupant_id TEXT"
 516                );
 517                db.execSQL("PRAGMA cheogram.user_version = 10");
 518            }
 519
 520            if(cheogramVersion < 11) {
 521                if (Build.VERSION.SDK_INT >= 34) {
 522                    db.execSQL(
 523                        "ALTER TABLE cheogram.muted_participants " +
 524                        "DROP COLUMN nick"
 525                    );
 526                } else {
 527                    db.execSQL("DROP TABLE cheogram.muted_participants");
 528                    db.execSQL(
 529                        "CREATE TABLE cheogram.muted_participants (" +
 530                        "muc_jid TEXT NOT NULL, " +
 531                        "occupant_id TEXT NOT NULL, " +
 532                        "PRIMARY KEY (muc_jid, occupant_id)" +
 533                        ")"
 534                    );
 535                }
 536                db.execSQL("PRAGMA cheogram.user_version = 11");
 537            }
 538
 539            if(cheogramVersion < 12) {
 540                db.execSQL(
 541                    "ALTER TABLE cheogram." + Message.TABLENAME + " " +
 542                    "ADD COLUMN notificationDismissed NUMBER DEFAULT 0"
 543                );
 544                db.execSQL("PRAGMA cheogram.user_version = 12");
 545            }
 546
 547            db.setTransactionSuccessful();
 548        } finally {
 549            db.endTransaction();
 550        }
 551    }
 552
 553    @Override
 554    public void onConfigure(SQLiteDatabase db) {
 555        db.execSQL("PRAGMA foreign_keys=ON");
 556        db.rawQuery("PRAGMA secure_delete=ON", null).close();
 557        db.execSQL("ATTACH DATABASE ? AS cheogram", new Object[]{context.getDatabasePath("cheogram").getPath()});
 558        cheogramMigrate(db);
 559    }
 560
 561    @Override
 562    public void onCreate(SQLiteDatabase db) {
 563        db.execSQL(
 564                "create table "
 565                        + Account.TABLENAME
 566                        + "("
 567                        + Account.UUID
 568                        + " TEXT PRIMARY KEY,"
 569                        + Account.USERNAME
 570                        + " TEXT,"
 571                        + Account.SERVER
 572                        + " TEXT,"
 573                        + Account.PASSWORD
 574                        + " TEXT,"
 575                        + Account.DISPLAY_NAME
 576                        + " TEXT, "
 577                        + Account.STATUS
 578                        + " TEXT,"
 579                        + Account.STATUS_MESSAGE
 580                        + " TEXT,"
 581                        + Account.ROSTERVERSION
 582                        + " TEXT,"
 583                        + Account.OPTIONS
 584                        + " NUMBER, "
 585                        + Account.AVATAR
 586                        + " TEXT, "
 587                        + Account.KEYS
 588                        + " TEXT, "
 589                        + Account.HOSTNAME
 590                        + " TEXT, "
 591                        + Account.RESOURCE
 592                        + " TEXT,"
 593                        + Account.PINNED_MECHANISM
 594                        + " TEXT,"
 595                        + Account.PINNED_CHANNEL_BINDING
 596                        + " TEXT,"
 597                        + Account.FAST_MECHANISM
 598                        + " TEXT,"
 599                        + Account.FAST_TOKEN
 600                        + " TEXT,"
 601                        + Account.PORT
 602                        + " NUMBER DEFAULT 5222)");
 603        db.execSQL(
 604                "create table "
 605                        + Conversation.TABLENAME
 606                        + " ("
 607                        + Conversation.UUID
 608                        + " TEXT PRIMARY KEY, "
 609                        + Conversation.NAME
 610                        + " TEXT, "
 611                        + Conversation.CONTACT
 612                        + " TEXT, "
 613                        + Conversation.ACCOUNT
 614                        + " TEXT, "
 615                        + Conversation.CONTACTJID
 616                        + " TEXT, "
 617                        + Conversation.CREATED
 618                        + " NUMBER, "
 619                        + Conversation.STATUS
 620                        + " NUMBER, "
 621                        + Conversation.MODE
 622                        + " NUMBER, "
 623                        + Conversation.ATTRIBUTES
 624                        + " TEXT, FOREIGN KEY("
 625                        + Conversation.ACCOUNT
 626                        + ") REFERENCES "
 627                        + Account.TABLENAME
 628                        + "("
 629                        + Account.UUID
 630                        + ") ON DELETE CASCADE);");
 631        db.execSQL(
 632                "create table "
 633                        + Message.TABLENAME
 634                        + "( "
 635                        + Message.UUID
 636                        + " TEXT PRIMARY KEY, "
 637                        + Message.CONVERSATION
 638                        + " TEXT, "
 639                        + Message.TIME_SENT
 640                        + " NUMBER, "
 641                        + Message.COUNTERPART
 642                        + " TEXT, "
 643                        + Message.TRUE_COUNTERPART
 644                        + " TEXT,"
 645                        + Message.BODY
 646                        + " TEXT, "
 647                        + Message.ENCRYPTION
 648                        + " NUMBER, "
 649                        + Message.STATUS
 650                        + " NUMBER,"
 651                        + Message.TYPE
 652                        + " NUMBER, "
 653                        + Message.RELATIVE_FILE_PATH
 654                        + " TEXT, "
 655                        + Message.SERVER_MSG_ID
 656                        + " TEXT, "
 657                        + Message.FINGERPRINT
 658                        + " TEXT, "
 659                        + Message.CARBON
 660                        + " INTEGER, "
 661                        + Message.EDITED
 662                        + " TEXT, "
 663                        + Message.READ
 664                        + " NUMBER DEFAULT 1, "
 665                        + Message.OOB
 666                        + " INTEGER, "
 667                        + Message.ERROR_MESSAGE
 668                        + " TEXT,"
 669                        + Message.READ_BY_MARKERS
 670                        + " TEXT,"
 671                        + Message.MARKABLE
 672                        + " NUMBER DEFAULT 0,"
 673                        + Message.DELETED
 674                        + " NUMBER DEFAULT 0,"
 675                        + Message.BODY_LANGUAGE
 676                        + " TEXT,"
 677                        + Message.OCCUPANT_ID
 678                        + " TEXT,"
 679                        + Message.REACTIONS
 680                        + " TEXT,"
 681                        + Message.REMOTE_MSG_ID
 682                        + " TEXT, FOREIGN KEY("
 683                        + Message.CONVERSATION
 684                        + ") REFERENCES "
 685                        + Conversation.TABLENAME
 686                        + "("
 687                        + Conversation.UUID
 688                        + ") ON DELETE CASCADE);");
 689        db.execSQL(CREATE_MESSAGE_TIME_INDEX);
 690        db.execSQL(CREATE_MESSAGE_CONVERSATION_INDEX);
 691        db.execSQL(CREATE_MESSAGE_DELETED_INDEX);
 692        db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX);
 693        db.execSQL(CREATE_MESSAGE_TYPE_INDEX);
 694        db.execSQL(CREATE_CONTATCS_STATEMENT);
 695        db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
 696        db.execSQL(CREATE_SESSIONS_STATEMENT);
 697        db.execSQL(CREATE_PREKEYS_STATEMENT);
 698        db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
 699        db.execSQL(CREATE_IDENTITIES_STATEMENT);
 700        db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
 701        db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
 702        db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
 703        db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
 704        db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
 705        db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
 706    }
 707
 708    @Override
 709    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
 710        if (oldVersion < 2 && newVersion >= 2) {
 711            db.execSQL(
 712                    "update "
 713                            + Account.TABLENAME
 714                            + " set "
 715                            + Account.OPTIONS
 716                            + " = "
 717                            + Account.OPTIONS
 718                            + " | 8");
 719        }
 720        if (oldVersion < 3 && newVersion >= 3) {
 721            db.execSQL(
 722                    "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.TYPE + " NUMBER");
 723        }
 724        if (oldVersion < 5 && newVersion >= 5) {
 725            db.execSQL("DROP TABLE " + Contact.TABLENAME);
 726            db.execSQL(CREATE_CONTATCS_STATEMENT);
 727            db.execSQL("UPDATE " + Account.TABLENAME + " SET " + Account.ROSTERVERSION + " = NULL");
 728        }
 729        if (oldVersion < 6 && newVersion >= 6) {
 730            db.execSQL(
 731                    "ALTER TABLE "
 732                            + Message.TABLENAME
 733                            + " ADD COLUMN "
 734                            + Message.TRUE_COUNTERPART
 735                            + " TEXT");
 736        }
 737        if (oldVersion < 7 && newVersion >= 7) {
 738            db.execSQL(
 739                    "ALTER TABLE "
 740                            + Message.TABLENAME
 741                            + " ADD COLUMN "
 742                            + Message.REMOTE_MSG_ID
 743                            + " TEXT");
 744            db.execSQL(
 745                    "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.AVATAR + " TEXT");
 746            db.execSQL(
 747                    "ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.AVATAR + " TEXT");
 748        }
 749        if (oldVersion < 8 && newVersion >= 8) {
 750            db.execSQL(
 751                    "ALTER TABLE "
 752                            + Conversation.TABLENAME
 753                            + " ADD COLUMN "
 754                            + Conversation.ATTRIBUTES
 755                            + " TEXT");
 756        }
 757        if (oldVersion < 9 && newVersion >= 9) {
 758            db.execSQL(
 759                    "ALTER TABLE "
 760                            + Contact.TABLENAME
 761                            + " ADD COLUMN "
 762                            + Contact.LAST_TIME
 763                            + " NUMBER");
 764            db.execSQL(
 765                    "ALTER TABLE "
 766                            + Contact.TABLENAME
 767                            + " ADD COLUMN "
 768                            + Contact.LAST_PRESENCE
 769                            + " TEXT");
 770        }
 771        if (oldVersion < 10 && newVersion >= 10) {
 772            db.execSQL(
 773                    "ALTER TABLE "
 774                            + Message.TABLENAME
 775                            + " ADD COLUMN "
 776                            + Message.RELATIVE_FILE_PATH
 777                            + " TEXT");
 778        }
 779        if (oldVersion < 11 && newVersion >= 11) {
 780            db.execSQL(
 781                    "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.GROUPS + " TEXT");
 782            db.execSQL("delete from " + Contact.TABLENAME);
 783            db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
 784        }
 785        if (oldVersion < 12 && newVersion >= 12) {
 786            db.execSQL(
 787                    "ALTER TABLE "
 788                            + Message.TABLENAME
 789                            + " ADD COLUMN "
 790                            + Message.SERVER_MSG_ID
 791                            + " TEXT");
 792        }
 793        if (oldVersion < 13 && newVersion >= 13) {
 794            db.execSQL("delete from " + Contact.TABLENAME);
 795            db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
 796        }
 797        if (oldVersion < 14 && newVersion >= 14) {
 798            canonicalizeJids(db);
 799        }
 800        if (oldVersion < 15 && newVersion >= 15) {
 801            recreateAxolotlDb(db);
 802            db.execSQL(
 803                    "ALTER TABLE "
 804                            + Message.TABLENAME
 805                            + " ADD COLUMN "
 806                            + Message.FINGERPRINT
 807                            + " TEXT");
 808        }
 809        if (oldVersion < 16 && newVersion >= 16) {
 810            db.execSQL(
 811                    "ALTER TABLE "
 812                            + Message.TABLENAME
 813                            + " ADD COLUMN "
 814                            + Message.CARBON
 815                            + " INTEGER");
 816        }
 817        if (oldVersion < 19 && newVersion >= 19) {
 818            db.execSQL(
 819                    "ALTER TABLE "
 820                            + Account.TABLENAME
 821                            + " ADD COLUMN "
 822                            + Account.DISPLAY_NAME
 823                            + " TEXT");
 824        }
 825        if (oldVersion < 20 && newVersion >= 20) {
 826            db.execSQL(
 827                    "ALTER TABLE "
 828                            + Account.TABLENAME
 829                            + " ADD COLUMN "
 830                            + Account.HOSTNAME
 831                            + " TEXT");
 832            db.execSQL(
 833                    "ALTER TABLE "
 834                            + Account.TABLENAME
 835                            + " ADD COLUMN "
 836                            + Account.PORT
 837                            + " NUMBER DEFAULT 5222");
 838        }
 839        if (oldVersion < 26 && newVersion >= 26) {
 840            db.execSQL(
 841                    "ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT");
 842            db.execSQL(
 843                    "ALTER TABLE "
 844                            + Account.TABLENAME
 845                            + " ADD COLUMN "
 846                            + Account.STATUS_MESSAGE
 847                            + " TEXT");
 848        }
 849        if (oldVersion < 40 && newVersion >= 40) {
 850            db.execSQL(
 851                    "ALTER TABLE "
 852                            + Account.TABLENAME
 853                            + " ADD COLUMN "
 854                            + Account.RESOURCE
 855                            + " TEXT");
 856        }
 857        /* Any migrations that alter the Account table need to happen BEFORE this migration, as it
 858         * depends on account de-serialization.
 859         */
 860        if (oldVersion < 17 && newVersion >= 17 && newVersion < 31) {
 861            List<Account> accounts = getAccounts(db);
 862            for (Account account : accounts) {
 863                String ownDeviceIdString =
 864                        account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID);
 865                if (ownDeviceIdString == null) {
 866                    continue;
 867                }
 868                int ownDeviceId = Integer.valueOf(ownDeviceIdString);
 869                SignalProtocolAddress ownAddress =
 870                        new SignalProtocolAddress(
 871                                account.getJid().asBareJid().toString(), ownDeviceId);
 872                deleteSession(db, account, ownAddress);
 873                IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
 874                if (identityKeyPair != null) {
 875                    String[] selectionArgs = {
 876                        account.getUuid(),
 877                        CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize())
 878                    };
 879                    ContentValues values = new ContentValues();
 880                    values.put(SQLiteAxolotlStore.TRUSTED, 2);
 881                    db.update(
 882                            SQLiteAxolotlStore.IDENTITIES_TABLENAME,
 883                            values,
 884                            SQLiteAxolotlStore.ACCOUNT
 885                                    + " = ? AND "
 886                                    + SQLiteAxolotlStore.FINGERPRINT
 887                                    + " = ? ",
 888                            selectionArgs);
 889                } else {
 890                    Log.d(
 891                            Config.LOGTAG,
 892                            account.getJid().asBareJid()
 893                                    + ": could not load own identity key pair");
 894                }
 895            }
 896        }
 897        if (oldVersion < 18 && newVersion >= 18) {
 898            db.execSQL(
 899                    "ALTER TABLE "
 900                            + Message.TABLENAME
 901                            + " ADD COLUMN "
 902                            + Message.READ
 903                            + " NUMBER DEFAULT 1");
 904        }
 905
 906        if (oldVersion < 21 && newVersion >= 21) {
 907            List<Account> accounts = getAccounts(db);
 908            for (Account account : accounts) {
 909                account.unsetPgpSignature();
 910                db.update(
 911                        Account.TABLENAME,
 912                        account.getContentValues(),
 913                        Account.UUID + "=?",
 914                        new String[] {account.getUuid()});
 915            }
 916        }
 917
 918        if (oldVersion >= 15 && oldVersion < 22 && newVersion >= 22) {
 919            db.execSQL(
 920                    "ALTER TABLE "
 921                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
 922                            + " ADD COLUMN "
 923                            + SQLiteAxolotlStore.CERTIFICATE);
 924        }
 925
 926        if (oldVersion < 23 && newVersion >= 23) {
 927            db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
 928        }
 929
 930        if (oldVersion < 24 && newVersion >= 24) {
 931            db.execSQL(
 932                    "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
 933        }
 934
 935        if (oldVersion < 25 && newVersion >= 25) {
 936            db.execSQL(
 937                    "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
 938        }
 939
 940        if (oldVersion < 26 && newVersion >= 26) {
 941            db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
 942        }
 943
 944        if (oldVersion < 27 && newVersion >= 27) {
 945            db.execSQL("DELETE FROM " + ServiceDiscoveryResult.TABLENAME);
 946        }
 947
 948        if (oldVersion < 28 && newVersion >= 28) {
 949            canonicalizeJids(db);
 950        }
 951
 952        if (oldVersion < 29 && newVersion >= 29) {
 953            db.execSQL(
 954                    "ALTER TABLE "
 955                            + Message.TABLENAME
 956                            + " ADD COLUMN "
 957                            + Message.ERROR_MESSAGE
 958                            + " TEXT");
 959        }
 960        if (oldVersion >= 15 && oldVersion < 31 && newVersion >= 31) {
 961            db.execSQL(
 962                    "ALTER TABLE "
 963                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
 964                            + " ADD COLUMN "
 965                            + SQLiteAxolotlStore.TRUST
 966                            + " TEXT");
 967            db.execSQL(
 968                    "ALTER TABLE "
 969                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
 970                            + " ADD COLUMN "
 971                            + SQLiteAxolotlStore.ACTIVE
 972                            + " NUMBER");
 973            HashMap<Integer, ContentValues> migration = new HashMap<>();
 974            migration.put(
 975                    0, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
 976            migration.put(
 977                    1, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
 978            migration.put(
 979                    2,
 980                    createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
 981            migration.put(
 982                    3,
 983                    createFingerprintStatusContentValues(
 984                            FingerprintStatus.Trust.COMPROMISED, false));
 985            migration.put(
 986                    4,
 987                    createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
 988            migration.put(
 989                    5,
 990                    createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
 991            migration.put(
 992                    6,
 993                    createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
 994            migration.put(
 995                    7,
 996                    createFingerprintStatusContentValues(
 997                            FingerprintStatus.Trust.VERIFIED_X509, true));
 998            migration.put(
 999                    8,
1000                    createFingerprintStatusContentValues(
1001                            FingerprintStatus.Trust.VERIFIED_X509, false));
1002            for (Map.Entry<Integer, ContentValues> entry : migration.entrySet()) {
1003                String whereClause = SQLiteAxolotlStore.TRUSTED + "=?";
1004                String[] where = {String.valueOf(entry.getKey())};
1005                db.update(
1006                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
1007                        entry.getValue(),
1008                        whereClause,
1009                        where);
1010            }
1011        }
1012        if (oldVersion >= 15 && oldVersion < 32 && newVersion >= 32) {
1013            db.execSQL(
1014                    "ALTER TABLE "
1015                            + SQLiteAxolotlStore.IDENTITIES_TABLENAME
1016                            + " ADD COLUMN "
1017                            + SQLiteAxolotlStore.LAST_ACTIVATION
1018                            + " NUMBER");
1019            ContentValues defaults = new ContentValues();
1020            defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION, System.currentTimeMillis());
1021            db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, defaults, null, null);
1022        }
1023        if (oldVersion >= 15 && oldVersion < 33 && newVersion >= 33) {
1024            String whereClause = SQLiteAxolotlStore.OWN + "=1";
1025            db.update(
1026                    SQLiteAxolotlStore.IDENTITIES_TABLENAME,
1027                    createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED, true),
1028                    whereClause,
1029                    null);
1030        }
1031
1032        if (oldVersion < 34 && newVersion >= 34) {
1033            db.execSQL(CREATE_MESSAGE_TIME_INDEX);
1034
1035            final File oldPicturesDirectory =
1036                    new File(
1037                            Environment.getExternalStoragePublicDirectory(
1038                                            Environment.DIRECTORY_PICTURES)
1039                                    + "/Conversations/");
1040            final File oldFilesDirectory =
1041                    new File(Environment.getExternalStorageDirectory() + "/Conversations/");
1042            final File newFilesDirectory =
1043                    new File(
1044                            Environment.getExternalStorageDirectory()
1045                                    + "/Conversations/Media/Conversations Files/");
1046            final File newVideosDirectory =
1047                    new File(
1048                            Environment.getExternalStorageDirectory()
1049                                    + "/Conversations/Media/Conversations Videos/");
1050            if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) {
1051                final File newPicturesDirectory =
1052                        new File(
1053                                Environment.getExternalStorageDirectory()
1054                                        + "/Conversations/Media/Conversations Images/");
1055                newPicturesDirectory.getParentFile().mkdirs();
1056                if (oldPicturesDirectory.renameTo(newPicturesDirectory)) {
1057                    Log.d(
1058                            Config.LOGTAG,
1059                            "moved "
1060                                    + oldPicturesDirectory.getAbsolutePath()
1061                                    + " to "
1062                                    + newPicturesDirectory.getAbsolutePath());
1063                }
1064            }
1065            if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) {
1066                newFilesDirectory.mkdirs();
1067                newVideosDirectory.mkdirs();
1068                final File[] files = oldFilesDirectory.listFiles();
1069                if (files == null) {
1070                    return;
1071                }
1072                for (File file : files) {
1073                    if (file.getName().equals(".nomedia")) {
1074                        if (file.delete()) {
1075                            Log.d(
1076                                    Config.LOGTAG,
1077                                    "deleted nomedia file in "
1078                                            + oldFilesDirectory.getAbsolutePath());
1079                        }
1080                    } else if (file.isFile()) {
1081                        final String name = file.getName();
1082                        boolean isVideo = false;
1083                        int start = name.lastIndexOf('.') + 1;
1084                        if (start < name.length()) {
1085                            String mime =
1086                                    MimeUtils.guessMimeTypeFromExtension(name.substring(start));
1087                            isVideo = mime != null && mime.startsWith("video/");
1088                        }
1089                        File dst =
1090                                new File(
1091                                        (isVideo ? newVideosDirectory : newFilesDirectory)
1092                                                        .getAbsolutePath()
1093                                                + "/"
1094                                                + file.getName());
1095                        if (file.renameTo(dst)) {
1096                            Log.d(Config.LOGTAG, "moved " + file + " to " + dst);
1097                        }
1098                    }
1099                }
1100            }
1101        }
1102        if (oldVersion < 35 && newVersion >= 35) {
1103            db.execSQL(CREATE_MESSAGE_CONVERSATION_INDEX);
1104        }
1105        if (oldVersion < 36 && newVersion >= 36) {
1106            List<Account> accounts = getAccounts(db);
1107            for (Account account : accounts) {
1108                account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true);
1109                account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, false);
1110                db.update(
1111                        Account.TABLENAME,
1112                        account.getContentValues(),
1113                        Account.UUID + "=?",
1114                        new String[] {account.getUuid()});
1115            }
1116        }
1117
1118        if (oldVersion < 37 && newVersion >= 37) {
1119            db.execSQL(
1120                    "ALTER TABLE "
1121                            + Message.TABLENAME
1122                            + " ADD COLUMN "
1123                            + Message.READ_BY_MARKERS
1124                            + " TEXT");
1125        }
1126
1127        if (oldVersion < 38 && newVersion >= 38) {
1128            db.execSQL(
1129                    "ALTER TABLE "
1130                            + Message.TABLENAME
1131                            + " ADD COLUMN "
1132                            + Message.MARKABLE
1133                            + " NUMBER DEFAULT 0");
1134        }
1135
1136        if (oldVersion < 39 && newVersion >= 39) {
1137            db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
1138        }
1139
1140        if (QuickConversationsService.isQuicksy() && oldVersion < 43 && newVersion >= 43) {
1141            List<Account> accounts = getAccounts(db);
1142            for (Account account : accounts) {
1143                account.setOption(Account.OPTION_MAGIC_CREATE, true);
1144                db.update(
1145                        Account.TABLENAME,
1146                        account.getContentValues(),
1147                        Account.UUID + "=?",
1148                        new String[] {account.getUuid()});
1149            }
1150        }
1151
1152        if (oldVersion < 44 && newVersion >= 44) {
1153            db.execSQL(
1154                    "ALTER TABLE "
1155                            + Message.TABLENAME
1156                            + " ADD COLUMN "
1157                            + Message.DELETED
1158                            + " NUMBER DEFAULT 0");
1159            db.execSQL(CREATE_MESSAGE_DELETED_INDEX);
1160            db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX);
1161            db.execSQL(CREATE_MESSAGE_TYPE_INDEX);
1162        }
1163
1164        if (oldVersion < 45 && newVersion >= 45) {
1165            db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.BODY_LANGUAGE);
1166        }
1167
1168        if (oldVersion < 46 && newVersion >= 46) {
1169            final long start = SystemClock.elapsedRealtime();
1170            db.rawQuery("PRAGMA secure_delete = FALSE", null).close();
1171            db.execSQL("update " + Message.TABLENAME + " set " + Message.EDITED + "=NULL");
1172            db.rawQuery("PRAGMA secure_delete=ON", null).close();
1173            final long diff = SystemClock.elapsedRealtime() - start;
1174            Log.d(Config.LOGTAG, "deleted old edit information in " + diff + "ms");
1175        }
1176        if (oldVersion < 47 && newVersion >= 47) {
1177            db.execSQL(
1178                    "ALTER TABLE "
1179                            + Contact.TABLENAME
1180                            + " ADD COLUMN "
1181                            + Contact.PRESENCE_NAME
1182                            + " TEXT");
1183        }
1184        if (oldVersion < 48 && newVersion >= 48) {
1185            db.execSQL(
1186                    "ALTER TABLE "
1187                            + Contact.TABLENAME
1188                            + " ADD COLUMN "
1189                            + Contact.RTP_CAPABILITY
1190                            + " TEXT");
1191        }
1192        if (oldVersion < 49 && newVersion >= 49) {
1193            db.beginTransaction();
1194            db.execSQL("DROP TRIGGER IF EXISTS after_message_insert;");
1195            db.execSQL("DROP TRIGGER IF EXISTS after_message_update;");
1196            db.execSQL("DROP TRIGGER IF EXISTS after_message_delete;");
1197            db.execSQL("DROP TABLE IF EXISTS messages_index;");
1198            // a hack that should not be necessary, but
1199            // there was at least one occurence when SQLite failed at this
1200            db.execSQL("DROP TABLE IF EXISTS messages_index_docsize;");
1201            db.execSQL("DROP TABLE IF EXISTS messages_index_segdir;");
1202            db.execSQL("DROP TABLE IF EXISTS messages_index_segments;");
1203            db.execSQL("DROP TABLE IF EXISTS messages_index_stat;");
1204            db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
1205            db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
1206            db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
1207            db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
1208            db.setTransactionSuccessful();
1209            db.endTransaction();
1210            requiresMessageIndexRebuild = true;
1211        }
1212        if (oldVersion < 50 && newVersion >= 50) {
1213            db.execSQL(
1214                    "ALTER TABLE "
1215                            + Account.TABLENAME
1216                            + " ADD COLUMN "
1217                            + Account.PINNED_MECHANISM
1218                            + " TEXT");
1219            db.execSQL(
1220                    "ALTER TABLE "
1221                            + Account.TABLENAME
1222                            + " ADD COLUMN "
1223                            + Account.PINNED_CHANNEL_BINDING
1224                            + " TEXT");
1225        }
1226        if (oldVersion < 51 && newVersion >= 51) {
1227            db.execSQL(
1228                    "ALTER TABLE "
1229                            + Account.TABLENAME
1230                            + " ADD COLUMN "
1231                            + Account.FAST_MECHANISM
1232                            + " TEXT");
1233            db.execSQL(
1234                    "ALTER TABLE "
1235                            + Account.TABLENAME
1236                            + " ADD COLUMN "
1237                            + Account.FAST_TOKEN
1238                            + " TEXT");
1239        }
1240        if (oldVersion < 52 && newVersion >= 52) {
1241            db.execSQL(
1242                    "ALTER TABLE "
1243                            + Message.TABLENAME
1244                            + " ADD COLUMN "
1245                            + Message.OCCUPANT_ID
1246                            + " TEXT");
1247            db.execSQL(
1248                    "ALTER TABLE "
1249                            + Message.TABLENAME
1250                            + " ADD COLUMN "
1251                            + Message.REACTIONS
1252                            + " TEXT");
1253        }
1254    }
1255
1256    private void canonicalizeJids(SQLiteDatabase db) {
1257        // migrate db to new, canonicalized JID domainpart representation
1258
1259        // Conversation table
1260        Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
1261        while (cursor.moveToNext()) {
1262            String newJid;
1263            try {
1264                newJid =
1265                        Jid.of(cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)))
1266                                .toString();
1267            } catch (IllegalArgumentException ignored) {
1268                Log.e(
1269                        Config.LOGTAG,
1270                        "Failed to migrate Conversation CONTACTJID "
1271                                + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
1272                                + ": "
1273                                + ignored
1274                                + ". Skipping...");
1275                continue;
1276            }
1277
1278            final String[] updateArgs = {
1279                newJid, cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
1280            };
1281            db.execSQL(
1282                    "update "
1283                            + Conversation.TABLENAME
1284                            + " set "
1285                            + Conversation.CONTACTJID
1286                            + " = ? "
1287                            + " where "
1288                            + Conversation.UUID
1289                            + " = ?",
1290                    updateArgs);
1291        }
1292        cursor.close();
1293
1294        // Contact table
1295        cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
1296        while (cursor.moveToNext()) {
1297            String newJid;
1298            try {
1299                newJid = Jid.of(cursor.getString(cursor.getColumnIndex(Contact.JID))).toString();
1300            } catch (final IllegalArgumentException e) {
1301                Log.e(
1302                        Config.LOGTAG,
1303                        "Failed to migrate Contact JID "
1304                                + cursor.getString(cursor.getColumnIndex(Contact.JID))
1305                                + ":  Skipping...",
1306                        e);
1307                continue;
1308            }
1309
1310            final String[] updateArgs = {
1311                newJid,
1312                cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
1313                cursor.getString(cursor.getColumnIndex(Contact.JID)),
1314            };
1315            db.execSQL(
1316                    "update "
1317                            + Contact.TABLENAME
1318                            + " set "
1319                            + Contact.JID
1320                            + " = ? "
1321                            + " where "
1322                            + Contact.ACCOUNT
1323                            + " = ? "
1324                            + " AND "
1325                            + Contact.JID
1326                            + " = ?",
1327                    updateArgs);
1328        }
1329        cursor.close();
1330
1331        // Account table
1332        cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
1333        while (cursor.moveToNext()) {
1334            String newServer;
1335            try {
1336                newServer =
1337                        Jid.of(
1338                                        cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
1339                                        cursor.getString(cursor.getColumnIndex(Account.SERVER)),
1340                                        null)
1341                                .getDomain()
1342                                .toString();
1343            } catch (IllegalArgumentException ignored) {
1344                Log.e(
1345                        Config.LOGTAG,
1346                        "Failed to migrate Account SERVER "
1347                                + cursor.getString(cursor.getColumnIndex(Account.SERVER))
1348                                + ": "
1349                                + ignored
1350                                + ". Skipping...");
1351                continue;
1352            }
1353
1354            String[] updateArgs = {
1355                newServer, cursor.getString(cursor.getColumnIndex(Account.UUID)),
1356            };
1357            db.execSQL(
1358                    "update "
1359                            + Account.TABLENAME
1360                            + " set "
1361                            + Account.SERVER
1362                            + " = ? "
1363                            + " where "
1364                            + Account.UUID
1365                            + " = ?",
1366                    updateArgs);
1367        }
1368        cursor.close();
1369    }
1370
1371    public DownloadableFile getFileForCid(Cid cid) {
1372        if (cid == null) return null;
1373
1374        SQLiteDatabase db = this.getReadableDatabase();
1375        Cursor cursor = db.query("cheogram.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null);
1376        DownloadableFile f = null;
1377        if (cursor.moveToNext()) {
1378            f = new DownloadableFile(cursor.getString(0));
1379        }
1380        cursor.close();
1381        return f;
1382    }
1383
1384    public String getUrlForCid(Cid cid) {
1385        SQLiteDatabase db = this.getReadableDatabase();
1386        Cursor cursor = db.query("cheogram.cids", new String[]{"url"}, "cid=?", new String[]{cid.toString()}, null, null, null);
1387        String url = null;
1388        if (cursor.moveToNext()) {
1389            url = cursor.getString(0);
1390        }
1391        cursor.close();
1392        return url;
1393    }
1394
1395    public void saveCid(Cid cid, File file) {
1396        saveCid(cid, file, null);
1397    }
1398
1399    public void saveCid(Cid cid, File file, String url) {
1400        SQLiteDatabase db = this.getWritableDatabase();
1401        ContentValues cv = new ContentValues();
1402        cv.put("cid", cid.toString());
1403        if (file != null) cv.put("path", file.getAbsolutePath());
1404        if (url != null) cv.put("url", url);
1405        if (db.update("cheogram.cids", cv, "cid=?", new String[]{cid.toString()}) < 1) {
1406            db.insertWithOnConflict("cheogram.cids", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
1407        }
1408    }
1409
1410    public void blockMedia(Cid cid) {
1411        SQLiteDatabase db = this.getWritableDatabase();
1412        ContentValues cv = new ContentValues();
1413        cv.put("cid", cid.toString());
1414        db.insertWithOnConflict("cheogram.blocked_media", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
1415    }
1416
1417    public boolean isBlockedMedia(Cid cid) {
1418        SQLiteDatabase db = this.getReadableDatabase();
1419        Cursor cursor = db.query("cheogram.blocked_media", new String[]{"count(*)"}, "cid=?", new String[]{cid.toString()}, null, null, null);
1420        boolean is = false;
1421        if (cursor.moveToNext()) {
1422            is = cursor.getInt(0) > 0;
1423        }
1424        cursor.close();
1425        return is;
1426    }
1427
1428    public void clearBlockedMedia() {
1429        SQLiteDatabase db = this.getWritableDatabase();
1430        db.execSQL("DELETE FROM cheogram.blocked_media");
1431    }
1432
1433    public Multimap<String, String> loadMutedMucUsers() {
1434        Multimap<String, String> result = HashMultimap.create();
1435        SQLiteDatabase db = this.getReadableDatabase();
1436        Cursor cursor = db.query("cheogram.muted_participants", new String[]{"muc_jid", "occupant_id"}, null, null, null, null, null);
1437        while (cursor.moveToNext()) {
1438            result.put(cursor.getString(0), cursor.getString(1));
1439        }
1440        cursor.close();
1441        return result;
1442    }
1443
1444    public boolean muteMucUser(MucOptions.User user) {
1445        if (user.getMuc() == null || user.getOccupantId() == null) return false;
1446
1447        SQLiteDatabase db = this.getWritableDatabase();
1448        ContentValues cv = new ContentValues();
1449        cv.put("muc_jid", user.getMuc().toString());
1450        cv.put("occupant_id", user.getOccupantId());
1451        db.insertWithOnConflict("cheogram.muted_participants", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
1452
1453        return true;
1454    }
1455
1456    public boolean unmuteMucUser(MucOptions.User user) {
1457        if (user.getMuc() == null || user.getOccupantId() == null) return false;
1458
1459        SQLiteDatabase db = this.getWritableDatabase();
1460        String where = "muc_jid=? AND occupant_id=?";
1461        String[] whereArgs = {user.getMuc().toString(), user.getOccupantId()};
1462        db.delete("cheogram.muted_participants", where, whereArgs);
1463
1464        return true;
1465    }
1466
1467    public void insertWebxdcUpdate(final WebxdcUpdate update) {
1468        SQLiteDatabase db = this.getWritableDatabase();
1469        db.insertWithOnConflict("cheogram.webxdc_updates", null, update.getContentValues(), SQLiteDatabase.CONFLICT_IGNORE);
1470    }
1471
1472    public WebxdcUpdate findLastWebxdcUpdate(Message message) {
1473        if (message.getThread() == null) {
1474            Log.w(Config.LOGTAG, "WebXDC message with no thread!");
1475            return null;
1476        }
1477
1478        SQLiteDatabase db = this.getReadableDatabase();
1479        String[] selectionArgs = {message.getConversation().getUuid(), message.getThread().getContent()};
1480        Cursor cursor = db.query("cheogram.webxdc_updates", null,
1481                Message.CONVERSATION + "=? AND thread=?",
1482                selectionArgs, null, null, "serial ASC");
1483        WebxdcUpdate update = null;
1484        if (cursor.moveToLast()) {
1485            update = new WebxdcUpdate(cursor, cursor.getLong(cursor.getColumnIndex("serial")));
1486        }
1487        cursor.close();
1488        return update;
1489    }
1490
1491    public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
1492        SQLiteDatabase db = this.getReadableDatabase();
1493        String[] selectionArgs = {message.getConversation().getUuid(), message.getThread().getContent(), String.valueOf(serial)};
1494        Cursor cursor = db.query("cheogram.webxdc_updates", null,
1495                Message.CONVERSATION + "=? AND thread=? AND serial>?",
1496                selectionArgs, null, null, "serial ASC");
1497        long maxSerial = 0;
1498        if (cursor.moveToLast()) {
1499            maxSerial = cursor.getLong(cursor.getColumnIndex("serial"));
1500        }
1501        cursor.moveToFirst();
1502        cursor.moveToPrevious();
1503
1504        List<WebxdcUpdate> updates = new ArrayList<>();
1505        while (cursor.moveToNext()) {
1506            updates.add(new WebxdcUpdate(cursor, maxSerial));
1507        }
1508        cursor.close();
1509        return updates;
1510    }
1511
1512    public void createConversation(Conversation conversation) {
1513        SQLiteDatabase db = this.getWritableDatabase();
1514        db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
1515    }
1516
1517    public void createMessage(Message message) {
1518        SQLiteDatabase db = this.getWritableDatabase();
1519        db.insert(Message.TABLENAME, null, message.getContentValues());
1520        db.insert("cheogram." + Message.TABLENAME, null, message.getCheogramContentValues());
1521    }
1522
1523    public void createAccount(Account account) {
1524        SQLiteDatabase db = this.getWritableDatabase();
1525        db.insert(Account.TABLENAME, null, account.getContentValues());
1526    }
1527
1528    public void insertDiscoveryResult(ServiceDiscoveryResult result) {
1529        SQLiteDatabase db = this.getWritableDatabase();
1530        db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues());
1531    }
1532
1533    public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) {
1534        SQLiteDatabase db = this.getReadableDatabase();
1535        String[] selectionArgs = {hash, ver};
1536        Cursor cursor =
1537                db.query(
1538                        ServiceDiscoveryResult.TABLENAME,
1539                        null,
1540                        ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?",
1541                        selectionArgs,
1542                        null,
1543                        null,
1544                        null);
1545        if (cursor.getCount() == 0) {
1546            cursor.close();
1547            return null;
1548        }
1549        cursor.moveToFirst();
1550
1551        ServiceDiscoveryResult result = null;
1552        try {
1553            result = new ServiceDiscoveryResult(cursor);
1554        } catch (JSONException e) {
1555            /* result is still null */
1556        }
1557
1558        cursor.close();
1559        return result;
1560    }
1561
1562    public void saveResolverResult(String domain, Resolver.Result result) {
1563        SQLiteDatabase db = this.getWritableDatabase();
1564        ContentValues contentValues = result.toContentValues();
1565        contentValues.put(Resolver.Result.DOMAIN, domain);
1566        db.insert(RESOLVER_RESULTS_TABLENAME, null, contentValues);
1567    }
1568
1569    public synchronized Resolver.Result findResolverResult(String domain) {
1570        SQLiteDatabase db = this.getReadableDatabase();
1571        String where = Resolver.Result.DOMAIN + "=?";
1572        String[] whereArgs = {domain};
1573        final Cursor cursor =
1574                db.query(RESOLVER_RESULTS_TABLENAME, null, where, whereArgs, null, null, null);
1575        Resolver.Result result = null;
1576        if (cursor != null) {
1577            try {
1578                if (cursor.moveToFirst()) {
1579                    result = Resolver.Result.fromCursor(cursor);
1580                }
1581            } catch (Exception e) {
1582                Log.d(
1583                        Config.LOGTAG,
1584                        "unable to find cached resolver result in database " + e.getMessage());
1585                return null;
1586            } finally {
1587                cursor.close();
1588            }
1589        }
1590        return result;
1591    }
1592
1593    public void insertPresenceTemplate(PresenceTemplate template) {
1594        SQLiteDatabase db = this.getWritableDatabase();
1595        String whereToDelete = PresenceTemplate.MESSAGE + "=?";
1596        String[] whereToDeleteArgs = {template.getStatusMessage()};
1597        db.delete(PresenceTemplate.TABELNAME, whereToDelete, whereToDeleteArgs);
1598        db.delete(
1599                PresenceTemplate.TABELNAME,
1600                PresenceTemplate.UUID
1601                        + " not in (select "
1602                        + PresenceTemplate.UUID
1603                        + " from "
1604                        + PresenceTemplate.TABELNAME
1605                        + " order by "
1606                        + PresenceTemplate.LAST_USED
1607                        + " desc limit 9)",
1608                null);
1609        db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues());
1610    }
1611
1612    public List<PresenceTemplate> getPresenceTemplates() {
1613        ArrayList<PresenceTemplate> templates = new ArrayList<>();
1614        SQLiteDatabase db = this.getReadableDatabase();
1615        Cursor cursor =
1616                db.query(
1617                        PresenceTemplate.TABELNAME,
1618                        null,
1619                        null,
1620                        null,
1621                        null,
1622                        null,
1623                        PresenceTemplate.LAST_USED + " desc");
1624        while (cursor.moveToNext()) {
1625            templates.add(PresenceTemplate.fromCursor(cursor));
1626        }
1627        cursor.close();
1628        return templates;
1629    }
1630
1631    public CopyOnWriteArrayList<Conversation> getConversations(int status) {
1632        CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>();
1633        SQLiteDatabase db = this.getReadableDatabase();
1634        String[] selectionArgs = {Integer.toString(status)};
1635        Cursor cursor =
1636                db.rawQuery(
1637                        "select * from "
1638                                + Conversation.TABLENAME
1639                                + " where "
1640                                + Conversation.STATUS
1641                                + " = ? and "
1642                                + Conversation.CONTACTJID
1643                                + " is not null order by "
1644                                + Conversation.CREATED
1645                                + " desc",
1646                        selectionArgs);
1647        while (cursor.moveToNext()) {
1648            final Conversation conversation = Conversation.fromCursor(cursor);
1649            if (conversation.getJid() instanceof Jid.Invalid) {
1650                continue;
1651            }
1652            list.add(conversation);
1653        }
1654        cursor.close();
1655        return list;
1656    }
1657
1658    public Message getMessage(Conversation conversation, String uuid) {
1659        ArrayList<Message> list = new ArrayList<>();
1660        SQLiteDatabase db = this.getReadableDatabase();
1661        Cursor cursor;
1662        cursor = db.rawQuery(
1663            "SELECT * FROM " + Message.TABLENAME + " " +
1664            "LEFT JOIN cheogram." + Message.TABLENAME +
1665            "  USING (" + Message.UUID + ")" +
1666            "WHERE " + Message.UUID + "=?",
1667            new String[]{uuid}
1668        );
1669        while (cursor.moveToNext()) {
1670            try {
1671                return Message.fromCursor(cursor, conversation);
1672            } catch (Exception e) {
1673                Log.e(Config.LOGTAG, "unable to restore message");
1674            }
1675        }
1676        cursor.close();
1677        return null;
1678    }
1679
1680    public ArrayList<Message> getMessages(Conversation conversations, int limit) {
1681        return getMessages(conversations, limit, -1);
1682    }
1683
1684    public Map<String, Message> getMessageFuzzyIds(Conversation conversation, Collection<String> ids) {
1685        final var result = new Hashtable<String, Message>();
1686        if (ids.size() < 1) return result;
1687        final ArrayList<String> params = new ArrayList<>();
1688        final ArrayList<String> template = new ArrayList<>();
1689        for (final var id : ids) {
1690            template.add("?");
1691        }
1692        params.addAll(ids);
1693        params.addAll(ids);
1694        params.addAll(ids);
1695        ArrayList<Message> list = new ArrayList<>();
1696        SQLiteDatabase db = this.getReadableDatabase();
1697        Cursor cursor;
1698        cursor = db.rawQuery(
1699            "SELECT * FROM " + Message.TABLENAME + " " +
1700            "LEFT JOIN cheogram." + Message.TABLENAME +
1701            "  USING (" + Message.UUID + ")" +
1702            "WHERE " + Message.UUID + " IN (" + TextUtils.join(",", template) + ") OR " + Message.SERVER_MSG_ID + " IN (" + TextUtils.join(",", template) + ") OR " + Message.REMOTE_MSG_ID + " IN (" + TextUtils.join(",", template) + ")",
1703            params.toArray(new String[0])
1704        );
1705
1706        while (cursor.moveToNext()) {
1707            try {
1708                final var m = Message.fromCursor(cursor, conversation);
1709                if (ids.contains(m.getUuid())) result.put(m.getUuid(), m);
1710                if (ids.contains(m.getServerMsgId())) result.put(m.getServerMsgId(), m);
1711                if (ids.contains(m.getRemoteMsgId())) result.put(m.getRemoteMsgId(), m);
1712            } catch (Exception e) {
1713                Log.e(Config.LOGTAG, "unable to restore message");
1714            }
1715        }
1716        cursor.close();
1717        return result;
1718    }
1719
1720    public ArrayList<Message> getMessages(Conversation conversation, int limit, long timestamp) {
1721        ArrayList<Message> list = new ArrayList<>();
1722        SQLiteDatabase db = this.getReadableDatabase();
1723        Cursor cursor;
1724        if (timestamp == -1) {
1725            String[] selectionArgs = {conversation.getUuid()};
1726            cursor = db.rawQuery(
1727                "SELECT * FROM " + Message.TABLENAME + " " +
1728                "LEFT JOIN cheogram." + Message.TABLENAME +
1729                "  USING (" + Message.UUID + ")" +
1730                " WHERE " + Message.UUID + " IN (" +
1731                "SELECT " + Message.UUID + " FROM " + Message.TABLENAME +
1732                " WHERE " + Message.CONVERSATION + "=? " +
1733                "ORDER BY " + Message.TIME_SENT + " DESC " +
1734                "LIMIT " + String.valueOf(limit) + ") " +
1735                "ORDER BY " + Message.TIME_SENT + " DESC ",
1736                selectionArgs
1737            );
1738        } else {
1739            String[] selectionArgs = {conversation.getUuid(),
1740                    Long.toString(timestamp)};
1741            cursor = db.rawQuery(
1742                "SELECT * FROM " + Message.TABLENAME + " " +
1743                "LEFT JOIN cheogram." + Message.TABLENAME +
1744                "  USING (" + Message.UUID + ")" +
1745                " WHERE " + Message.UUID + " IN (" +
1746                "SELECT " + Message.UUID + " FROM " + Message.TABLENAME +
1747                " WHERE " + Message.CONVERSATION + "=? AND " +
1748                Message.TIME_SENT + "<? " +
1749                "ORDER BY " + Message.TIME_SENT + " DESC " +
1750                "LIMIT " + String.valueOf(limit) + ") " +
1751                "ORDER BY " + Message.TIME_SENT + " DESC ",
1752                selectionArgs
1753            );
1754        }
1755        CursorUtils.upgradeCursorWindowSize(cursor);
1756        final Multimap<String, Message> waitingForReplies = HashMultimap.create();
1757        final var replyIds = new HashSet<String>();
1758        while (cursor.moveToNext()) {
1759            try {
1760                final var m = Message.fromCursor(cursor, conversation);
1761                final var reply = m.getReply();
1762                if (reply != null && reply.getAttribute("id") != null) { // Guard against busted replies
1763                    replyIds.add(reply.getAttribute("id"));
1764                    waitingForReplies.put(reply.getAttribute("id"), m);
1765                }
1766                list.add(0, m);
1767            } catch (Exception e) {
1768                Log.e(Config.LOGTAG, "unable to restore message", e);
1769            }
1770        }
1771        for (final var parent : getMessageFuzzyIds(conversation, replyIds).entrySet()) {
1772            for (final var m : waitingForReplies.get(parent.getKey())) {
1773                m.setInReplyTo(parent.getValue());
1774            }
1775        }
1776        cursor.close();
1777        return list;
1778    }
1779
1780    public Cursor getMessageSearchCursor(final List<String> term, final String uuid) {
1781        final SQLiteDatabase db = this.getReadableDatabase();
1782        final StringBuilder SQL = new StringBuilder();
1783        final String[] selectionArgs;
1784        SQL.append(
1785                "SELECT "
1786                        + Message.TABLENAME
1787                        + ".*,"
1788                        + Conversation.TABLENAME
1789                        + "."
1790                        + Conversation.CONTACTJID
1791                        + ","
1792                        + Conversation.TABLENAME
1793                        + "."
1794                        + Conversation.ACCOUNT
1795                        + ","
1796                        + Conversation.TABLENAME
1797                        + "."
1798                        + Conversation.MODE
1799                        + " FROM "
1800                        + Message.TABLENAME
1801                        + " JOIN "
1802                        + Conversation.TABLENAME
1803                        + " ON "
1804                        + Message.TABLENAME
1805                        + "."
1806                        + Message.CONVERSATION
1807                        + "="
1808                        + Conversation.TABLENAME
1809                        + "."
1810                        + Conversation.UUID
1811                        + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE "
1812                        + Message.ENCRYPTION
1813                        + " NOT IN("
1814                        + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE
1815                        + ","
1816                        + Message.ENCRYPTION_PGP
1817                        + ","
1818                        + Message.ENCRYPTION_DECRYPTION_FAILED
1819                        + ","
1820                        + Message.ENCRYPTION_AXOLOTL_FAILED
1821                        + ") AND "
1822                        + Message.TYPE
1823                        + " IN("
1824                        + Message.TYPE_TEXT
1825                        + ","
1826                        + Message.TYPE_PRIVATE
1827                        + ") AND messages_index.body MATCH ?");
1828        if (uuid == null) {
1829            selectionArgs = new String[] {FtsUtils.toMatchString(term)};
1830        } else {
1831            selectionArgs = new String[] {FtsUtils.toMatchString(term), uuid};
1832            SQL.append(" AND " + Conversation.TABLENAME + '.' + Conversation.UUID + "=?");
1833        }
1834        SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS);
1835        Log.d(Config.LOGTAG, "search term: " + FtsUtils.toMatchString(term));
1836        return db.rawQuery(SQL.toString(), selectionArgs);
1837    }
1838
1839    public List<String> markFileAsDeleted(final File file, final boolean internal) {
1840        SQLiteDatabase db = this.getReadableDatabase();
1841        String selection;
1842        String[] selectionArgs;
1843        if (internal) {
1844            final String name = file.getName();
1845            if (name.endsWith(".pgp")) {
1846                selection =
1847                        "("
1848                                + Message.RELATIVE_FILE_PATH
1849                                + " IN(?,?) OR ("
1850                                + Message.RELATIVE_FILE_PATH
1851                                + "=? and encryption in(1,4))) and type in (1,2,5)";
1852                selectionArgs =
1853                        new String[] {
1854                            file.getAbsolutePath(), name, name.substring(0, name.length() - 4)
1855                        };
1856            } else {
1857                selection = Message.RELATIVE_FILE_PATH + " IN(?,?) and type in (1,2,5)";
1858                selectionArgs = new String[] {file.getAbsolutePath(), name};
1859            }
1860        } else {
1861            selection = Message.RELATIVE_FILE_PATH + "=? and type in (1,2,5)";
1862            selectionArgs = new String[] {file.getAbsolutePath()};
1863        }
1864        final List<String> uuids = new ArrayList<>();
1865        Cursor cursor =
1866                db.query(
1867                        Message.TABLENAME,
1868                        new String[] {Message.UUID},
1869                        selection,
1870                        selectionArgs,
1871                        null,
1872                        null,
1873                        null);
1874        while (cursor != null && cursor.moveToNext()) {
1875            uuids.add(cursor.getString(0));
1876        }
1877        if (cursor != null) {
1878            cursor.close();
1879        }
1880        markFileAsDeleted(uuids);
1881        return uuids;
1882    }
1883
1884    public void markFileAsDeleted(List<String> uuids) {
1885        SQLiteDatabase db = this.getReadableDatabase();
1886        final ContentValues contentValues = new ContentValues();
1887        final String where = Message.UUID + "=?";
1888        contentValues.put(Message.DELETED, 1);
1889        db.beginTransaction();
1890        for (String uuid : uuids) {
1891            db.update(Message.TABLENAME, contentValues, where, new String[] {uuid});
1892        }
1893        db.setTransactionSuccessful();
1894        db.endTransaction();
1895    }
1896
1897    public void markFilesAsChanged(List<FilePathInfo> files) {
1898        SQLiteDatabase db = this.getReadableDatabase();
1899        final String where = Message.UUID + "=?";
1900        db.beginTransaction();
1901        for (FilePathInfo info : files) {
1902            final ContentValues contentValues = new ContentValues();
1903            contentValues.put(Message.DELETED, info.deleted ? 1 : 0);
1904            db.update(Message.TABLENAME, contentValues, where, new String[] {info.uuid.toString()});
1905        }
1906        db.setTransactionSuccessful();
1907        db.endTransaction();
1908    }
1909
1910    public List<FilePathInfo> getFilePathInfo() {
1911        final SQLiteDatabase db = this.getReadableDatabase();
1912        final Cursor cursor =
1913                db.query(
1914                        Message.TABLENAME,
1915                        new String[] {Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED},
1916                        "type in (1,2,5) and " + Message.RELATIVE_FILE_PATH + " is not null",
1917                        null,
1918                        null,
1919                        null,
1920                        null);
1921        final List<FilePathInfo> list = new ArrayList<>();
1922        while (cursor != null && cursor.moveToNext()) {
1923            list.add(
1924                    new FilePathInfo(
1925                            cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0));
1926        }
1927        if (cursor != null) {
1928            cursor.close();
1929        }
1930        return list;
1931    }
1932
1933    public List<FilePath> getRelativeFilePaths(String account, Jid jid, int limit) {
1934        SQLiteDatabase db = this.getReadableDatabase();
1935        final String SQL =
1936                "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and"
1937                        + " "
1938                        + Message.RELATIVE_FILE_PATH
1939                        + " is not null and conversationUuid=(select uuid from conversations where"
1940                        + " accountUuid=? and (contactJid=? or contactJid like ?)) order by"
1941                        + " timeSent desc";
1942        final String[] args = {account, jid.toString(), jid.toString() + "/%"};
1943        Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args);
1944        List<FilePath> filesPaths = new ArrayList<>();
1945        while (cursor.moveToNext()) {
1946            filesPaths.add(new FilePath(cursor.getString(0), cursor.getString(1)));
1947        }
1948        cursor.close();
1949        return filesPaths;
1950    }
1951
1952    public Message getMessageWithServerMsgId(
1953            final Conversation conversation, final String messageId) {
1954        final var db = this.getReadableDatabase();
1955        final String sql =
1956                "select * from messages LEFT JOIN cheogram.messages USING (uuid) where conversationUuid=? and serverMsgId=? LIMIT 1";
1957        final String[] args = {conversation.getUuid(), messageId};
1958        final Cursor cursor = db.rawQuery(sql, args);
1959        if (cursor == null) {
1960            return null;
1961        }
1962        Message message = null;
1963        try {
1964            if (cursor.moveToFirst()) {
1965                message = Message.fromCursor(cursor, conversation);
1966            }
1967        } catch (final IOException e) { }
1968        cursor.close();
1969        return message;
1970    }
1971
1972    public Message getMessageWithUuidOrRemoteId(
1973            final Conversation conversation, final String messageId) {
1974        final var db = this.getReadableDatabase();
1975        final String sql =
1976                "select * from messages LEFT JOIN cheogram.messages USING (uuid) where conversationUuid=? and (uuid=? OR remoteMsgId=?) LIMIT 1";
1977        final String[] args = {conversation.getUuid(), messageId, messageId};
1978        final Cursor cursor = db.rawQuery(sql, args);
1979        if (cursor == null) {
1980            return null;
1981        }
1982        Message message = null;
1983        try {
1984            if (cursor.moveToFirst()) {
1985                message = Message.fromCursor(cursor, conversation);
1986            }
1987        } catch (final IOException e) { }
1988        cursor.close();
1989        return message;
1990    }
1991
1992    public static class FilePath {
1993        public final UUID uuid;
1994        public final String path;
1995
1996        private FilePath(String uuid, String path) {
1997            this.uuid = UUID.fromString(uuid);
1998            this.path = path;
1999        }
2000    }
2001
2002    public static class FilePathInfo extends FilePath {
2003        public boolean deleted;
2004
2005        private FilePathInfo(String uuid, String path, boolean deleted) {
2006            super(uuid, path);
2007            this.deleted = deleted;
2008        }
2009
2010        public boolean setDeleted(boolean deleted) {
2011            final boolean changed = deleted != this.deleted;
2012            this.deleted = deleted;
2013            return changed;
2014        }
2015    }
2016
2017    public Conversation findConversation(final String uuid) {
2018        final var db = this.getReadableDatabase();
2019        final String[] selectionArgs = {uuid};
2020        try (final Cursor cursor =
2021                db.query(
2022                        Conversation.TABLENAME,
2023                        null,
2024                        Conversation.UUID + "=?",
2025                        selectionArgs,
2026                        null,
2027                        null,
2028                        null)) {
2029            if (cursor.getCount() == 0) {
2030                return null;
2031            }
2032            cursor.moveToFirst();
2033            final Conversation conversation = Conversation.fromCursor(cursor);
2034            if (conversation.getJid() instanceof Jid.Invalid) {
2035                return null;
2036            }
2037            return conversation;
2038        }
2039    }
2040
2041    public Conversation findConversation(final Account account, final Jid contactJid) {
2042        final SQLiteDatabase db = this.getReadableDatabase();
2043        final String[] selectionArgs = {
2044            account.getUuid(),
2045            contactJid.asBareJid().toString() + "/%",
2046            contactJid.asBareJid().toString()
2047        };
2048        try (final Cursor cursor =
2049                db.query(
2050                        Conversation.TABLENAME,
2051                        null,
2052                        Conversation.ACCOUNT
2053                                + "=? AND ("
2054                                + Conversation.CONTACTJID
2055                                + " like ? OR "
2056                                + Conversation.CONTACTJID
2057                                + "=?)",
2058                        selectionArgs,
2059                        null,
2060                        null,
2061                        null)) {
2062            if (cursor.getCount() == 0) {
2063                return null;
2064            }
2065            cursor.moveToFirst();
2066            final Conversation conversation = Conversation.fromCursor(cursor);
2067            if (conversation.getJid() instanceof Jid.Invalid) {
2068                return null;
2069            }
2070            conversation.setAccount(account);
2071            return conversation;
2072        }
2073    }
2074
2075    public String findConversationUuid(final Jid account, final Jid jid) {
2076        final SQLiteDatabase db = this.getReadableDatabase();
2077        final String[] selectionArgs = {
2078            account.getLocal(),
2079            account.getDomain().toString(),
2080            jid.asBareJid().toString() + "/%",
2081            jid.asBareJid().toString()
2082        };
2083        try (final Cursor cursor =
2084                db.rawQuery(
2085                        "SELECT conversations.uuid FROM conversations JOIN accounts ON"
2086                            + " conversations.accountUuid=accounts.uuid WHERE accounts.username=?"
2087                            + " AND accounts.server=? AND (contactJid=? OR contactJid LIKE ?)",
2088                        selectionArgs)) {
2089            if (cursor.getCount() == 0) {
2090                return null;
2091            }
2092            cursor.moveToFirst();
2093            return cursor.getString(0);
2094        }
2095    }
2096
2097    public void updateConversation(final Conversation conversation) {
2098        final SQLiteDatabase db = this.getWritableDatabase();
2099        final String[] args = {conversation.getUuid()};
2100        db.update(
2101                Conversation.TABLENAME,
2102                conversation.getContentValues(),
2103                Conversation.UUID + "=?",
2104                args);
2105    }
2106
2107    public List<Account> getAccounts() {
2108        SQLiteDatabase db = this.getReadableDatabase();
2109        return getAccounts(db);
2110    }
2111
2112    public List<Jid> getAccountJids(final boolean enabledOnly) {
2113        final SQLiteDatabase db = this.getReadableDatabase();
2114        final List<Jid> jids = new ArrayList<>();
2115        final String[] columns = new String[] {Account.USERNAME, Account.SERVER};
2116        final String where = enabledOnly ? "not options & (1 <<1)" : null;
2117        try (final Cursor cursor =
2118                db.query(Account.TABLENAME, columns, where, null, null, null, null)) {
2119            while (cursor != null && cursor.moveToNext()) {
2120                jids.add(Jid.of(cursor.getString(0), cursor.getString(1), null));
2121            }
2122        } catch (final Exception e) {
2123            return jids;
2124        }
2125        return jids;
2126    }
2127
2128    private List<Account> getAccounts(SQLiteDatabase db) {
2129        final List<Account> list = new ArrayList<>();
2130        try (final Cursor cursor =
2131                db.query(Account.TABLENAME, null, null, null, null, null, null)) {
2132            while (cursor != null && cursor.moveToNext()) {
2133                list.add(Account.fromCursor(cursor));
2134            }
2135        }
2136        return list;
2137    }
2138
2139    public boolean updateAccount(Account account) {
2140        final var db = this.getWritableDatabase();
2141        final String[] args = {account.getUuid()};
2142        final int rows =
2143                db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args);
2144        return rows == 1;
2145    }
2146
2147    public boolean deleteAccount(final Account account) {
2148        final var db = this.getWritableDatabase();
2149        final String[] args = {account.getUuid()};
2150        final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args);
2151        return rows == 1;
2152    }
2153
2154    public boolean updateMessage(final Message message, final boolean includeBody) {
2155        final var db = this.getWritableDatabase();
2156        final String[] args = {message.getUuid()};
2157        final var contentValues = message.getContentValues();
2158        contentValues.remove(Message.UUID);
2159        if (!includeBody) {
2160            contentValues.remove(Message.BODY);
2161        }
2162        return db.update(Message.TABLENAME, contentValues, Message.UUID + "=?", args) == 1 &&
2163               db.update("cheogram." + Message.TABLENAME, message.getCheogramContentValues(), Message.UUID + "=?", args) == 1;
2164    }
2165
2166    public boolean updateMessage(Message message, String uuid) {
2167        SQLiteDatabase db = this.getWritableDatabase();
2168        String[] args = {uuid};
2169        return db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + "=?", args) == 1 &&
2170               db.update("cheogram." + Message.TABLENAME, message.getCheogramContentValues(), Message.UUID + "=?", args) == 1;
2171    }
2172
2173
2174    public boolean deleteMessage(String uuid) {
2175        SQLiteDatabase db = this.getWritableDatabase();
2176        String[] args = {uuid};
2177        return db.delete(Message.TABLENAME, Message.UUID + "=?", args) == 1 &&
2178               db.delete("cheogram." + Message.TABLENAME, Message.UUID + "=?", args) == 1;
2179    }
2180
2181    public void readRoster(Roster roster) {
2182        final SQLiteDatabase db = this.getReadableDatabase();
2183        final String[] args = {roster.getAccount().getUuid()};
2184        try (final Cursor cursor =
2185                db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) {
2186            while (cursor.moveToNext()) {
2187                roster.initContact(Contact.fromCursor(cursor));
2188            }
2189        }
2190    }
2191
2192    public void writeRoster(final Roster roster) {
2193        long start = SystemClock.elapsedRealtime();
2194        final Account account = roster.getAccount();
2195        final SQLiteDatabase db = this.getWritableDatabase();
2196        db.beginTransaction();
2197        for (Contact contact : roster.getContacts()) {
2198            if (contact.getOption(Contact.Options.IN_ROSTER)
2199                    || contact.hasAvatarOrPresenceName()
2200                    || contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
2201                db.insert(Contact.TABLENAME, null, contact.getContentValues());
2202            } else {
2203                String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
2204                String[] whereArgs = {account.getUuid(), contact.getJid().toString()};
2205                db.delete(Contact.TABLENAME, where, whereArgs);
2206            }
2207        }
2208        db.setTransactionSuccessful();
2209        db.endTransaction();
2210        account.setRosterVersion(roster.getVersion());
2211        updateAccount(account);
2212        long duration = SystemClock.elapsedRealtime() - start;
2213        Log.d(
2214                Config.LOGTAG,
2215                account.getJid().asBareJid() + ": persisted roster in " + duration + "ms");
2216    }
2217
2218    public void deleteMessagesInConversation(Conversation conversation) {
2219        long start = SystemClock.elapsedRealtime();
2220        final SQLiteDatabase db = this.getWritableDatabase();
2221        db.beginTransaction();
2222        final String[] args = {conversation.getUuid()};
2223        int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
2224        db.delete("cheogram.webxdc_updates", Message.CONVERSATION + "=?", args);
2225        db.setTransactionSuccessful();
2226        db.endTransaction();
2227        Log.d(
2228                Config.LOGTAG,
2229                "deleted "
2230                        + num
2231                        + " messages for "
2232                        + conversation.getJid().asBareJid()
2233                        + " in "
2234                        + (SystemClock.elapsedRealtime() - start)
2235                        + "ms");
2236    }
2237
2238    public void expireOldMessages(long timestamp) {
2239        final String[] args = {String.valueOf(timestamp)};
2240        SQLiteDatabase db = this.getReadableDatabase();
2241        db.beginTransaction();
2242        db.delete(Message.TABLENAME, "timeSent<?", args);
2243        db.delete("cheogram.messages", "timeReceived<?", args);
2244        db.setTransactionSuccessful();
2245        db.endTransaction();
2246    }
2247
2248    public MamReference getLastMessageReceived(Account account) {
2249        Cursor cursor = null;
2250        try {
2251            SQLiteDatabase db = this.getReadableDatabase();
2252            String sql =
2253                    "select messages.timeSent,messages.serverMsgId from accounts join conversations"
2254                        + " on accounts.uuid=conversations.accountUuid join messages on"
2255                        + " conversations.uuid=messages.conversationUuid where accounts.uuid=? and"
2256                        + " (messages.status=0 or messages.carbon=1 or messages.serverMsgId not"
2257                        + " null) and (conversations.mode=0 or (messages.serverMsgId not null and"
2258                        + " messages.type=4)) order by messages.timesent desc limit 1";
2259            String[] args = {account.getUuid()};
2260            cursor = db.rawQuery(sql, args);
2261            if (cursor.getCount() == 0) {
2262                return null;
2263            } else {
2264                cursor.moveToFirst();
2265                return new MamReference(cursor.getLong(0), cursor.getString(1));
2266            }
2267        } catch (Exception e) {
2268            return null;
2269        } finally {
2270            if (cursor != null) {
2271                cursor.close();
2272            }
2273        }
2274    }
2275
2276    public long getLastTimeFingerprintUsed(Account account, String fingerprint) {
2277        String SQL =
2278                "select messages.timeSent from accounts join conversations on"
2279                        + " accounts.uuid=conversations.accountUuid join messages on"
2280                        + " conversations.uuid=messages.conversationUuid where accounts.uuid=? and"
2281                        + " messages.axolotl_fingerprint=? order by messages.timesent desc limit 1";
2282        String[] args = {account.getUuid(), fingerprint};
2283        Cursor cursor = getReadableDatabase().rawQuery(SQL, args);
2284        long time;
2285        if (cursor.moveToFirst()) {
2286            time = cursor.getLong(0);
2287        } else {
2288            time = 0;
2289        }
2290        cursor.close();
2291        return time;
2292    }
2293
2294    public MamReference getLastClearDate(Account account) {
2295        SQLiteDatabase db = this.getReadableDatabase();
2296        String[] columns = {Conversation.ATTRIBUTES};
2297        String selection = Conversation.ACCOUNT + "=?";
2298        String[] args = {account.getUuid()};
2299        Cursor cursor =
2300                db.query(Conversation.TABLENAME, columns, selection, args, null, null, null);
2301        MamReference maxClearDate = new MamReference(0);
2302        while (cursor.moveToNext()) {
2303            try {
2304                final JSONObject o = new JSONObject(cursor.getString(0));
2305                maxClearDate =
2306                        MamReference.max(
2307                                maxClearDate,
2308                                MamReference.fromAttribute(
2309                                        o.getString(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY)));
2310            } catch (Exception e) {
2311                // ignored
2312            }
2313        }
2314        cursor.close();
2315        return maxClearDate;
2316    }
2317
2318    private Cursor getCursorForSession(Account account, SignalProtocolAddress contact) {
2319        final SQLiteDatabase db = this.getReadableDatabase();
2320        String[] selectionArgs = {
2321            account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId())
2322        };
2323        return db.query(
2324                SQLiteAxolotlStore.SESSION_TABLENAME,
2325                null,
2326                SQLiteAxolotlStore.ACCOUNT
2327                        + " = ? AND "
2328                        + SQLiteAxolotlStore.NAME
2329                        + " = ? AND "
2330                        + SQLiteAxolotlStore.DEVICE_ID
2331                        + " = ? ",
2332                selectionArgs,
2333                null,
2334                null,
2335                null);
2336    }
2337
2338    public SessionRecord loadSession(Account account, SignalProtocolAddress contact) {
2339        SessionRecord session = null;
2340        Cursor cursor = getCursorForSession(account, contact);
2341        if (cursor.getCount() != 0) {
2342            cursor.moveToFirst();
2343            try {
2344                session =
2345                        new SessionRecord(
2346                                Base64.decode(
2347                                        cursor.getString(
2348                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2349                                        Base64.DEFAULT));
2350            } catch (IOException e) {
2351                cursor.close();
2352                throw new AssertionError(e);
2353            }
2354        }
2355        cursor.close();
2356        return session;
2357    }
2358
2359    public List<Integer> getSubDeviceSessions(Account account, SignalProtocolAddress contact) {
2360        final SQLiteDatabase db = this.getReadableDatabase();
2361        return getSubDeviceSessions(db, account, contact);
2362    }
2363
2364    private List<Integer> getSubDeviceSessions(
2365            SQLiteDatabase db, Account account, SignalProtocolAddress contact) {
2366        List<Integer> devices = new ArrayList<>();
2367        String[] columns = {SQLiteAxolotlStore.DEVICE_ID};
2368        String[] selectionArgs = {account.getUuid(), contact.getName()};
2369        Cursor cursor =
2370                db.query(
2371                        SQLiteAxolotlStore.SESSION_TABLENAME,
2372                        columns,
2373                        SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.NAME + " = ?",
2374                        selectionArgs,
2375                        null,
2376                        null,
2377                        null);
2378
2379        while (cursor.moveToNext()) {
2380            devices.add(cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID)));
2381        }
2382
2383        cursor.close();
2384        return devices;
2385    }
2386
2387    public List<String> getKnownSignalAddresses(Account account) {
2388        List<String> addresses = new ArrayList<>();
2389        String[] colums = {"DISTINCT " + SQLiteAxolotlStore.NAME};
2390        String[] selectionArgs = {account.getUuid()};
2391        Cursor cursor =
2392                getReadableDatabase()
2393                        .query(
2394                                SQLiteAxolotlStore.SESSION_TABLENAME,
2395                                colums,
2396                                SQLiteAxolotlStore.ACCOUNT + " = ?",
2397                                selectionArgs,
2398                                null,
2399                                null,
2400                                null);
2401        while (cursor.moveToNext()) {
2402            addresses.add(cursor.getString(0));
2403        }
2404        cursor.close();
2405        return addresses;
2406    }
2407
2408    public boolean containsSession(Account account, SignalProtocolAddress contact) {
2409        Cursor cursor = getCursorForSession(account, contact);
2410        int count = cursor.getCount();
2411        cursor.close();
2412        return count != 0;
2413    }
2414
2415    public void storeSession(
2416            Account account, SignalProtocolAddress contact, SessionRecord session) {
2417        SQLiteDatabase db = this.getWritableDatabase();
2418        ContentValues values = new ContentValues();
2419        values.put(SQLiteAxolotlStore.NAME, contact.getName());
2420        values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
2421        values.put(
2422                SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT));
2423        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2424        db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
2425    }
2426
2427    public void deleteSession(Account account, SignalProtocolAddress contact) {
2428        SQLiteDatabase db = this.getWritableDatabase();
2429        deleteSession(db, account, contact);
2430    }
2431
2432    private void deleteSession(SQLiteDatabase db, Account account, SignalProtocolAddress contact) {
2433        String[] args = {
2434            account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId())
2435        };
2436        db.delete(
2437                SQLiteAxolotlStore.SESSION_TABLENAME,
2438                SQLiteAxolotlStore.ACCOUNT
2439                        + " = ? AND "
2440                        + SQLiteAxolotlStore.NAME
2441                        + " = ? AND "
2442                        + SQLiteAxolotlStore.DEVICE_ID
2443                        + " = ? ",
2444                args);
2445    }
2446
2447    public void deleteAllSessions(Account account, SignalProtocolAddress contact) {
2448        SQLiteDatabase db = this.getWritableDatabase();
2449        String[] args = {account.getUuid(), contact.getName()};
2450        db.delete(
2451                SQLiteAxolotlStore.SESSION_TABLENAME,
2452                SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.NAME + " = ?",
2453                args);
2454    }
2455
2456    private Cursor getCursorForPreKey(Account account, int preKeyId) {
2457        SQLiteDatabase db = this.getReadableDatabase();
2458        String[] columns = {SQLiteAxolotlStore.KEY};
2459        String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
2460        Cursor cursor =
2461                db.query(
2462                        SQLiteAxolotlStore.PREKEY_TABLENAME,
2463                        columns,
2464                        SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2465                        selectionArgs,
2466                        null,
2467                        null,
2468                        null);
2469
2470        return cursor;
2471    }
2472
2473    public PreKeyRecord loadPreKey(Account account, int preKeyId) {
2474        PreKeyRecord record = null;
2475        Cursor cursor = getCursorForPreKey(account, preKeyId);
2476        if (cursor.getCount() != 0) {
2477            cursor.moveToFirst();
2478            try {
2479                record =
2480                        new PreKeyRecord(
2481                                Base64.decode(
2482                                        cursor.getString(
2483                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2484                                        Base64.DEFAULT));
2485            } catch (IOException e) {
2486                throw new AssertionError(e);
2487            }
2488        }
2489        cursor.close();
2490        return record;
2491    }
2492
2493    public boolean containsPreKey(Account account, int preKeyId) {
2494        Cursor cursor = getCursorForPreKey(account, preKeyId);
2495        int count = cursor.getCount();
2496        cursor.close();
2497        return count != 0;
2498    }
2499
2500    public void storePreKey(Account account, PreKeyRecord record) {
2501        SQLiteDatabase db = this.getWritableDatabase();
2502        ContentValues values = new ContentValues();
2503        values.put(SQLiteAxolotlStore.ID, record.getId());
2504        values.put(
2505                SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
2506        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2507        db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
2508    }
2509
2510    public int deletePreKey(Account account, int preKeyId) {
2511        SQLiteDatabase db = this.getWritableDatabase();
2512        String[] args = {account.getUuid(), Integer.toString(preKeyId)};
2513        return db.delete(
2514                SQLiteAxolotlStore.PREKEY_TABLENAME,
2515                SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2516                args);
2517    }
2518
2519    private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
2520        SQLiteDatabase db = this.getReadableDatabase();
2521        String[] columns = {SQLiteAxolotlStore.KEY};
2522        String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
2523        Cursor cursor =
2524                db.query(
2525                        SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2526                        columns,
2527                        SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2528                        selectionArgs,
2529                        null,
2530                        null,
2531                        null);
2532
2533        return cursor;
2534    }
2535
2536    public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
2537        SignedPreKeyRecord record = null;
2538        Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
2539        if (cursor.getCount() != 0) {
2540            cursor.moveToFirst();
2541            try {
2542                record =
2543                        new SignedPreKeyRecord(
2544                                Base64.decode(
2545                                        cursor.getString(
2546                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2547                                        Base64.DEFAULT));
2548            } catch (IOException e) {
2549                throw new AssertionError(e);
2550            }
2551        }
2552        cursor.close();
2553        return record;
2554    }
2555
2556    public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
2557        List<SignedPreKeyRecord> prekeys = new ArrayList<>();
2558        SQLiteDatabase db = this.getReadableDatabase();
2559        String[] columns = {SQLiteAxolotlStore.KEY};
2560        String[] selectionArgs = {account.getUuid()};
2561        Cursor cursor =
2562                db.query(
2563                        SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2564                        columns,
2565                        SQLiteAxolotlStore.ACCOUNT + "=?",
2566                        selectionArgs,
2567                        null,
2568                        null,
2569                        null);
2570
2571        while (cursor.moveToNext()) {
2572            try {
2573                prekeys.add(
2574                        new SignedPreKeyRecord(
2575                                Base64.decode(
2576                                        cursor.getString(
2577                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2578                                        Base64.DEFAULT)));
2579            } catch (IOException ignored) {
2580            }
2581        }
2582        cursor.close();
2583        return prekeys;
2584    }
2585
2586    public int getSignedPreKeysCount(Account account) {
2587        String[] columns = {"count(" + SQLiteAxolotlStore.KEY + ")"};
2588        String[] selectionArgs = {account.getUuid()};
2589        SQLiteDatabase db = this.getReadableDatabase();
2590        Cursor cursor =
2591                db.query(
2592                        SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2593                        columns,
2594                        SQLiteAxolotlStore.ACCOUNT + "=?",
2595                        selectionArgs,
2596                        null,
2597                        null,
2598                        null);
2599        final int count;
2600        if (cursor.moveToFirst()) {
2601            count = cursor.getInt(0);
2602        } else {
2603            count = 0;
2604        }
2605        cursor.close();
2606        return count;
2607    }
2608
2609    public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
2610        Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
2611        int count = cursor.getCount();
2612        cursor.close();
2613        return count != 0;
2614    }
2615
2616    public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
2617        SQLiteDatabase db = this.getWritableDatabase();
2618        ContentValues values = new ContentValues();
2619        values.put(SQLiteAxolotlStore.ID, record.getId());
2620        values.put(
2621                SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
2622        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2623        db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
2624    }
2625
2626    public void deleteSignedPreKey(Account account, int signedPreKeyId) {
2627        SQLiteDatabase db = this.getWritableDatabase();
2628        String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
2629        db.delete(
2630                SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2631                SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2632                args);
2633    }
2634
2635    private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
2636        final SQLiteDatabase db = this.getReadableDatabase();
2637        return getIdentityKeyCursor(db, account, name, own);
2638    }
2639
2640    private Cursor getIdentityKeyCursor(
2641            SQLiteDatabase db, Account account, String name, boolean own) {
2642        return getIdentityKeyCursor(db, account, name, own, null);
2643    }
2644
2645    private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
2646        final SQLiteDatabase db = this.getReadableDatabase();
2647        return getIdentityKeyCursor(db, account, fingerprint);
2648    }
2649
2650    private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String fingerprint) {
2651        return getIdentityKeyCursor(db, account, null, null, fingerprint);
2652    }
2653
2654    private Cursor getIdentityKeyCursor(
2655            SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
2656        String[] columns = {
2657            SQLiteAxolotlStore.TRUST,
2658            SQLiteAxolotlStore.ACTIVE,
2659            SQLiteAxolotlStore.LAST_ACTIVATION,
2660            SQLiteAxolotlStore.KEY
2661        };
2662        ArrayList<String> selectionArgs = new ArrayList<>(4);
2663        selectionArgs.add(account.getUuid());
2664        String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?";
2665        if (name != null) {
2666            selectionArgs.add(name);
2667            selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?";
2668        }
2669        if (fingerprint != null) {
2670            selectionArgs.add(fingerprint);
2671            selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?";
2672        }
2673        if (own != null) {
2674            selectionArgs.add(own ? "1" : "0");
2675            selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?";
2676        }
2677        Cursor cursor =
2678                db.query(
2679                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2680                        columns,
2681                        selectionString,
2682                        selectionArgs.toArray(new String[selectionArgs.size()]),
2683                        null,
2684                        null,
2685                        null);
2686
2687        return cursor;
2688    }
2689
2690    public IdentityKeyPair loadOwnIdentityKeyPair(Account account) {
2691        SQLiteDatabase db = getReadableDatabase();
2692        return loadOwnIdentityKeyPair(db, account);
2693    }
2694
2695    private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) {
2696        String name = account.getJid().asBareJid().toString();
2697        IdentityKeyPair identityKeyPair = null;
2698        Cursor cursor = getIdentityKeyCursor(db, account, name, true);
2699        if (cursor.getCount() != 0) {
2700            cursor.moveToFirst();
2701            try {
2702                identityKeyPair =
2703                        new IdentityKeyPair(
2704                                Base64.decode(
2705                                        cursor.getString(
2706                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2707                                        Base64.DEFAULT));
2708            } catch (InvalidKeyException e) {
2709                Log.d(
2710                        Config.LOGTAG,
2711                        AxolotlService.getLogprefix(account)
2712                                + "Encountered invalid IdentityKey in database for account"
2713                                + account.getJid().asBareJid()
2714                                + ", address: "
2715                                + name);
2716            }
2717        }
2718        cursor.close();
2719
2720        return identityKeyPair;
2721    }
2722
2723    public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
2724        return loadIdentityKeys(account, name, null);
2725    }
2726
2727    public Set<IdentityKey> loadIdentityKeys(
2728            Account account, String name, FingerprintStatus status) {
2729        Set<IdentityKey> identityKeys = new HashSet<>();
2730        Cursor cursor = getIdentityKeyCursor(account, name, false);
2731
2732        while (cursor.moveToNext()) {
2733            if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) {
2734                continue;
2735            }
2736            try {
2737                String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY));
2738                if (key != null) {
2739                    identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
2740                } else {
2741                    Log.d(
2742                            Config.LOGTAG,
2743                            AxolotlService.getLogprefix(account)
2744                                    + "Missing key (possibly preverified) in database for account"
2745                                    + account.getJid().asBareJid()
2746                                    + ", address: "
2747                                    + name);
2748                }
2749            } catch (InvalidKeyException e) {
2750                Log.d(
2751                        Config.LOGTAG,
2752                        AxolotlService.getLogprefix(account)
2753                                + "Encountered invalid IdentityKey in database for account"
2754                                + account.getJid().asBareJid()
2755                                + ", address: "
2756                                + name);
2757            }
2758        }
2759        cursor.close();
2760
2761        return identityKeys;
2762    }
2763
2764    public long numTrustedKeys(Account account, String name) {
2765        SQLiteDatabase db = getReadableDatabase();
2766        String[] args = {
2767            account.getUuid(),
2768            name,
2769            FingerprintStatus.Trust.TRUSTED.toString(),
2770            FingerprintStatus.Trust.VERIFIED.toString(),
2771            FingerprintStatus.Trust.VERIFIED_X509.toString()
2772        };
2773        return DatabaseUtils.queryNumEntries(
2774                db,
2775                SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2776                SQLiteAxolotlStore.ACCOUNT
2777                        + " = ?"
2778                        + " AND "
2779                        + SQLiteAxolotlStore.NAME
2780                        + " = ?"
2781                        + " AND ("
2782                        + SQLiteAxolotlStore.TRUST
2783                        + " = ? OR "
2784                        + SQLiteAxolotlStore.TRUST
2785                        + " = ? OR "
2786                        + SQLiteAxolotlStore.TRUST
2787                        + " = ?)"
2788                        + " AND "
2789                        + SQLiteAxolotlStore.ACTIVE
2790                        + " > 0",
2791                args);
2792    }
2793
2794    private void storeIdentityKey(
2795            Account account,
2796            String name,
2797            boolean own,
2798            String fingerprint,
2799            String base64Serialized,
2800            FingerprintStatus status) {
2801        SQLiteDatabase db = this.getWritableDatabase();
2802        ContentValues values = new ContentValues();
2803        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2804        values.put(SQLiteAxolotlStore.NAME, name);
2805        values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
2806        values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
2807        values.put(SQLiteAxolotlStore.KEY, base64Serialized);
2808        values.putAll(status.toContentValues());
2809        String where =
2810                SQLiteAxolotlStore.ACCOUNT
2811                        + "=? AND "
2812                        + SQLiteAxolotlStore.NAME
2813                        + "=? AND "
2814                        + SQLiteAxolotlStore.FINGERPRINT
2815                        + " =?";
2816        String[] whereArgs = {account.getUuid(), name, fingerprint};
2817        int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, where, whereArgs);
2818        if (rows == 0) {
2819            db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
2820        }
2821    }
2822
2823    public void storePreVerification(
2824            Account account, String name, String fingerprint, FingerprintStatus status) {
2825        SQLiteDatabase db = this.getWritableDatabase();
2826        ContentValues values = new ContentValues();
2827        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2828        values.put(SQLiteAxolotlStore.NAME, name);
2829        values.put(SQLiteAxolotlStore.OWN, 0);
2830        values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
2831        values.putAll(status.toContentValues());
2832        db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
2833    }
2834
2835    public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) {
2836        Cursor cursor = getIdentityKeyCursor(account, fingerprint);
2837        final FingerprintStatus status;
2838        if (cursor.getCount() > 0) {
2839            cursor.moveToFirst();
2840            status = FingerprintStatus.fromCursor(cursor);
2841        } else {
2842            status = null;
2843        }
2844        cursor.close();
2845        return status;
2846    }
2847
2848    public boolean setIdentityKeyTrust(
2849            Account account, String fingerprint, FingerprintStatus fingerprintStatus) {
2850        SQLiteDatabase db = this.getWritableDatabase();
2851        return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus);
2852    }
2853
2854    private boolean setIdentityKeyTrust(
2855            SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) {
2856        String[] selectionArgs = {account.getUuid(), fingerprint};
2857        int rows =
2858                db.update(
2859                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2860                        status.toContentValues(),
2861                        SQLiteAxolotlStore.ACCOUNT
2862                                + " = ? AND "
2863                                + SQLiteAxolotlStore.FINGERPRINT
2864                                + " = ? ",
2865                        selectionArgs);
2866        return rows == 1;
2867    }
2868
2869    public boolean setIdentityKeyCertificate(
2870            Account account, String fingerprint, X509Certificate x509Certificate) {
2871        SQLiteDatabase db = this.getWritableDatabase();
2872        String[] selectionArgs = {account.getUuid(), fingerprint};
2873        try {
2874            ContentValues values = new ContentValues();
2875            values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded());
2876            return db.update(
2877                            SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2878                            values,
2879                            SQLiteAxolotlStore.ACCOUNT
2880                                    + " = ? AND "
2881                                    + SQLiteAxolotlStore.FINGERPRINT
2882                                    + " = ? ",
2883                            selectionArgs)
2884                    == 1;
2885        } catch (CertificateEncodingException e) {
2886            Log.d(Config.LOGTAG, "could not encode certificate");
2887            return false;
2888        }
2889    }
2890
2891    public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) {
2892        SQLiteDatabase db = this.getReadableDatabase();
2893        String[] selectionArgs = {account.getUuid(), fingerprint};
2894        String[] colums = {SQLiteAxolotlStore.CERTIFICATE};
2895        String selection =
2896                SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? ";
2897        Cursor cursor =
2898                db.query(
2899                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2900                        colums,
2901                        selection,
2902                        selectionArgs,
2903                        null,
2904                        null,
2905                        null);
2906        if (cursor.getCount() < 1) {
2907            return null;
2908        } else {
2909            cursor.moveToFirst();
2910            byte[] certificate =
2911                    cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE));
2912            cursor.close();
2913            if (certificate == null || certificate.length == 0) {
2914                return null;
2915            }
2916            try {
2917                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
2918                return (X509Certificate)
2919                        certificateFactory.generateCertificate(
2920                                new ByteArrayInputStream(certificate));
2921            } catch (CertificateException e) {
2922                Log.d(Config.LOGTAG, "certificate exception " + e.getMessage());
2923                return null;
2924            }
2925        }
2926    }
2927
2928    public void storeIdentityKey(
2929            Account account, String name, IdentityKey identityKey, FingerprintStatus status) {
2930        storeIdentityKey(
2931                account,
2932                name,
2933                false,
2934                CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()),
2935                Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT),
2936                status);
2937    }
2938
2939    public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
2940        storeIdentityKey(
2941                account,
2942                account.getJid().asBareJid().toString(),
2943                true,
2944                CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()),
2945                Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT),
2946                FingerprintStatus.createActiveVerified(false));
2947    }
2948
2949    private void recreateAxolotlDb(SQLiteDatabase db) {
2950        Log.d(
2951                Config.LOGTAG,
2952                AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<");
2953        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME);
2954        db.execSQL(CREATE_SESSIONS_STATEMENT);
2955        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME);
2956        db.execSQL(CREATE_PREKEYS_STATEMENT);
2957        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
2958        db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
2959        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME);
2960        db.execSQL(CREATE_IDENTITIES_STATEMENT);
2961    }
2962
2963    public void wipeAxolotlDb(Account account) {
2964        String accountName = account.getUuid();
2965        Log.d(
2966                Config.LOGTAG,
2967                AxolotlService.getLogprefix(account)
2968                        + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT "
2969                        + accountName
2970                        + " <<<");
2971        SQLiteDatabase db = this.getWritableDatabase();
2972        String[] deleteArgs = {accountName};
2973        db.delete(
2974                SQLiteAxolotlStore.SESSION_TABLENAME,
2975                SQLiteAxolotlStore.ACCOUNT + " = ?",
2976                deleteArgs);
2977        db.delete(
2978                SQLiteAxolotlStore.PREKEY_TABLENAME,
2979                SQLiteAxolotlStore.ACCOUNT + " = ?",
2980                deleteArgs);
2981        db.delete(
2982                SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2983                SQLiteAxolotlStore.ACCOUNT + " = ?",
2984                deleteArgs);
2985        db.delete(
2986                SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2987                SQLiteAxolotlStore.ACCOUNT + " = ?",
2988                deleteArgs);
2989    }
2990
2991    public List<ShortcutService.FrequentContact> getFrequentContacts(final int days) {
2992        final var db = this.getReadableDatabase();
2993        final String SQL =
2994                "select "
2995                        + Conversation.TABLENAME
2996                        + "."
2997                        + Conversation.UUID
2998                        + ","
2999                        + Conversation.TABLENAME
3000                        + "."
3001                        + Conversation.ACCOUNT
3002                        + ","
3003                        + Conversation.TABLENAME
3004                        + "."
3005                        + Conversation.CONTACTJID
3006                        + " from "
3007                        + Conversation.TABLENAME
3008                        + " join "
3009                        + Message.TABLENAME
3010                        + " on conversations.uuid=messages.conversationUuid where"
3011                        + " messages.status!=0 and carbon==0  and conversations.mode=0 and"
3012                        + " messages.timeSent>=? group by conversations.uuid order by count(body)"
3013                        + " desc limit 4;";
3014        String[] whereArgs =
3015                new String[] {
3016                    String.valueOf(System.currentTimeMillis() - (Config.MILLISECONDS_IN_DAY * days))
3017                };
3018        Cursor cursor = db.rawQuery(SQL, whereArgs);
3019        ArrayList<ShortcutService.FrequentContact> contacts = new ArrayList<>();
3020        while (cursor.moveToNext()) {
3021            try {
3022                contacts.add(
3023                        new ShortcutService.FrequentContact(
3024                                cursor.getString(0),
3025                                cursor.getString(1),
3026                                Jid.of(cursor.getString(2))));
3027            } catch (final Exception e) {
3028                Log.e(Config.LOGTAG, "could not create frequent contact", e);
3029            }
3030        }
3031        cursor.close();
3032        return contacts;
3033    }
3034}