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