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 eu.siacs.conversations.Config;
  15import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  16import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  17import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
  18import eu.siacs.conversations.entities.Account;
  19import eu.siacs.conversations.entities.Contact;
  20import eu.siacs.conversations.entities.Conversation;
  21import eu.siacs.conversations.entities.Message;
  22import eu.siacs.conversations.entities.PresenceTemplate;
  23import eu.siacs.conversations.entities.Roster;
  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 void readRoster(Roster roster) {
1813        final SQLiteDatabase db = this.getReadableDatabase();
1814        final String[] args = {roster.getAccount().getUuid()};
1815        try (final Cursor cursor =
1816                db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) {
1817            while (cursor.moveToNext()) {
1818                roster.initContact(Contact.fromCursor(cursor));
1819            }
1820        }
1821    }
1822
1823    public void writeRoster(final Roster roster) {
1824        long start = SystemClock.elapsedRealtime();
1825        final Account account = roster.getAccount();
1826        final SQLiteDatabase db = this.getWritableDatabase();
1827        db.beginTransaction();
1828        for (Contact contact : roster.getContacts()) {
1829            if (contact.getOption(Contact.Options.IN_ROSTER)
1830                    || contact.hasAvatarOrPresenceName()
1831                    || contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
1832                db.insert(Contact.TABLENAME, null, contact.getContentValues());
1833            } else {
1834                String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
1835                String[] whereArgs = {account.getUuid(), contact.getJid().toString()};
1836                db.delete(Contact.TABLENAME, where, whereArgs);
1837            }
1838        }
1839        db.setTransactionSuccessful();
1840        db.endTransaction();
1841        account.setRosterVersion(roster.getVersion());
1842        updateAccount(account);
1843        long duration = SystemClock.elapsedRealtime() - start;
1844        Log.d(
1845                Config.LOGTAG,
1846                account.getJid().asBareJid() + ": persisted roster in " + duration + "ms");
1847    }
1848
1849    public void deleteMessagesInConversation(Conversation conversation) {
1850        long start = SystemClock.elapsedRealtime();
1851        final SQLiteDatabase db = this.getWritableDatabase();
1852        db.beginTransaction();
1853        final String[] args = {conversation.getUuid()};
1854        int num = db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
1855        db.setTransactionSuccessful();
1856        db.endTransaction();
1857        Log.d(
1858                Config.LOGTAG,
1859                "deleted "
1860                        + num
1861                        + " messages for "
1862                        + conversation.getJid().asBareJid()
1863                        + " in "
1864                        + (SystemClock.elapsedRealtime() - start)
1865                        + "ms");
1866    }
1867
1868    public void expireOldMessages(long timestamp) {
1869        final String[] args = {String.valueOf(timestamp)};
1870        SQLiteDatabase db = this.getReadableDatabase();
1871        db.beginTransaction();
1872        db.delete(Message.TABLENAME, "timeSent<?", args);
1873        db.setTransactionSuccessful();
1874        db.endTransaction();
1875    }
1876
1877    public MamReference getLastMessageReceived(Account account) {
1878        Cursor cursor = null;
1879        try {
1880            SQLiteDatabase db = this.getReadableDatabase();
1881            String sql =
1882                    "select messages.timeSent,messages.serverMsgId from accounts join conversations"
1883                        + " on accounts.uuid=conversations.accountUuid join messages on"
1884                        + " conversations.uuid=messages.conversationUuid where accounts.uuid=? and"
1885                        + " (messages.status=0 or messages.carbon=1 or messages.serverMsgId not"
1886                        + " null) and (conversations.mode=0 or (messages.serverMsgId not null and"
1887                        + " messages.type=4)) order by messages.timesent desc limit 1";
1888            String[] args = {account.getUuid()};
1889            cursor = db.rawQuery(sql, args);
1890            if (cursor.getCount() == 0) {
1891                return null;
1892            } else {
1893                cursor.moveToFirst();
1894                return new MamReference(cursor.getLong(0), cursor.getString(1));
1895            }
1896        } catch (Exception e) {
1897            return null;
1898        } finally {
1899            if (cursor != null) {
1900                cursor.close();
1901            }
1902        }
1903    }
1904
1905    public long getLastTimeFingerprintUsed(Account account, String fingerprint) {
1906        String SQL =
1907                "select messages.timeSent from accounts join conversations on"
1908                        + " accounts.uuid=conversations.accountUuid join messages on"
1909                        + " conversations.uuid=messages.conversationUuid where accounts.uuid=? and"
1910                        + " messages.axolotl_fingerprint=? order by messages.timesent desc limit 1";
1911        String[] args = {account.getUuid(), fingerprint};
1912        Cursor cursor = getReadableDatabase().rawQuery(SQL, args);
1913        long time;
1914        if (cursor.moveToFirst()) {
1915            time = cursor.getLong(0);
1916        } else {
1917            time = 0;
1918        }
1919        cursor.close();
1920        return time;
1921    }
1922
1923    public MamReference getLastClearDate(Account account) {
1924        SQLiteDatabase db = this.getReadableDatabase();
1925        String[] columns = {Conversation.ATTRIBUTES};
1926        String selection = Conversation.ACCOUNT + "=?";
1927        String[] args = {account.getUuid()};
1928        Cursor cursor =
1929                db.query(Conversation.TABLENAME, columns, selection, args, null, null, null);
1930        MamReference maxClearDate = new MamReference(0);
1931        while (cursor.moveToNext()) {
1932            try {
1933                final JSONObject o = new JSONObject(cursor.getString(0));
1934                maxClearDate =
1935                        MamReference.max(
1936                                maxClearDate,
1937                                MamReference.fromAttribute(
1938                                        o.getString(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY)));
1939            } catch (Exception e) {
1940                // ignored
1941            }
1942        }
1943        cursor.close();
1944        return maxClearDate;
1945    }
1946
1947    private Cursor getCursorForSession(Account account, SignalProtocolAddress contact) {
1948        final SQLiteDatabase db = this.getReadableDatabase();
1949        String[] selectionArgs = {
1950            account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId())
1951        };
1952        return db.query(
1953                SQLiteAxolotlStore.SESSION_TABLENAME,
1954                null,
1955                SQLiteAxolotlStore.ACCOUNT
1956                        + " = ? AND "
1957                        + SQLiteAxolotlStore.NAME
1958                        + " = ? AND "
1959                        + SQLiteAxolotlStore.DEVICE_ID
1960                        + " = ? ",
1961                selectionArgs,
1962                null,
1963                null,
1964                null);
1965    }
1966
1967    public SessionRecord loadSession(Account account, SignalProtocolAddress contact) {
1968        SessionRecord session = null;
1969        Cursor cursor = getCursorForSession(account, contact);
1970        if (cursor.getCount() != 0) {
1971            cursor.moveToFirst();
1972            try {
1973                session =
1974                        new SessionRecord(
1975                                Base64.decode(
1976                                        cursor.getString(
1977                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
1978                                        Base64.DEFAULT));
1979            } catch (IOException e) {
1980                cursor.close();
1981                throw new AssertionError(e);
1982            }
1983        }
1984        cursor.close();
1985        return session;
1986    }
1987
1988    public List<Integer> getSubDeviceSessions(Account account, SignalProtocolAddress contact) {
1989        final SQLiteDatabase db = this.getReadableDatabase();
1990        return getSubDeviceSessions(db, account, contact);
1991    }
1992
1993    private List<Integer> getSubDeviceSessions(
1994            SQLiteDatabase db, Account account, SignalProtocolAddress contact) {
1995        List<Integer> devices = new ArrayList<>();
1996        String[] columns = {SQLiteAxolotlStore.DEVICE_ID};
1997        String[] selectionArgs = {account.getUuid(), contact.getName()};
1998        Cursor cursor =
1999                db.query(
2000                        SQLiteAxolotlStore.SESSION_TABLENAME,
2001                        columns,
2002                        SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.NAME + " = ?",
2003                        selectionArgs,
2004                        null,
2005                        null,
2006                        null);
2007
2008        while (cursor.moveToNext()) {
2009            devices.add(cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID)));
2010        }
2011
2012        cursor.close();
2013        return devices;
2014    }
2015
2016    public List<String> getKnownSignalAddresses(Account account) {
2017        List<String> addresses = new ArrayList<>();
2018        String[] colums = {"DISTINCT " + SQLiteAxolotlStore.NAME};
2019        String[] selectionArgs = {account.getUuid()};
2020        Cursor cursor =
2021                getReadableDatabase()
2022                        .query(
2023                                SQLiteAxolotlStore.SESSION_TABLENAME,
2024                                colums,
2025                                SQLiteAxolotlStore.ACCOUNT + " = ?",
2026                                selectionArgs,
2027                                null,
2028                                null,
2029                                null);
2030        while (cursor.moveToNext()) {
2031            addresses.add(cursor.getString(0));
2032        }
2033        cursor.close();
2034        return addresses;
2035    }
2036
2037    public boolean containsSession(Account account, SignalProtocolAddress contact) {
2038        Cursor cursor = getCursorForSession(account, contact);
2039        int count = cursor.getCount();
2040        cursor.close();
2041        return count != 0;
2042    }
2043
2044    public void storeSession(
2045            Account account, SignalProtocolAddress contact, SessionRecord session) {
2046        SQLiteDatabase db = this.getWritableDatabase();
2047        ContentValues values = new ContentValues();
2048        values.put(SQLiteAxolotlStore.NAME, contact.getName());
2049        values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
2050        values.put(
2051                SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT));
2052        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2053        db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
2054    }
2055
2056    public void deleteSession(Account account, SignalProtocolAddress contact) {
2057        SQLiteDatabase db = this.getWritableDatabase();
2058        deleteSession(db, account, contact);
2059    }
2060
2061    private void deleteSession(SQLiteDatabase db, Account account, SignalProtocolAddress contact) {
2062        String[] args = {
2063            account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId())
2064        };
2065        db.delete(
2066                SQLiteAxolotlStore.SESSION_TABLENAME,
2067                SQLiteAxolotlStore.ACCOUNT
2068                        + " = ? AND "
2069                        + SQLiteAxolotlStore.NAME
2070                        + " = ? AND "
2071                        + SQLiteAxolotlStore.DEVICE_ID
2072                        + " = ? ",
2073                args);
2074    }
2075
2076    public void deleteAllSessions(Account account, SignalProtocolAddress contact) {
2077        SQLiteDatabase db = this.getWritableDatabase();
2078        String[] args = {account.getUuid(), contact.getName()};
2079        db.delete(
2080                SQLiteAxolotlStore.SESSION_TABLENAME,
2081                SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.NAME + " = ?",
2082                args);
2083    }
2084
2085    private Cursor getCursorForPreKey(Account account, int preKeyId) {
2086        SQLiteDatabase db = this.getReadableDatabase();
2087        String[] columns = {SQLiteAxolotlStore.KEY};
2088        String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
2089        Cursor cursor =
2090                db.query(
2091                        SQLiteAxolotlStore.PREKEY_TABLENAME,
2092                        columns,
2093                        SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2094                        selectionArgs,
2095                        null,
2096                        null,
2097                        null);
2098
2099        return cursor;
2100    }
2101
2102    public PreKeyRecord loadPreKey(Account account, int preKeyId) {
2103        PreKeyRecord record = null;
2104        Cursor cursor = getCursorForPreKey(account, preKeyId);
2105        if (cursor.getCount() != 0) {
2106            cursor.moveToFirst();
2107            try {
2108                record =
2109                        new PreKeyRecord(
2110                                Base64.decode(
2111                                        cursor.getString(
2112                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2113                                        Base64.DEFAULT));
2114            } catch (IOException e) {
2115                throw new AssertionError(e);
2116            }
2117        }
2118        cursor.close();
2119        return record;
2120    }
2121
2122    public boolean containsPreKey(Account account, int preKeyId) {
2123        Cursor cursor = getCursorForPreKey(account, preKeyId);
2124        int count = cursor.getCount();
2125        cursor.close();
2126        return count != 0;
2127    }
2128
2129    public void storePreKey(Account account, PreKeyRecord record) {
2130        SQLiteDatabase db = this.getWritableDatabase();
2131        ContentValues values = new ContentValues();
2132        values.put(SQLiteAxolotlStore.ID, record.getId());
2133        values.put(
2134                SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
2135        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2136        db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
2137    }
2138
2139    public int deletePreKey(Account account, int preKeyId) {
2140        SQLiteDatabase db = this.getWritableDatabase();
2141        String[] args = {account.getUuid(), Integer.toString(preKeyId)};
2142        return db.delete(
2143                SQLiteAxolotlStore.PREKEY_TABLENAME,
2144                SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2145                args);
2146    }
2147
2148    private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
2149        SQLiteDatabase db = this.getReadableDatabase();
2150        String[] columns = {SQLiteAxolotlStore.KEY};
2151        String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
2152        Cursor cursor =
2153                db.query(
2154                        SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2155                        columns,
2156                        SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2157                        selectionArgs,
2158                        null,
2159                        null,
2160                        null);
2161
2162        return cursor;
2163    }
2164
2165    public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
2166        SignedPreKeyRecord record = null;
2167        Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
2168        if (cursor.getCount() != 0) {
2169            cursor.moveToFirst();
2170            try {
2171                record =
2172                        new SignedPreKeyRecord(
2173                                Base64.decode(
2174                                        cursor.getString(
2175                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2176                                        Base64.DEFAULT));
2177            } catch (IOException e) {
2178                throw new AssertionError(e);
2179            }
2180        }
2181        cursor.close();
2182        return record;
2183    }
2184
2185    public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
2186        List<SignedPreKeyRecord> prekeys = new ArrayList<>();
2187        SQLiteDatabase db = this.getReadableDatabase();
2188        String[] columns = {SQLiteAxolotlStore.KEY};
2189        String[] selectionArgs = {account.getUuid()};
2190        Cursor cursor =
2191                db.query(
2192                        SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2193                        columns,
2194                        SQLiteAxolotlStore.ACCOUNT + "=?",
2195                        selectionArgs,
2196                        null,
2197                        null,
2198                        null);
2199
2200        while (cursor.moveToNext()) {
2201            try {
2202                prekeys.add(
2203                        new SignedPreKeyRecord(
2204                                Base64.decode(
2205                                        cursor.getString(
2206                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2207                                        Base64.DEFAULT)));
2208            } catch (IOException ignored) {
2209            }
2210        }
2211        cursor.close();
2212        return prekeys;
2213    }
2214
2215    public int getSignedPreKeysCount(Account account) {
2216        String[] columns = {"count(" + SQLiteAxolotlStore.KEY + ")"};
2217        String[] selectionArgs = {account.getUuid()};
2218        SQLiteDatabase db = this.getReadableDatabase();
2219        Cursor cursor =
2220                db.query(
2221                        SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2222                        columns,
2223                        SQLiteAxolotlStore.ACCOUNT + "=?",
2224                        selectionArgs,
2225                        null,
2226                        null,
2227                        null);
2228        final int count;
2229        if (cursor.moveToFirst()) {
2230            count = cursor.getInt(0);
2231        } else {
2232            count = 0;
2233        }
2234        cursor.close();
2235        return count;
2236    }
2237
2238    public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
2239        Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
2240        int count = cursor.getCount();
2241        cursor.close();
2242        return count != 0;
2243    }
2244
2245    public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
2246        SQLiteDatabase db = this.getWritableDatabase();
2247        ContentValues values = new ContentValues();
2248        values.put(SQLiteAxolotlStore.ID, record.getId());
2249        values.put(
2250                SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT));
2251        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2252        db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
2253    }
2254
2255    public void deleteSignedPreKey(Account account, int signedPreKeyId) {
2256        SQLiteDatabase db = this.getWritableDatabase();
2257        String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
2258        db.delete(
2259                SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2260                SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
2261                args);
2262    }
2263
2264    private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
2265        final SQLiteDatabase db = this.getReadableDatabase();
2266        return getIdentityKeyCursor(db, account, name, own);
2267    }
2268
2269    private Cursor getIdentityKeyCursor(
2270            SQLiteDatabase db, Account account, String name, boolean own) {
2271        return getIdentityKeyCursor(db, account, name, own, null);
2272    }
2273
2274    private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
2275        final SQLiteDatabase db = this.getReadableDatabase();
2276        return getIdentityKeyCursor(db, account, fingerprint);
2277    }
2278
2279    private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String fingerprint) {
2280        return getIdentityKeyCursor(db, account, null, null, fingerprint);
2281    }
2282
2283    private Cursor getIdentityKeyCursor(
2284            SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
2285        String[] columns = {
2286            SQLiteAxolotlStore.TRUST,
2287            SQLiteAxolotlStore.ACTIVE,
2288            SQLiteAxolotlStore.LAST_ACTIVATION,
2289            SQLiteAxolotlStore.KEY
2290        };
2291        ArrayList<String> selectionArgs = new ArrayList<>(4);
2292        selectionArgs.add(account.getUuid());
2293        String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?";
2294        if (name != null) {
2295            selectionArgs.add(name);
2296            selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?";
2297        }
2298        if (fingerprint != null) {
2299            selectionArgs.add(fingerprint);
2300            selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?";
2301        }
2302        if (own != null) {
2303            selectionArgs.add(own ? "1" : "0");
2304            selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?";
2305        }
2306        Cursor cursor =
2307                db.query(
2308                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2309                        columns,
2310                        selectionString,
2311                        selectionArgs.toArray(new String[selectionArgs.size()]),
2312                        null,
2313                        null,
2314                        null);
2315
2316        return cursor;
2317    }
2318
2319    public IdentityKeyPair loadOwnIdentityKeyPair(Account account) {
2320        SQLiteDatabase db = getReadableDatabase();
2321        return loadOwnIdentityKeyPair(db, account);
2322    }
2323
2324    private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) {
2325        String name = account.getJid().asBareJid().toString();
2326        IdentityKeyPair identityKeyPair = null;
2327        Cursor cursor = getIdentityKeyCursor(db, account, name, true);
2328        if (cursor.getCount() != 0) {
2329            cursor.moveToFirst();
2330            try {
2331                identityKeyPair =
2332                        new IdentityKeyPair(
2333                                Base64.decode(
2334                                        cursor.getString(
2335                                                cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),
2336                                        Base64.DEFAULT));
2337            } catch (InvalidKeyException e) {
2338                Log.d(
2339                        Config.LOGTAG,
2340                        AxolotlService.getLogprefix(account)
2341                                + "Encountered invalid IdentityKey in database for account"
2342                                + account.getJid().asBareJid()
2343                                + ", address: "
2344                                + name);
2345            }
2346        }
2347        cursor.close();
2348
2349        return identityKeyPair;
2350    }
2351
2352    public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
2353        return loadIdentityKeys(account, name, null);
2354    }
2355
2356    public Set<IdentityKey> loadIdentityKeys(
2357            Account account, String name, FingerprintStatus status) {
2358        Set<IdentityKey> identityKeys = new HashSet<>();
2359        Cursor cursor = getIdentityKeyCursor(account, name, false);
2360
2361        while (cursor.moveToNext()) {
2362            if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) {
2363                continue;
2364            }
2365            try {
2366                String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY));
2367                if (key != null) {
2368                    identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
2369                } else {
2370                    Log.d(
2371                            Config.LOGTAG,
2372                            AxolotlService.getLogprefix(account)
2373                                    + "Missing key (possibly preverified) in database for account"
2374                                    + account.getJid().asBareJid()
2375                                    + ", address: "
2376                                    + name);
2377                }
2378            } catch (InvalidKeyException e) {
2379                Log.d(
2380                        Config.LOGTAG,
2381                        AxolotlService.getLogprefix(account)
2382                                + "Encountered invalid IdentityKey in database for account"
2383                                + account.getJid().asBareJid()
2384                                + ", address: "
2385                                + name);
2386            }
2387        }
2388        cursor.close();
2389
2390        return identityKeys;
2391    }
2392
2393    public long numTrustedKeys(Account account, String name) {
2394        SQLiteDatabase db = getReadableDatabase();
2395        String[] args = {
2396            account.getUuid(),
2397            name,
2398            FingerprintStatus.Trust.TRUSTED.toString(),
2399            FingerprintStatus.Trust.VERIFIED.toString(),
2400            FingerprintStatus.Trust.VERIFIED_X509.toString()
2401        };
2402        return DatabaseUtils.queryNumEntries(
2403                db,
2404                SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2405                SQLiteAxolotlStore.ACCOUNT
2406                        + " = ?"
2407                        + " AND "
2408                        + SQLiteAxolotlStore.NAME
2409                        + " = ?"
2410                        + " AND ("
2411                        + SQLiteAxolotlStore.TRUST
2412                        + " = ? OR "
2413                        + SQLiteAxolotlStore.TRUST
2414                        + " = ? OR "
2415                        + SQLiteAxolotlStore.TRUST
2416                        + " = ?)"
2417                        + " AND "
2418                        + SQLiteAxolotlStore.ACTIVE
2419                        + " > 0",
2420                args);
2421    }
2422
2423    private void storeIdentityKey(
2424            Account account,
2425            String name,
2426            boolean own,
2427            String fingerprint,
2428            String base64Serialized,
2429            FingerprintStatus status) {
2430        SQLiteDatabase db = this.getWritableDatabase();
2431        ContentValues values = new ContentValues();
2432        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2433        values.put(SQLiteAxolotlStore.NAME, name);
2434        values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
2435        values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
2436        values.put(SQLiteAxolotlStore.KEY, base64Serialized);
2437        values.putAll(status.toContentValues());
2438        String where =
2439                SQLiteAxolotlStore.ACCOUNT
2440                        + "=? AND "
2441                        + SQLiteAxolotlStore.NAME
2442                        + "=? AND "
2443                        + SQLiteAxolotlStore.FINGERPRINT
2444                        + " =?";
2445        String[] whereArgs = {account.getUuid(), name, fingerprint};
2446        int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, where, whereArgs);
2447        if (rows == 0) {
2448            db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
2449        }
2450    }
2451
2452    public void storePreVerification(
2453            Account account, String name, String fingerprint, FingerprintStatus status) {
2454        SQLiteDatabase db = this.getWritableDatabase();
2455        ContentValues values = new ContentValues();
2456        values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
2457        values.put(SQLiteAxolotlStore.NAME, name);
2458        values.put(SQLiteAxolotlStore.OWN, 0);
2459        values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
2460        values.putAll(status.toContentValues());
2461        db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
2462    }
2463
2464    public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) {
2465        Cursor cursor = getIdentityKeyCursor(account, fingerprint);
2466        final FingerprintStatus status;
2467        if (cursor.getCount() > 0) {
2468            cursor.moveToFirst();
2469            status = FingerprintStatus.fromCursor(cursor);
2470        } else {
2471            status = null;
2472        }
2473        cursor.close();
2474        return status;
2475    }
2476
2477    public boolean setIdentityKeyTrust(
2478            Account account, String fingerprint, FingerprintStatus fingerprintStatus) {
2479        SQLiteDatabase db = this.getWritableDatabase();
2480        return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus);
2481    }
2482
2483    private boolean setIdentityKeyTrust(
2484            SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) {
2485        String[] selectionArgs = {account.getUuid(), fingerprint};
2486        int rows =
2487                db.update(
2488                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2489                        status.toContentValues(),
2490                        SQLiteAxolotlStore.ACCOUNT
2491                                + " = ? AND "
2492                                + SQLiteAxolotlStore.FINGERPRINT
2493                                + " = ? ",
2494                        selectionArgs);
2495        return rows == 1;
2496    }
2497
2498    public boolean setIdentityKeyCertificate(
2499            Account account, String fingerprint, X509Certificate x509Certificate) {
2500        SQLiteDatabase db = this.getWritableDatabase();
2501        String[] selectionArgs = {account.getUuid(), fingerprint};
2502        try {
2503            ContentValues values = new ContentValues();
2504            values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded());
2505            return db.update(
2506                            SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2507                            values,
2508                            SQLiteAxolotlStore.ACCOUNT
2509                                    + " = ? AND "
2510                                    + SQLiteAxolotlStore.FINGERPRINT
2511                                    + " = ? ",
2512                            selectionArgs)
2513                    == 1;
2514        } catch (CertificateEncodingException e) {
2515            Log.d(Config.LOGTAG, "could not encode certificate");
2516            return false;
2517        }
2518    }
2519
2520    public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) {
2521        SQLiteDatabase db = this.getReadableDatabase();
2522        String[] selectionArgs = {account.getUuid(), fingerprint};
2523        String[] colums = {SQLiteAxolotlStore.CERTIFICATE};
2524        String selection =
2525                SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? ";
2526        Cursor cursor =
2527                db.query(
2528                        SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2529                        colums,
2530                        selection,
2531                        selectionArgs,
2532                        null,
2533                        null,
2534                        null);
2535        if (cursor.getCount() < 1) {
2536            return null;
2537        } else {
2538            cursor.moveToFirst();
2539            byte[] certificate =
2540                    cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE));
2541            cursor.close();
2542            if (certificate == null || certificate.length == 0) {
2543                return null;
2544            }
2545            try {
2546                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
2547                return (X509Certificate)
2548                        certificateFactory.generateCertificate(
2549                                new ByteArrayInputStream(certificate));
2550            } catch (CertificateException e) {
2551                Log.d(Config.LOGTAG, "certificate exception " + e.getMessage());
2552                return null;
2553            }
2554        }
2555    }
2556
2557    public void storeIdentityKey(
2558            Account account, String name, IdentityKey identityKey, FingerprintStatus status) {
2559        storeIdentityKey(
2560                account,
2561                name,
2562                false,
2563                CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()),
2564                Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT),
2565                status);
2566    }
2567
2568    public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
2569        storeIdentityKey(
2570                account,
2571                account.getJid().asBareJid().toString(),
2572                true,
2573                CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()),
2574                Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT),
2575                FingerprintStatus.createActiveVerified(false));
2576    }
2577
2578    private void recreateAxolotlDb(SQLiteDatabase db) {
2579        Log.d(
2580                Config.LOGTAG,
2581                AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<");
2582        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME);
2583        db.execSQL(CREATE_SESSIONS_STATEMENT);
2584        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME);
2585        db.execSQL(CREATE_PREKEYS_STATEMENT);
2586        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
2587        db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
2588        db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME);
2589        db.execSQL(CREATE_IDENTITIES_STATEMENT);
2590    }
2591
2592    public void wipeAxolotlDb(Account account) {
2593        String accountName = account.getUuid();
2594        Log.d(
2595                Config.LOGTAG,
2596                AxolotlService.getLogprefix(account)
2597                        + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT "
2598                        + accountName
2599                        + " <<<");
2600        SQLiteDatabase db = this.getWritableDatabase();
2601        String[] deleteArgs = {accountName};
2602        db.delete(
2603                SQLiteAxolotlStore.SESSION_TABLENAME,
2604                SQLiteAxolotlStore.ACCOUNT + " = ?",
2605                deleteArgs);
2606        db.delete(
2607                SQLiteAxolotlStore.PREKEY_TABLENAME,
2608                SQLiteAxolotlStore.ACCOUNT + " = ?",
2609                deleteArgs);
2610        db.delete(
2611                SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
2612                SQLiteAxolotlStore.ACCOUNT + " = ?",
2613                deleteArgs);
2614        db.delete(
2615                SQLiteAxolotlStore.IDENTITIES_TABLENAME,
2616                SQLiteAxolotlStore.ACCOUNT + " = ?",
2617                deleteArgs);
2618    }
2619
2620    public List<ShortcutService.FrequentContact> getFrequentContacts(final int days) {
2621        final var db = this.getReadableDatabase();
2622        final String SQL =
2623                "select "
2624                        + Conversation.TABLENAME
2625                        + "."
2626                        + Conversation.UUID
2627                        + ","
2628                        + Conversation.TABLENAME
2629                        + "."
2630                        + Conversation.ACCOUNT
2631                        + ","
2632                        + Conversation.TABLENAME
2633                        + "."
2634                        + Conversation.CONTACTJID
2635                        + " from "
2636                        + Conversation.TABLENAME
2637                        + " join "
2638                        + Message.TABLENAME
2639                        + " on conversations.uuid=messages.conversationUuid where"
2640                        + " messages.status!=0 and carbon==0  and conversations.mode=0 and"
2641                        + " messages.timeSent>=? group by conversations.uuid order by count(body)"
2642                        + " desc limit 4;";
2643        String[] whereArgs =
2644                new String[] {
2645                    String.valueOf(System.currentTimeMillis() - (Config.MILLISECONDS_IN_DAY * days))
2646                };
2647        Cursor cursor = db.rawQuery(SQL, whereArgs);
2648        ArrayList<ShortcutService.FrequentContact> contacts = new ArrayList<>();
2649        while (cursor.moveToNext()) {
2650            try {
2651                contacts.add(
2652                        new ShortcutService.FrequentContact(
2653                                cursor.getString(0),
2654                                cursor.getString(1),
2655                                Jid.of(cursor.getString(2))));
2656            } catch (final Exception e) {
2657                Log.e(Config.LOGTAG, "could not create frequent contact", e);
2658            }
2659        }
2660        cursor.close();
2661        return contacts;
2662    }
2663}