DatabaseBackend.java

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