diff options
Diffstat (limited to '')
3 files changed, 2170 insertions, 2170 deletions
diff --git a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java index 4ce66edf8..993a42c21 100644 --- a/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java +++ b/src/main/java/de/pixart/messenger/persistance/DatabaseBackend.java @@ -52,1261 +52,1261 @@ import de.pixart.messenger.xmpp.jid.Jid; public class DatabaseBackend extends SQLiteOpenHelper { - private static DatabaseBackend instance = null; - - public static final String DATABASE_NAME = "history"; - public static final int DATABASE_VERSION = 31; - - private static String CREATE_CONTATCS_STATEMENT = "create table " - + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " - + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT," - + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," - + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," - + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " - + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " - + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " - + Account.TABLENAME + "(" + Account.UUID - + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " - + Contact.JID + ") ON CONFLICT REPLACE);"; - - private static String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table " - + ServiceDiscoveryResult.TABLENAME + "(" - + ServiceDiscoveryResult.HASH + " TEXT, " - + ServiceDiscoveryResult.VER + " TEXT, " - + ServiceDiscoveryResult.RESULT + " TEXT, " - + "UNIQUE(" + ServiceDiscoveryResult.HASH + ", " - + ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);"; - - private static String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE " - + PresenceTemplate.TABELNAME + "(" - + PresenceTemplate.UUID + " TEXT, " - + PresenceTemplate.LAST_USED + " NUMBER," - + PresenceTemplate.MESSAGE + " TEXT," - + PresenceTemplate.STATUS + " TEXT," - + "UNIQUE("+PresenceTemplate.MESSAGE + "," +PresenceTemplate.STATUS+") ON CONFLICT REPLACE);"; - - private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.PREKEY_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.ID + " INTEGER, " - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.ID - + ") ON CONFLICT REPLACE" - + ");"; - - private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.ID + " INTEGER, " - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.ID - + ") ON CONFLICT REPLACE" + - ");"; - - private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.SESSION_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.NAME + " TEXT, " - + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.NAME + ", " - + SQLiteAxolotlStore.DEVICE_ID - + ") ON CONFLICT REPLACE" - + ");"; - - private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " - + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" - + SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + SQLiteAxolotlStore.NAME + " TEXT, " - + SQLiteAxolotlStore.OWN + " INTEGER, " - + SQLiteAxolotlStore.FINGERPRINT + " TEXT, " - + SQLiteAxolotlStore.CERTIFICATE + " BLOB, " - + SQLiteAxolotlStore.TRUST + " TEXT, " - + SQLiteAxolotlStore.ACTIVE + " NUMBER, " - + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " - + SQLiteAxolotlStore.NAME + ", " - + SQLiteAxolotlStore.FINGERPRINT - + ") ON CONFLICT IGNORE" - + ");"; - - private static String START_TIMES_TABLE = "start_times"; - private static String CREATE_START_TIMES_TABLE = "create table "+START_TIMES_TABLE+" (timestamp NUMBER);"; - - private DatabaseBackend(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("PRAGMA foreign_keys=ON;"); - db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID+ " TEXT PRIMARY KEY," - + Account.USERNAME + " TEXT," - + Account.SERVER + " TEXT," - + Account.PASSWORD + " TEXT," - + Account.DISPLAY_NAME + " TEXT, " - + Account.STATUS + " TEXT," - + Account.STATUS_MESSAGE + " TEXT," - + Account.ROSTERVERSION + " TEXT," - + Account.OPTIONS + " NUMBER, " - + Account.AVATAR + " TEXT, " - + Account.KEYS + " TEXT, " - + Account.HOSTNAME + " TEXT, " - + Account.PORT + " NUMBER DEFAULT 5222)"); - db.execSQL("create table " + Conversation.TABLENAME + " (" - + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME - + " TEXT, " + Conversation.CONTACT + " TEXT, " - + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID - + " TEXT, " + Conversation.CREATED + " NUMBER, " - + Conversation.STATUS + " NUMBER, " + Conversation.MODE - + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY(" - + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME - + "(" + Account.UUID + ") ON DELETE CASCADE);"); - db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID - + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " - + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART - + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," - + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " - + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " - + Message.RELATIVE_FILE_PATH + " TEXT, " - + Message.SERVER_MSG_ID + " TEXT, " - + Message.FINGERPRINT + " TEXT, " - + Message.CARBON + " INTEGER, " - + Message.EDITED + " TEXT, " - + Message.READ + " NUMBER DEFAULT 1, " - + Message.OOB + " INTEGER, " - + Message.ERROR_MESSAGE + " TEXT," - + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" - + Message.CONVERSATION + ") REFERENCES " - + Conversation.TABLENAME + "(" + Conversation.UUID - + ") ON DELETE CASCADE);"); - - db.execSQL(CREATE_CONTATCS_STATEMENT); - db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); - db.execSQL(CREATE_SESSIONS_STATEMENT); - db.execSQL(CREATE_PREKEYS_STATEMENT); - db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); - db.execSQL(CREATE_IDENTITIES_STATEMENT); - db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); - db.execSQL(CREATE_START_TIMES_TABLE); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion < 2 && newVersion >= 2) { - db.execSQL("update " + Account.TABLENAME + " set " - + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); - } - if (oldVersion < 3 && newVersion >= 3) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.TYPE + " NUMBER"); - } - if (oldVersion < 5 && newVersion >= 5) { - db.execSQL("DROP TABLE " + Contact.TABLENAME); - db.execSQL(CREATE_CONTATCS_STATEMENT); - db.execSQL("UPDATE " + Account.TABLENAME + " SET " - + Account.ROSTERVERSION + " = NULL"); - } - if (oldVersion < 6 && newVersion >= 6) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.TRUE_COUNTERPART + " TEXT"); - } - if (oldVersion < 7 && newVersion >= 7) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.REMOTE_MSG_ID + " TEXT"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.AVATAR + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " - + Account.AVATAR + " TEXT"); - } - if (oldVersion < 8 && newVersion >= 8) { - db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " - + Conversation.ATTRIBUTES + " TEXT"); - } - if (oldVersion < 9 && newVersion >= 9) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_TIME + " NUMBER"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_PRESENCE + " TEXT"); - } - if (oldVersion < 10 && newVersion >= 10) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.RELATIVE_FILE_PATH + " TEXT"); - } - if (oldVersion < 11 && newVersion >= 11) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.GROUPS + " TEXT"); - db.execSQL("delete from " + Contact.TABLENAME); - db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL"); - } - if (oldVersion < 12 && newVersion >= 12) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.SERVER_MSG_ID + " TEXT"); - } - if (oldVersion < 13 && newVersion >= 13) { - db.execSQL("delete from " + Contact.TABLENAME); - db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL"); - } - if (oldVersion < 14 && newVersion >= 14) { - canonicalizeJids(db); - } - if (oldVersion < 15 && newVersion >= 15) { - recreateAxolotlDb(db); - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.FINGERPRINT + " TEXT"); - } else if (oldVersion < 22 && newVersion >= 22) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); - } - if (oldVersion < 16 && newVersion >= 16) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.CARBON + " INTEGER"); - } - if (oldVersion < 19 && newVersion >= 19) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.DISPLAY_NAME + " TEXT"); - } - if (oldVersion < 20 && newVersion >= 20) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222"); - } - if (oldVersion < 26 && newVersion >= 26) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT"); - } - /* Any migrations that alter the Account table need to happen BEFORE this migration, as it + private static DatabaseBackend instance = null; + + public static final String DATABASE_NAME = "history"; + public static final int DATABASE_VERSION = 31; + + private static String CREATE_CONTATCS_STATEMENT = "create table " + + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT," + + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " + + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + + Account.TABLENAME + "(" + Account.UUID + + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " + + Contact.JID + ") ON CONFLICT REPLACE);"; + + private static String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table " + + ServiceDiscoveryResult.TABLENAME + "(" + + ServiceDiscoveryResult.HASH + " TEXT, " + + ServiceDiscoveryResult.VER + " TEXT, " + + ServiceDiscoveryResult.RESULT + " TEXT, " + + "UNIQUE(" + ServiceDiscoveryResult.HASH + ", " + + ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);"; + + private static String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE " + + PresenceTemplate.TABELNAME + "(" + + PresenceTemplate.UUID + " TEXT, " + + PresenceTemplate.LAST_USED + " NUMBER," + + PresenceTemplate.MESSAGE + " TEXT," + + PresenceTemplate.STATUS + " TEXT," + + "UNIQUE(" + PresenceTemplate.MESSAGE + "," + PresenceTemplate.STATUS + ") ON CONFLICT REPLACE);"; + + private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.PREKEY_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.ID + + ") ON CONFLICT REPLACE" + + ");"; + + private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.ID + + ") ON CONFLICT REPLACE" + + ");"; + + private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.SESSION_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.NAME + " TEXT, " + + SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.NAME + ", " + + SQLiteAxolotlStore.DEVICE_ID + + ") ON CONFLICT REPLACE" + + ");"; + + private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " + + SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" + + SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + SQLiteAxolotlStore.NAME + " TEXT, " + + SQLiteAxolotlStore.OWN + " INTEGER, " + + SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + + SQLiteAxolotlStore.CERTIFICATE + " BLOB, " + + SQLiteAxolotlStore.TRUST + " TEXT, " + + SQLiteAxolotlStore.ACTIVE + " NUMBER, " + + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", " + + SQLiteAxolotlStore.NAME + ", " + + SQLiteAxolotlStore.FINGERPRINT + + ") ON CONFLICT IGNORE" + + ");"; + + private static String START_TIMES_TABLE = "start_times"; + private static String CREATE_START_TIMES_TABLE = "create table " + START_TIMES_TABLE + " (timestamp NUMBER);"; + + private DatabaseBackend(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("PRAGMA foreign_keys=ON;"); + db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID + " TEXT PRIMARY KEY," + + Account.USERNAME + " TEXT," + + Account.SERVER + " TEXT," + + Account.PASSWORD + " TEXT," + + Account.DISPLAY_NAME + " TEXT, " + + Account.STATUS + " TEXT," + + Account.STATUS_MESSAGE + " TEXT," + + Account.ROSTERVERSION + " TEXT," + + Account.OPTIONS + " NUMBER, " + + Account.AVATAR + " TEXT, " + + Account.KEYS + " TEXT, " + + Account.HOSTNAME + " TEXT, " + + Account.PORT + " NUMBER DEFAULT 5222)"); + db.execSQL("create table " + Conversation.TABLENAME + " (" + + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME + + " TEXT, " + Conversation.CONTACT + " TEXT, " + + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID + + " TEXT, " + Conversation.CREATED + " NUMBER, " + + Conversation.STATUS + " NUMBER, " + Conversation.MODE + + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY(" + + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME + + "(" + Account.UUID + ") ON DELETE CASCADE);"); + db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID + + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," + + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " + + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + + Message.RELATIVE_FILE_PATH + " TEXT, " + + Message.SERVER_MSG_ID + " TEXT, " + + Message.FINGERPRINT + " TEXT, " + + Message.CARBON + " INTEGER, " + + Message.EDITED + " TEXT, " + + Message.READ + " NUMBER DEFAULT 1, " + + Message.OOB + " INTEGER, " + + Message.ERROR_MESSAGE + " TEXT," + + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + + Message.CONVERSATION + ") REFERENCES " + + Conversation.TABLENAME + "(" + Conversation.UUID + + ") ON DELETE CASCADE);"); + + db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL(CREATE_IDENTITIES_STATEMENT); + db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); + db.execSQL(CREATE_START_TIMES_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2 && newVersion >= 2) { + db.execSQL("update " + Account.TABLENAME + " set " + + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); + } + if (oldVersion < 3 && newVersion >= 3) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TYPE + " NUMBER"); + } + if (oldVersion < 5 && newVersion >= 5) { + db.execSQL("DROP TABLE " + Contact.TABLENAME); + db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL("UPDATE " + Account.TABLENAME + " SET " + + Account.ROSTERVERSION + " = NULL"); + } + if (oldVersion < 6 && newVersion >= 6) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TRUE_COUNTERPART + " TEXT"); + } + if (oldVersion < 7 && newVersion >= 7) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.REMOTE_MSG_ID + " TEXT"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.AVATAR + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + + Account.AVATAR + " TEXT"); + } + if (oldVersion < 8 && newVersion >= 8) { + db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " + + Conversation.ATTRIBUTES + " TEXT"); + } + if (oldVersion < 9 && newVersion >= 9) { + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_TIME + " NUMBER"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_PRESENCE + " TEXT"); + } + if (oldVersion < 10 && newVersion >= 10) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.RELATIVE_FILE_PATH + " TEXT"); + } + if (oldVersion < 11 && newVersion >= 11) { + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.GROUPS + " TEXT"); + db.execSQL("delete from " + Contact.TABLENAME); + db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL"); + } + if (oldVersion < 12 && newVersion >= 12) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.SERVER_MSG_ID + " TEXT"); + } + if (oldVersion < 13 && newVersion >= 13) { + db.execSQL("delete from " + Contact.TABLENAME); + db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL"); + } + if (oldVersion < 14 && newVersion >= 14) { + canonicalizeJids(db); + } + if (oldVersion < 15 && newVersion >= 15) { + recreateAxolotlDb(db); + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.FINGERPRINT + " TEXT"); + } else if (oldVersion < 22 && newVersion >= 22) { + db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); + } + if (oldVersion < 16 && newVersion >= 16) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.CARBON + " INTEGER"); + } + if (oldVersion < 19 && newVersion >= 19) { + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.DISPLAY_NAME + " TEXT"); + } + if (oldVersion < 20 && newVersion >= 20) { + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222"); + } + if (oldVersion < 26 && newVersion >= 26) { + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT"); + } + /* Any migrations that alter the Account table need to happen BEFORE this migration, as it * depends on account de-serialization. */ - if (oldVersion < 17 && newVersion >= 17) { - List<Account> accounts = getAccounts(db); - for (Account account : accounts) { - String ownDeviceIdString = account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID); - if (ownDeviceIdString == null) { - continue; - } - int ownDeviceId = Integer.valueOf(ownDeviceIdString); - AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownDeviceId); - deleteSession(db, account, ownAddress); - IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account); - if (identityKeyPair != null) { - String[] selectionArgs = { - account.getUuid(), - identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", "") - }; - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.TRUSTED, 2); - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs); - } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair"); - } - } - } - if (oldVersion < 18 && newVersion >= 18) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ + " NUMBER DEFAULT 1"); - } - - if (oldVersion < 21 && newVersion >= 21) { - List<Account> accounts = getAccounts(db); - for (Account account : accounts) { - account.unsetPgpSignature(); - db.update(Account.TABLENAME, account.getContentValues(), Account.UUID - + "=?", new String[]{account.getUuid()}); - } - } - - if (oldVersion < 23 && newVersion >= 23) { - db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); - } - - if (oldVersion < 24 && newVersion >= 24) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); - } - - if (oldVersion < 25 && newVersion >= 25) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER"); - } - - if (oldVersion < 26 && newVersion >= 26) { - db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); - } - - if (oldVersion < 27 && newVersion >= 27) { - db.execSQL("DELETE FROM "+ServiceDiscoveryResult.TABLENAME); - } - - if (oldVersion < 28 && newVersion >= 28) { - canonicalizeJids(db); - } - - if (oldVersion < 29 && newVersion >= 29) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT"); - } - - if (oldVersion < 30 && newVersion >= 30) { - db.execSQL(CREATE_START_TIMES_TABLE); - } - if (oldVersion < 31 && newVersion >= 31) { - db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT"); - db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER"); - HashMap<Integer,ContentValues> migration = new HashMap<>(); - migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNDECIDED,true)); - migration.put(1,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); - migration.put(2,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true)); - migration.put(3,createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false)); - migration.put(4,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); - migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNDECIDED, false)); - migration.put(6,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false)); - migration.put(7,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true)); - migration.put(8,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false)); - for(Map.Entry<Integer,ContentValues> entry : migration.entrySet()) { - String whereClause = SQLiteAxolotlStore.TRUSTED+"=?"; - String[] where = {String.valueOf(entry.getKey())}; - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,entry.getValue(),whereClause,where); - } - } - } - - private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) { - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.TRUST,trust.toString()); - values.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0); - return values; - } - - private void canonicalizeJids(SQLiteDatabase db) { - // migrate db to new, canonicalized JID domainpart representation - - // Conversation table - Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]); - while (cursor.moveToNext()) { - String newJid; - try { - newJid = Jid.fromString( - cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) - ).toPreppedString(); - } catch (InvalidJidException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " - + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) - + ": " + ignored + ". Skipping..."); - continue; - } - - String updateArgs[] = { - newJid, - cursor.getString(cursor.getColumnIndex(Conversation.UUID)), - }; - db.execSQL("update " + Conversation.TABLENAME - + " set " + Conversation.CONTACTJID + " = ? " - + " where " + Conversation.UUID + " = ?", updateArgs); - } - cursor.close(); - - // Contact table - cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]); - while (cursor.moveToNext()) { - String newJid; - try { - newJid = Jid.fromString( - cursor.getString(cursor.getColumnIndex(Contact.JID)) - ).toPreppedString(); - } catch (InvalidJidException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Contact JID " - + cursor.getString(cursor.getColumnIndex(Contact.JID)) - + ": " + ignored + ". Skipping..."); - continue; - } - - String updateArgs[] = { - newJid, - cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)), - cursor.getString(cursor.getColumnIndex(Contact.JID)), - }; - db.execSQL("update " + Contact.TABLENAME - + " set " + Contact.JID + " = ? " - + " where " + Contact.ACCOUNT + " = ? " - + " AND " + Contact.JID + " = ?", updateArgs); - } - cursor.close(); - - // Account table - cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]); - while (cursor.moveToNext()) { - String newServer; - try { - newServer = Jid.fromParts( - cursor.getString(cursor.getColumnIndex(Account.USERNAME)), - cursor.getString(cursor.getColumnIndex(Account.SERVER)), - "mobile" - ).getDomainpart(); - } catch (InvalidJidException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Account SERVER " - + cursor.getString(cursor.getColumnIndex(Account.SERVER)) - + ": " + ignored + ". Skipping..."); - continue; - } - - String updateArgs[] = { - newServer, - cursor.getString(cursor.getColumnIndex(Account.UUID)), - }; - db.execSQL("update " + Account.TABLENAME - + " set " + Account.SERVER + " = ? " - + " where " + Account.UUID + " = ?", updateArgs); - } - cursor.close(); - } - - public static synchronized DatabaseBackend getInstance(Context context) { - if (instance == null) { - instance = new DatabaseBackend(context); - } - return instance; - } - - public void createConversation(Conversation conversation) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(Conversation.TABLENAME, null, conversation.getContentValues()); - } - - public void createMessage(Message message) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(Message.TABLENAME, null, message.getContentValues()); - } - - public void createAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(Account.TABLENAME, null, account.getContentValues()); - } - - public void insertDiscoveryResult(ServiceDiscoveryResult result) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues()); - } - - public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {hash, ver}; - Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null, - ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?", - selectionArgs, null, null, null); - if (cursor.getCount() == 0) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - - ServiceDiscoveryResult result = null; - try { - result = new ServiceDiscoveryResult(cursor); - } catch (JSONException e) { /* result is still null */ } - - cursor.close(); - return result; - } - - public void insertPresenceTemplate(PresenceTemplate template) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues()); - } - - public List<PresenceTemplate> getPresenceTemplates() { - ArrayList<PresenceTemplate> templates = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.query(PresenceTemplate.TABELNAME,null,null,null,null,null,PresenceTemplate.LAST_USED+" desc"); - while (cursor.moveToNext()) { - templates.add(PresenceTemplate.fromCursor(cursor)); - } - cursor.close(); - return templates; - } - - public void deletePresenceTemplate(PresenceTemplate template) { - Log.d(Config.LOGTAG,"deleting presence template with uuid "+template.getUuid()); - SQLiteDatabase db = this.getWritableDatabase(); - String where = PresenceTemplate.UUID+"=?"; - String[] whereArgs = {template.getUuid()}; - db.delete(PresenceTemplate.TABELNAME,where,whereArgs); - } - - public CopyOnWriteArrayList<Conversation> getConversations(int status) { - CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {Integer.toString(status)}; - Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME - + " where " + Conversation.STATUS + " = ? order by " - + Conversation.CREATED + " desc", selectionArgs); - while (cursor.moveToNext()) { - list.add(Conversation.fromCursor(cursor)); - } - cursor.close(); - return list; - } - - public ArrayList<Message> getMessages(Conversation conversations, int limit) { - return getMessages(conversations, limit, -1); - } - - public ArrayList<Message> getMessages(Conversation conversation, int limit, - long timestamp) { - ArrayList<Message> list = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor; - if (timestamp == -1) { - String[] selectionArgs = {conversation.getUuid()}; - cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=?", selectionArgs, null, null, Message.TIME_SENT - + " DESC", String.valueOf(limit)); - } else { - String[] selectionArgs = {conversation.getUuid(), - Long.toString(timestamp)}; - cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=? and " + Message.TIME_SENT + "<?", selectionArgs, - null, null, Message.TIME_SENT + " DESC", - String.valueOf(limit)); - } - if (cursor.getCount() > 0) { - cursor.moveToLast(); - do { - Message message = Message.fromCursor(cursor); - message.setConversation(conversation); - list.add(message); - } while (cursor.moveToPrevious()); - } - cursor.close(); - return list; - } - - public Iterable<Message> getMessagesIterable(final Conversation conversation) { - return new Iterable<Message>() { - @Override - public Iterator<Message> iterator() { - class MessageIterator implements Iterator<Message> { - SQLiteDatabase db = getReadableDatabase(); - String[] selectionArgs = {conversation.getUuid()}; - Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION - + "=?", selectionArgs, null, null, Message.TIME_SENT - + " ASC", null); - - public MessageIterator() { - cursor.moveToFirst(); - } - - @Override - public boolean hasNext() { - return !cursor.isAfterLast(); - } - - @Override - public Message next() { - Message message = Message.fromCursor(cursor); - cursor.moveToNext(); - return message; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - } - return new MessageIterator(); - } - }; - } - - public Conversation findConversation(final Account account, final Jid contactJid) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {account.getUuid(), - contactJid.toBareJid().toPreppedString() + "/%", - contactJid.toBareJid().toPreppedString() - }; - Cursor cursor = db.query(Conversation.TABLENAME, null, - Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID - + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null); - if (cursor.getCount() == 0) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - Conversation conversation = Conversation.fromCursor(cursor); - cursor.close(); - return conversation; - } - - public void updateConversation(final Conversation conversation) { - final SQLiteDatabase db = this.getWritableDatabase(); - final String[] args = {conversation.getUuid()}; - db.update(Conversation.TABLENAME, conversation.getContentValues(), - Conversation.UUID + "=?", args); - } - - public List<Account> getAccounts() { - SQLiteDatabase db = this.getReadableDatabase(); - return getAccounts(db); - } - - private List<Account> getAccounts(SQLiteDatabase db) { - List<Account> list = new ArrayList<>(); - Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, - null, null); - while (cursor.moveToNext()) { - list.add(Account.fromCursor(cursor)); - } - cursor.close(); - return list; - } - - public boolean updateAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid()}; - final int rows = db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args); - return rows == 1; - } - - public boolean deleteAccount(Account account) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid()}; - final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args); - return rows == 1; - } - - public boolean hasEnabledAccounts() { - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from " - + Account.TABLENAME + " where not options & (1 <<1)", null); - try { - cursor.moveToFirst(); - int count = cursor.getInt(0); - return (count > 0); - } catch (SQLiteCantOpenDatabaseException e) { - return true; // better safe than sorry - } catch (RuntimeException e) { - return true; // better safe than sorry - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - @Override - public SQLiteDatabase getWritableDatabase() { - SQLiteDatabase db = super.getWritableDatabase(); - db.execSQL("PRAGMA foreign_keys=ON;"); - return db; - } - - public void updateMessage(Message message) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {message.getUuid()}; - db.update(Message.TABLENAME, message.getContentValues(), Message.UUID - + "=?", args); - } - - public void updateMessage(Message message, String uuid) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {uuid}; - db.update(Message.TABLENAME, message.getContentValues(), Message.UUID - + "=?", args); - } - - public void readRoster(Roster roster) { - SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor; - String args[] = {roster.getAccount().getUuid()}; - cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null); - while (cursor.moveToNext()) { - roster.initContact(Contact.fromCursor(cursor)); - } - cursor.close(); - } - - public void writeRoster(final Roster roster) { - final Account account = roster.getAccount(); - final SQLiteDatabase db = this.getWritableDatabase(); - db.beginTransaction(); - for (Contact contact : roster.getContacts()) { - if (contact.getOption(Contact.Options.IN_ROSTER)) { - db.insert(Contact.TABLENAME, null, contact.getContentValues()); - } else { - String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; - String[] whereArgs = {account.getUuid(), contact.getJid().toPreppedString()}; - db.delete(Contact.TABLENAME, where, whereArgs); - } - } - db.setTransactionSuccessful(); - db.endTransaction(); - account.setRosterVersion(roster.getVersion()); - updateAccount(account); - } - - public void deleteMessagesInConversation(Conversation conversation) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {conversation.getUuid()}; - db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); - } - - public Pair<Long, String> getLastMessageReceived(Account account) { - Cursor cursor = null; - try { - SQLiteDatabase db = this.getReadableDatabase(); - String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1"; - String[] args = {account.getUuid()}; - cursor = db.rawQuery(sql, args); - if (cursor.getCount() == 0) { - return null; - } else { - cursor.moveToFirst(); - return new Pair<>(cursor.getLong(0), cursor.getString(1)); - } - } catch (Exception e) { - return null; - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - - public Pair<Long, String> getLastClearDate(Account account) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {Conversation.ATTRIBUTES}; - String selection = Conversation.ACCOUNT + "=?"; - String[] args = {account.getUuid()}; - Cursor cursor = db.query(Conversation.TABLENAME, columns, selection, args, null, null, null); - long maxClearDate = 0; - while (cursor.moveToNext()) { - try { - final JSONObject jsonObject = new JSONObject(cursor.getString(0)); - maxClearDate = Math.max(maxClearDate, jsonObject.getLong(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY)); - } catch (Exception e) { - //ignored - } - } - cursor.close(); - return new Pair<>(maxClearDate, null); - } - - private Cursor getCursorForSession(Account account, AxolotlAddress contact) { - final SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {account.getUuid(), - contact.getName(), - Integer.toString(contact.getDeviceId())}; - return db.query(SQLiteAxolotlStore.SESSION_TABLENAME, - null, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ? AND " - + SQLiteAxolotlStore.DEVICE_ID + " = ? ", - selectionArgs, - null, null, null); - } - - public SessionRecord loadSession(Account account, AxolotlAddress contact) { - SessionRecord session = null; - Cursor cursor = getCursorForSession(account, contact); - if (cursor.getCount() != 0) { - cursor.moveToFirst(); - try { - session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); - } catch (IOException e) { - cursor.close(); - throw new AssertionError(e); - } - } - cursor.close(); - return session; - } - - public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) { - final SQLiteDatabase db = this.getReadableDatabase(); - return getSubDeviceSessions(db, account, contact); - } - - private List<Integer> getSubDeviceSessions(SQLiteDatabase db, Account account, AxolotlAddress contact) { - List<Integer> devices = new ArrayList<>(); - String[] columns = {SQLiteAxolotlStore.DEVICE_ID}; - String[] selectionArgs = {account.getUuid(), - contact.getName()}; - Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ?", - selectionArgs, - null, null, null); - - while (cursor.moveToNext()) { - devices.add(cursor.getInt( - cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID))); - } - - cursor.close(); - return devices; - } - - public boolean containsSession(Account account, AxolotlAddress contact) { - Cursor cursor = getCursorForSession(account, contact); - int count = cursor.getCount(); - cursor.close(); - return count != 0; - } - - public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.NAME, contact.getName()); - values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT)); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values); - } - - public void deleteSession(Account account, AxolotlAddress contact) { - SQLiteDatabase db = this.getWritableDatabase(); - deleteSession(db, account, contact); - } - - private void deleteSession(SQLiteDatabase db, Account account, AxolotlAddress contact) { - String[] args = {account.getUuid(), - contact.getName(), - Integer.toString(contact.getDeviceId())}; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ? AND " - + SQLiteAxolotlStore.DEVICE_ID + " = ? ", - args); - } - - public void deleteAllSessions(Account account, AxolotlAddress contact) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid(), contact.getName()}; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.NAME + " = ?", - args); - } - - private Cursor getCursorForPreKey(Account account, int preKeyId) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; - Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", - selectionArgs, - null, null, null); - - return cursor; - } - - public PreKeyRecord loadPreKey(Account account, int preKeyId) { - PreKeyRecord record = null; - Cursor cursor = getCursorForPreKey(account, preKeyId); - if (cursor.getCount() != 0) { - cursor.moveToFirst(); - try { - record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); - } catch (IOException e) { - throw new AssertionError(e); - } - } - cursor.close(); - return record; - } - - public boolean containsPreKey(Account account, int preKeyId) { - Cursor cursor = getCursorForPreKey(account, preKeyId); - int count = cursor.getCount(); - cursor.close(); - return count != 0; - } - - public void storePreKey(Account account, PreKeyRecord record) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ID, record.getId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); - } - - public void deletePreKey(Account account, int preKeyId) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid(), Integer.toString(preKeyId)}; - db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", - args); - } - - private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", - selectionArgs, - null, null, null); - - return cursor; - } - - public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { - SignedPreKeyRecord record = null; - Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId); - if (cursor.getCount() != 0) { - cursor.moveToFirst(); - try { - record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); - } catch (IOException e) { - throw new AssertionError(e); - } - } - cursor.close(); - return record; - } - - public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) { - List<SignedPreKeyRecord> prekeys = new ArrayList<>(); - SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid()}; - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=?", - selectionArgs, - null, null, null); - - while (cursor.moveToNext()) { - try { - prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); - } catch (IOException ignored) { - } - } - cursor.close(); - return prekeys; - } - - public boolean containsSignedPreKey(Account account, int signedPreKeyId) { - Cursor cursor = getCursorForPreKey(account, signedPreKeyId); - int count = cursor.getCount(); - cursor.close(); - return count != 0; - } - - public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ID, record.getId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); - } - - public void deleteSignedPreKey(Account account, int signedPreKeyId) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; - db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", - args); - } - - private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { - final SQLiteDatabase db = this.getReadableDatabase(); - return getIdentityKeyCursor(db, account, name, own); - } - - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, boolean own) { - return getIdentityKeyCursor(db, account, name, own, null); - } - - private Cursor getIdentityKeyCursor(Account account, String fingerprint) { - final SQLiteDatabase db = this.getReadableDatabase(); - return getIdentityKeyCursor(db, account, fingerprint); - } - - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String fingerprint) { - return getIdentityKeyCursor(db, account, null, null, fingerprint); - } - - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) { - String[] columns = {SQLiteAxolotlStore.TRUST, - SQLiteAxolotlStore.ACTIVE, - SQLiteAxolotlStore.KEY}; - ArrayList<String> selectionArgs = new ArrayList<>(4); - selectionArgs.add(account.getUuid()); - String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?"; - if (name != null) { - selectionArgs.add(name); - selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?"; - } - if (fingerprint != null) { - selectionArgs.add(fingerprint); - selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?"; - } - if (own != null) { - selectionArgs.add(own ? "1" : "0"); - selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?"; - } - Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, - columns, - selectionString, - selectionArgs.toArray(new String[selectionArgs.size()]), - null, null, null); - - return cursor; - } - - public IdentityKeyPair loadOwnIdentityKeyPair(Account account) { - SQLiteDatabase db = getReadableDatabase(); - return loadOwnIdentityKeyPair(db, account); - } - - private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) { - String name = account.getJid().toBareJid().toPreppedString(); - IdentityKeyPair identityKeyPair = null; - Cursor cursor = getIdentityKeyCursor(db, account, name, true); - if (cursor.getCount() != 0) { - cursor.moveToFirst(); - try { - identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); - } - } - cursor.close(); - - return identityKeyPair; - } - - public Set<IdentityKey> loadIdentityKeys(Account account, String name) { - return loadIdentityKeys(account, name, null); - } - - public Set<IdentityKey> loadIdentityKeys(Account account, String name, FingerprintStatus status) { - Set<IdentityKey> identityKeys = new HashSet<>(); - Cursor cursor = getIdentityKeyCursor(account, name, false); - - while (cursor.moveToNext()) { - if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) { - continue; - } - try { - String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)); - if (key != null) { - identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0)); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name); - } - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); - } - } - cursor.close(); - - return identityKeys; - } - - public long numTrustedKeys(Account account, String name) { - SQLiteDatabase db = getReadableDatabase(); - String[] args = { - account.getUuid(), - name, - FingerprintStatus.Trust.TRUSTED.toString(), - FingerprintStatus.Trust.VERIFIED.toString(), - FingerprintStatus.Trust.VERIFIED_X509.toString() - }; - return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?" - + " AND " + SQLiteAxolotlStore.NAME + " = ?" - + " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " +SQLiteAxolotlStore.TRUST +" = ?)" - + " AND " +SQLiteAxolotlStore.ACTIVE + " > 0", - args - ); - } - - private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - values.put(SQLiteAxolotlStore.NAME, name); - values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0); - values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); - values.put(SQLiteAxolotlStore.KEY, base64Serialized); - values.putAll(status.toContentValues()); - String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?"; - String[] whereArgs = {account.getUuid(),name,fingerprint}; - int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs); - if (rows == 0) { - db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); - } - } - - public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - values.put(SQLiteAxolotlStore.NAME, name); - values.put(SQLiteAxolotlStore.OWN, 0); - values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); - values.putAll(status.toContentValues()); - db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); - } - - public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) { - Cursor cursor = getIdentityKeyCursor(account, fingerprint); - final FingerprintStatus status; - if (cursor.getCount() > 0) { - cursor.moveToFirst(); - status = FingerprintStatus.fromCursor(cursor); - } else { - status = null; - } - cursor.close(); - return status; - } - - public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) { - SQLiteDatabase db = this.getWritableDatabase(); - return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus); - } - - private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) { - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; - int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(), - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs); - return rows == 1; - } - - public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; - try { - ContentValues values = new ContentValues(); - values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded()); - return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs) == 1; - } catch (CertificateEncodingException e) { - Log.d(Config.LOGTAG, "could not encode certificate"); - return false; - } - } - - public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; - String[] colums = {SQLiteAxolotlStore.CERTIFICATE}; - String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? "; - Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null); - if (cursor.getCount() < 1) { - return null; - } else { - cursor.moveToFirst(); - byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE)); - cursor.close(); - if (certificate == null || certificate.length == 0) { - return null; - } - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); - } catch (CertificateException e) { - Log.d(Config.LOGTAG,"certificate exception "+e.getMessage()); - return null; - } - } - } - - public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) { - storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status); - } - - public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false)); - } - - public void recreateAxolotlDb(SQLiteDatabase db) { - Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<"); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME); - db.execSQL(CREATE_SESSIONS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME); - db.execSQL(CREATE_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); - db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME); - db.execSQL(CREATE_IDENTITIES_STATEMENT); - } - - public void wipeAxolotlDb(Account account) { - String accountName = account.getUuid(); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); - SQLiteDatabase db = this.getWritableDatabase(); - String[] deleteArgs = { - accountName - }; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?", - deleteArgs); - } - - public boolean startTimeCountExceedsThreshold() { - SQLiteDatabase db = this.getWritableDatabase(); - long cleanBeforeTimestamp = System.currentTimeMillis() - Config.FREQUENT_RESTARTS_DETECTION_WINDOW; - db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp < "+cleanBeforeTimestamp); - ContentValues values = new ContentValues(); - values.put("timestamp",System.currentTimeMillis()); - db.insert(START_TIMES_TABLE,null,values); - String[] columns = new String[]{"count(timestamp)"}; - Cursor cursor = db.query(START_TIMES_TABLE,columns,null,null,null,null,null); - int count; - if (cursor.moveToFirst()) { - count = cursor.getInt(0); - } else { - count = 0; - } - cursor.close(); - Log.d(Config.LOGTAG,"start time counter reached "+count); - return count >= Config.FREQUENT_RESTARTS_THRESHOLD; - } - - public void clearStartTimeCounter() { - Log.d(Config.LOGTAG,"resetting start time counter"); - SQLiteDatabase db = this.getWritableDatabase(); - db.execSQL("delete from " + START_TIMES_TABLE); - } + if (oldVersion < 17 && newVersion >= 17) { + List<Account> accounts = getAccounts(db); + for (Account account : accounts) { + String ownDeviceIdString = account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID); + if (ownDeviceIdString == null) { + continue; + } + int ownDeviceId = Integer.valueOf(ownDeviceIdString); + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownDeviceId); + deleteSession(db, account, ownAddress); + IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account); + if (identityKeyPair != null) { + String[] selectionArgs = { + account.getUuid(), + identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", "") + }; + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.TRUSTED, 2); + db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair"); + } + } + } + if (oldVersion < 18 && newVersion >= 18) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ + " NUMBER DEFAULT 1"); + } + + if (oldVersion < 21 && newVersion >= 21) { + List<Account> accounts = getAccounts(db); + for (Account account : accounts) { + account.unsetPgpSignature(); + db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + + "=?", new String[]{account.getUuid()}); + } + } + + if (oldVersion < 23 && newVersion >= 23) { + db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); + } + + if (oldVersion < 24 && newVersion >= 24) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); + } + + if (oldVersion < 25 && newVersion >= 25) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER"); + } + + if (oldVersion < 26 && newVersion >= 26) { + db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); + } + + if (oldVersion < 27 && newVersion >= 27) { + db.execSQL("DELETE FROM " + ServiceDiscoveryResult.TABLENAME); + } + + if (oldVersion < 28 && newVersion >= 28) { + canonicalizeJids(db); + } + + if (oldVersion < 29 && newVersion >= 29) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT"); + } + + if (oldVersion < 30 && newVersion >= 30) { + db.execSQL(CREATE_START_TIMES_TABLE); + } + if (oldVersion < 31 && newVersion >= 31) { + db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.TRUST + " TEXT"); + db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.ACTIVE + " NUMBER"); + HashMap<Integer, ContentValues> migration = new HashMap<>(); + migration.put(0, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNDECIDED, true)); + migration.put(1, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); + migration.put(2, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true)); + migration.put(3, createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false)); + migration.put(4, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); + migration.put(5, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNDECIDED, false)); + migration.put(6, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false)); + migration.put(7, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true)); + migration.put(8, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false)); + for (Map.Entry<Integer, ContentValues> entry : migration.entrySet()) { + String whereClause = SQLiteAxolotlStore.TRUSTED + "=?"; + String[] where = {String.valueOf(entry.getKey())}; + db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, entry.getValue(), whereClause, where); + } + } + } + + private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) { + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.TRUST, trust.toString()); + values.put(SQLiteAxolotlStore.ACTIVE, active ? 1 : 0); + return values; + } + + private void canonicalizeJids(SQLiteDatabase db) { + // migrate db to new, canonicalized JID domainpart representation + + // Conversation table + Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]); + while (cursor.moveToNext()) { + String newJid; + try { + newJid = Jid.fromString( + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) + ).toPreppedString(); + } catch (InvalidJidException ignored) { + Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " + + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) + + ": " + ignored + ". Skipping..."); + continue; + } + + String updateArgs[] = { + newJid, + cursor.getString(cursor.getColumnIndex(Conversation.UUID)), + }; + db.execSQL("update " + Conversation.TABLENAME + + " set " + Conversation.CONTACTJID + " = ? " + + " where " + Conversation.UUID + " = ?", updateArgs); + } + cursor.close(); + + // Contact table + cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]); + while (cursor.moveToNext()) { + String newJid; + try { + newJid = Jid.fromString( + cursor.getString(cursor.getColumnIndex(Contact.JID)) + ).toPreppedString(); + } catch (InvalidJidException ignored) { + Log.e(Config.LOGTAG, "Failed to migrate Contact JID " + + cursor.getString(cursor.getColumnIndex(Contact.JID)) + + ": " + ignored + ". Skipping..."); + continue; + } + + String updateArgs[] = { + newJid, + cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)), + cursor.getString(cursor.getColumnIndex(Contact.JID)), + }; + db.execSQL("update " + Contact.TABLENAME + + " set " + Contact.JID + " = ? " + + " where " + Contact.ACCOUNT + " = ? " + + " AND " + Contact.JID + " = ?", updateArgs); + } + cursor.close(); + + // Account table + cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]); + while (cursor.moveToNext()) { + String newServer; + try { + newServer = Jid.fromParts( + cursor.getString(cursor.getColumnIndex(Account.USERNAME)), + cursor.getString(cursor.getColumnIndex(Account.SERVER)), + "mobile" + ).getDomainpart(); + } catch (InvalidJidException ignored) { + Log.e(Config.LOGTAG, "Failed to migrate Account SERVER " + + cursor.getString(cursor.getColumnIndex(Account.SERVER)) + + ": " + ignored + ". Skipping..."); + continue; + } + + String updateArgs[] = { + newServer, + cursor.getString(cursor.getColumnIndex(Account.UUID)), + }; + db.execSQL("update " + Account.TABLENAME + + " set " + Account.SERVER + " = ? " + + " where " + Account.UUID + " = ?", updateArgs); + } + cursor.close(); + } + + public static synchronized DatabaseBackend getInstance(Context context) { + if (instance == null) { + instance = new DatabaseBackend(context); + } + return instance; + } + + public void createConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Conversation.TABLENAME, null, conversation.getContentValues()); + } + + public void createMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Message.TABLENAME, null, message.getContentValues()); + } + + public void createAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Account.TABLENAME, null, account.getContentValues()); + } + + public void insertDiscoveryResult(ServiceDiscoveryResult result) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues()); + } + + public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = {hash, ver}; + Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null, + ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + cursor.close(); + return null; + } + cursor.moveToFirst(); + + ServiceDiscoveryResult result = null; + try { + result = new ServiceDiscoveryResult(cursor); + } catch (JSONException e) { /* result is still null */ } + + cursor.close(); + return result; + } + + public void insertPresenceTemplate(PresenceTemplate template) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues()); + } + + public List<PresenceTemplate> getPresenceTemplates() { + ArrayList<PresenceTemplate> templates = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query(PresenceTemplate.TABELNAME, null, null, null, null, null, PresenceTemplate.LAST_USED + " desc"); + while (cursor.moveToNext()) { + templates.add(PresenceTemplate.fromCursor(cursor)); + } + cursor.close(); + return templates; + } + + public void deletePresenceTemplate(PresenceTemplate template) { + Log.d(Config.LOGTAG, "deleting presence template with uuid " + template.getUuid()); + SQLiteDatabase db = this.getWritableDatabase(); + String where = PresenceTemplate.UUID + "=?"; + String[] whereArgs = {template.getUuid()}; + db.delete(PresenceTemplate.TABELNAME, where, whereArgs); + } + + public CopyOnWriteArrayList<Conversation> getConversations(int status) { + CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = {Integer.toString(status)}; + Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME + + " where " + Conversation.STATUS + " = ? order by " + + Conversation.CREATED + " desc", selectionArgs); + while (cursor.moveToNext()) { + list.add(Conversation.fromCursor(cursor)); + } + cursor.close(); + return list; + } + + public ArrayList<Message> getMessages(Conversation conversations, int limit) { + return getMessages(conversations, limit, -1); + } + + public ArrayList<Message> getMessages(Conversation conversation, int limit, + long timestamp) { + ArrayList<Message> list = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + if (timestamp == -1) { + String[] selectionArgs = {conversation.getUuid()}; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + + " DESC", String.valueOf(limit)); + } else { + String[] selectionArgs = {conversation.getUuid(), + Long.toString(timestamp)}; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? and " + Message.TIME_SENT + "<?", selectionArgs, + null, null, Message.TIME_SENT + " DESC", + String.valueOf(limit)); + } + if (cursor.getCount() > 0) { + cursor.moveToLast(); + do { + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); + } while (cursor.moveToPrevious()); + } + cursor.close(); + return list; + } + + public Iterable<Message> getMessagesIterable(final Conversation conversation) { + return new Iterable<Message>() { + @Override + public Iterator<Message> iterator() { + class MessageIterator implements Iterator<Message> { + SQLiteDatabase db = getReadableDatabase(); + String[] selectionArgs = {conversation.getUuid()}; + Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + + " ASC", null); + + public MessageIterator() { + cursor.moveToFirst(); + } + + @Override + public boolean hasNext() { + return !cursor.isAfterLast(); + } + + @Override + public Message next() { + Message message = Message.fromCursor(cursor); + cursor.moveToNext(); + return message; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + return new MessageIterator(); + } + }; + } + + public Conversation findConversation(final Account account, final Jid contactJid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = {account.getUuid(), + contactJid.toBareJid().toPreppedString() + "/%", + contactJid.toBareJid().toPreppedString() + }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID + + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + cursor.close(); + return null; + } + cursor.moveToFirst(); + Conversation conversation = Conversation.fromCursor(cursor); + cursor.close(); + return conversation; + } + + public void updateConversation(final Conversation conversation) { + final SQLiteDatabase db = this.getWritableDatabase(); + final String[] args = {conversation.getUuid()}; + db.update(Conversation.TABLENAME, conversation.getContentValues(), + Conversation.UUID + "=?", args); + } + + public List<Account> getAccounts() { + SQLiteDatabase db = this.getReadableDatabase(); + return getAccounts(db); + } + + private List<Account> getAccounts(SQLiteDatabase db) { + List<Account> list = new ArrayList<>(); + Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, + null, null); + while (cursor.moveToNext()) { + list.add(Account.fromCursor(cursor)); + } + cursor.close(); + return list; + } + + public boolean updateAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid()}; + final int rows = db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args); + return rows == 1; + } + + public boolean deleteAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid()}; + final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args); + return rows == 1; + } + + public boolean hasEnabledAccounts() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from " + + Account.TABLENAME + " where not options & (1 <<1)", null); + try { + cursor.moveToFirst(); + int count = cursor.getInt(0); + return (count > 0); + } catch (SQLiteCantOpenDatabaseException e) { + return true; // better safe than sorry + } catch (RuntimeException e) { + return true; // better safe than sorry + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @Override + public SQLiteDatabase getWritableDatabase() { + SQLiteDatabase db = super.getWritableDatabase(); + db.execSQL("PRAGMA foreign_keys=ON;"); + return db; + } + + public void updateMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {message.getUuid()}; + db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + + "=?", args); + } + + public void updateMessage(Message message, String uuid) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {uuid}; + db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + + "=?", args); + } + + public void readRoster(Roster roster) { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + String args[] = {roster.getAccount().getUuid()}; + cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null); + while (cursor.moveToNext()) { + roster.initContact(Contact.fromCursor(cursor)); + } + cursor.close(); + } + + public void writeRoster(final Roster roster) { + final Account account = roster.getAccount(); + final SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransaction(); + for (Contact contact : roster.getContacts()) { + if (contact.getOption(Contact.Options.IN_ROSTER)) { + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } else { + String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; + String[] whereArgs = {account.getUuid(), contact.getJid().toPreppedString()}; + db.delete(Contact.TABLENAME, where, whereArgs); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + account.setRosterVersion(roster.getVersion()); + updateAccount(account); + } + + public void deleteMessagesInConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {conversation.getUuid()}; + db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); + } + + public Pair<Long, String> getLastMessageReceived(Account account) { + Cursor cursor = null; + try { + SQLiteDatabase db = this.getReadableDatabase(); + String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1"; + String[] args = {account.getUuid()}; + cursor = db.rawQuery(sql, args); + if (cursor.getCount() == 0) { + return null; + } else { + cursor.moveToFirst(); + return new Pair<>(cursor.getLong(0), cursor.getString(1)); + } + } catch (Exception e) { + return null; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + public Pair<Long, String> getLastClearDate(Account account) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {Conversation.ATTRIBUTES}; + String selection = Conversation.ACCOUNT + "=?"; + String[] args = {account.getUuid()}; + Cursor cursor = db.query(Conversation.TABLENAME, columns, selection, args, null, null, null); + long maxClearDate = 0; + while (cursor.moveToNext()) { + try { + final JSONObject jsonObject = new JSONObject(cursor.getString(0)); + maxClearDate = Math.max(maxClearDate, jsonObject.getLong(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY)); + } catch (Exception e) { + //ignored + } + } + cursor.close(); + return new Pair<>(maxClearDate, null); + } + + private Cursor getCursorForSession(Account account, AxolotlAddress contact) { + final SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = {account.getUuid(), + contact.getName(), + Integer.toString(contact.getDeviceId())}; + return db.query(SQLiteAxolotlStore.SESSION_TABLENAME, + null, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + " = ? ", + selectionArgs, + null, null, null); + } + + public SessionRecord loadSession(Account account, AxolotlAddress contact) { + SessionRecord session = null; + Cursor cursor = getCursorForSession(account, contact); + if (cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + } catch (IOException e) { + cursor.close(); + throw new AssertionError(e); + } + } + cursor.close(); + return session; + } + + public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) { + final SQLiteDatabase db = this.getReadableDatabase(); + return getSubDeviceSessions(db, account, contact); + } + + private List<Integer> getSubDeviceSessions(SQLiteDatabase db, Account account, AxolotlAddress contact) { + List<Integer> devices = new ArrayList<>(); + String[] columns = {SQLiteAxolotlStore.DEVICE_ID}; + String[] selectionArgs = {account.getUuid(), + contact.getName()}; + Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ?", + selectionArgs, + null, null, null); + + while (cursor.moveToNext()) { + devices.add(cursor.getInt( + cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID))); + } + + cursor.close(); + return devices; + } + + public boolean containsSession(Account account, AxolotlAddress contact) { + Cursor cursor = getCursorForSession(account, contact); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.NAME, contact.getName()); + values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values); + } + + public void deleteSession(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + deleteSession(db, account, contact); + } + + private void deleteSession(SQLiteDatabase db, Account account, AxolotlAddress contact) { + String[] args = {account.getUuid(), + contact.getName(), + Integer.toString(contact.getDeviceId())}; + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.NAME + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + " = ? ", + args); + } + + public void deleteAllSessions(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), contact.getName()}; + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.NAME + " = ?", + args); + } + + private Cursor getCursorForPreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; + Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public PreKeyRecord loadPreKey(Account account, int preKeyId) { + PreKeyRecord record = null; + Cursor cursor = getCursorForPreKey(account, preKeyId); + if (cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + } catch (IOException e) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public boolean containsPreKey(Account account, int preKeyId) { + Cursor cursor = getCursorForPreKey(account, preKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storePreKey(Account account, PreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ID, record.getId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); + } + + public void deletePreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(preKeyId)}; + db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", + args); + } + + private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; + Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { + SignedPreKeyRecord record = null; + Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId); + if (cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + } catch (IOException e) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) { + List<SignedPreKeyRecord> prekeys = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid()}; + Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=?", + selectionArgs, + null, null, null); + + while (cursor.moveToNext()) { + try { + prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); + } catch (IOException ignored) { + } + } + cursor.close(); + return prekeys; + } + + public boolean containsSignedPreKey(Account account, int signedPreKeyId) { + Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ID, record.getId()); + values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); + } + + public void deleteSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; + db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + + SQLiteAxolotlStore.ID + "=?", + args); + } + + private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { + final SQLiteDatabase db = this.getReadableDatabase(); + return getIdentityKeyCursor(db, account, name, own); + } + + private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, boolean own) { + return getIdentityKeyCursor(db, account, name, own, null); + } + + private Cursor getIdentityKeyCursor(Account account, String fingerprint) { + final SQLiteDatabase db = this.getReadableDatabase(); + return getIdentityKeyCursor(db, account, fingerprint); + } + + private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String fingerprint) { + return getIdentityKeyCursor(db, account, null, null, fingerprint); + } + + private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) { + String[] columns = {SQLiteAxolotlStore.TRUST, + SQLiteAxolotlStore.ACTIVE, + SQLiteAxolotlStore.KEY}; + ArrayList<String> selectionArgs = new ArrayList<>(4); + selectionArgs.add(account.getUuid()); + String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?"; + if (name != null) { + selectionArgs.add(name); + selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?"; + } + if (fingerprint != null) { + selectionArgs.add(fingerprint); + selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?"; + } + if (own != null) { + selectionArgs.add(own ? "1" : "0"); + selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?"; + } + Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, + columns, + selectionString, + selectionArgs.toArray(new String[selectionArgs.size()]), + null, null, null); + + return cursor; + } + + public IdentityKeyPair loadOwnIdentityKeyPair(Account account) { + SQLiteDatabase db = getReadableDatabase(); + return loadOwnIdentityKeyPair(db, account); + } + + private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) { + String name = account.getJid().toBareJid().toPreppedString(); + IdentityKeyPair identityKeyPair = null; + Cursor cursor = getIdentityKeyCursor(db, account, name, true); + if (cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + } + } + cursor.close(); + + return identityKeyPair; + } + + public Set<IdentityKey> loadIdentityKeys(Account account, String name) { + return loadIdentityKeys(account, name, null); + } + + public Set<IdentityKey> loadIdentityKeys(Account account, String name, FingerprintStatus status) { + Set<IdentityKey> identityKeys = new HashSet<>(); + Cursor cursor = getIdentityKeyCursor(account, name, false); + + while (cursor.moveToNext()) { + if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) { + continue; + } + try { + String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)); + if (key != null) { + identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0)); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name); + } + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + } + } + cursor.close(); + + return identityKeys; + } + + public long numTrustedKeys(Account account, String name) { + SQLiteDatabase db = getReadableDatabase(); + String[] args = { + account.getUuid(), + name, + FingerprintStatus.Trust.TRUSTED.toString(), + FingerprintStatus.Trust.VERIFIED.toString(), + FingerprintStatus.Trust.VERIFIED_X509.toString() + }; + return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?" + + " AND " + SQLiteAxolotlStore.NAME + " = ?" + + " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ?)" + + " AND " + SQLiteAxolotlStore.ACTIVE + " > 0", + args + ); + } + + private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(SQLiteAxolotlStore.NAME, name); + values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0); + values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); + values.put(SQLiteAxolotlStore.KEY, base64Serialized); + values.putAll(status.toContentValues()); + String where = SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.NAME + "=? AND " + SQLiteAxolotlStore.FINGERPRINT + " =?"; + String[] whereArgs = {account.getUuid(), name, fingerprint}; + int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, where, whereArgs); + if (rows == 0) { + db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); + } + } + + public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(SQLiteAxolotlStore.NAME, name); + values.put(SQLiteAxolotlStore.OWN, 0); + values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); + values.putAll(status.toContentValues()); + db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); + } + + public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) { + Cursor cursor = getIdentityKeyCursor(account, fingerprint); + final FingerprintStatus status; + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + status = FingerprintStatus.fromCursor(cursor); + } else { + status = null; + } + cursor.close(); + return status; + } + + public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) { + SQLiteDatabase db = this.getWritableDatabase(); + return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus); + } + + private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) { + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(), + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs); + return rows == 1; + } + + public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + try { + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded()); + return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs) == 1; + } catch (CertificateEncodingException e) { + Log.d(Config.LOGTAG, "could not encode certificate"); + return false; + } + } + + public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + String[] colums = {SQLiteAxolotlStore.CERTIFICATE}; + String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? "; + Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null); + if (cursor.getCount() < 1) { + return null; + } else { + cursor.moveToFirst(); + byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE)); + cursor.close(); + if (certificate == null || certificate.length == 0) { + return null; + } + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); + } catch (CertificateException e) { + Log.d(Config.LOGTAG, "certificate exception " + e.getMessage()); + return null; + } + } + } + + public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) { + storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status); + } + + public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) { + storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false)); + } + + public void recreateAxolotlDb(SQLiteDatabase db) { + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<"); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME); + db.execSQL(CREATE_IDENTITIES_STATEMENT); + } + + public void wipeAxolotlDb(Account account) { + String accountName = account.getUuid(); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + SQLiteDatabase db = this.getWritableDatabase(); + String[] deleteArgs = { + accountName + }; + db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + } + + public boolean startTimeCountExceedsThreshold() { + SQLiteDatabase db = this.getWritableDatabase(); + long cleanBeforeTimestamp = System.currentTimeMillis() - Config.FREQUENT_RESTARTS_DETECTION_WINDOW; + db.execSQL("delete from " + START_TIMES_TABLE + " where timestamp < " + cleanBeforeTimestamp); + ContentValues values = new ContentValues(); + values.put("timestamp", System.currentTimeMillis()); + db.insert(START_TIMES_TABLE, null, values); + String[] columns = new String[]{"count(timestamp)"}; + Cursor cursor = db.query(START_TIMES_TABLE, columns, null, null, null, null, null); + int count; + if (cursor.moveToFirst()) { + count = cursor.getInt(0); + } else { + count = 0; + } + cursor.close(); + Log.d(Config.LOGTAG, "start time counter reached " + count); + return count >= Config.FREQUENT_RESTARTS_THRESHOLD; + } + + public void clearStartTimeCounter() { + Log.d(Config.LOGTAG, "resetting start time counter"); + SQLiteDatabase db = this.getWritableDatabase(); + db.execSQL("delete from " + START_TIMES_TABLE); + } } diff --git a/src/main/java/de/pixart/messenger/persistance/FileBackend.java b/src/main/java/de/pixart/messenger/persistance/FileBackend.java index 9babae1de..893834ad7 100644 --- a/src/main/java/de/pixart/messenger/persistance/FileBackend.java +++ b/src/main/java/de/pixart/messenger/persistance/FileBackend.java @@ -58,918 +58,918 @@ import de.pixart.messenger.utils.FileWriterException; import de.pixart.messenger.xmpp.pep.Avatar; public class FileBackend { - private static final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); - - public static final String CONVERSATIONS_FILE_PROVIDER = "de.pixart.messenger.files"; - - private XmppConnectionService mXmppConnectionService; - - public FileBackend(XmppConnectionService service) { - this.mXmppConnectionService = service; - } - - private void createNoMedia() { - final File nomedia_files = new File(getConversationsFileDirectory()+".nomedia"); - final File nomedia_audios = new File(getConversationsAudioDirectory()+".nomedia"); - if (!nomedia_files.exists()) { - try { - nomedia_files.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for files directory"); - } - } - if (!nomedia_audios.exists()) { - try { - nomedia_audios.createNewFile(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "could not create nomedia file for audio directory"); - } - } - } - - public void updateMediaScanner(File file) { - if (file.getAbsolutePath().startsWith(getConversationsImageDirectory()) - || file.getAbsolutePath().startsWith(getConversationsVideoDirectory())) { - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); - intent.setData(Uri.fromFile(file)); - mXmppConnectionService.sendBroadcast(intent); - } else { - createNoMedia(); - } - } - - public boolean deleteFile(Message message) { - File file = getFile(message); - if (file.delete()) { - updateMediaScanner(file); - return true; - } else { - return false; - } - } - - public DownloadableFile getFile(Message message) { - return getFile(message, true); - } - - public DownloadableFile getFile(Message message, boolean decrypted) { - final boolean encrypted = !decrypted - && (message.getEncryption() == Message.ENCRYPTION_PGP - || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); - final DownloadableFile file; - String path = message.getRelativeFilePath(); - if (path == null) { - String filename = fileDateFormat.format(new Date(message.getTimeSent()))+"_"+message.getUuid().substring(0,4); - path = filename; - } - if (path.startsWith("/")) { - file = new DownloadableFile(path); - } else { - String mime = message.getMimeType(); - if (mime != null && mime.startsWith("image")) { - file = new DownloadableFile(getConversationsImageDirectory() + path); - } else if (mime != null && mime.startsWith("video")) { - file = new DownloadableFile(getConversationsVideoDirectory() + path); - } else if (mime != null && mime.startsWith("audio")) { - file = new DownloadableFile(getConversationsAudioDirectory() + path); - } else { - file = new DownloadableFile(getConversationsFileDirectory() + path); - } - } - if (encrypted) { - return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp"); - } else { - return file; - } - } - - private static long getFileSize(Context context, Uri uri) { - Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); - if (cursor != null && cursor.moveToFirst()) { - return cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)); - } else { - return -1; - } - } - - public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) { - if (max <= 0) { - Log.d(Config.LOGTAG,"server did not report max file size for http upload"); - return true; //exception to be compatible with HTTP Upload < v0.2 - } - for(Uri uri : uris) { - if (FileBackend.getFileSize(context, uri) > max) { - Log.d(Config.LOGTAG,"not all files are under "+max+" bytes. suggesting falling back to jingle"); - return false; - } - } - return true; - } - - public static String getConversationsFileDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Pix-Art Messenger/files/"; - } - - public static String getConversationsImageDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Pix-Art Messenger/images/"; - } - - public static String getConversationsVideoDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Pix-Art Messenger/videos/"; - } - - public static String getConversationsAudioDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Pix-Art Messenger/audios/"; - } - - public static String getConversationsDirectory() { - return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Pix-Art Messenger/"; - } - - public Bitmap resize(Bitmap originalBitmap, int size) { - int w = originalBitmap.getWidth(); - int h = originalBitmap.getHeight(); - if (Math.max(w, h) > size) { - int scalledW; - int scalledH; - if (w <= h) { - scalledW = (int) (w / ((double) h / size)); - scalledH = size; - } else { - scalledW = size; - scalledH = (int) (h / ((double) w / size)); - } - Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); - if (originalBitmap != null && !originalBitmap.isRecycled()) { - originalBitmap.recycle(); - } - return result; - } else { - return originalBitmap; - } - } - - public static Bitmap rotate(Bitmap bitmap, int degree) { - if (degree == 0) { - return bitmap; - } - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - Matrix mtx = new Matrix(); - mtx.postRotate(degree); - Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); - if (bitmap != null && !bitmap.isRecycled()) { - bitmap.recycle(); - } - return result; - } - - public boolean useImageAsIs(Uri uri) { - String path = getOriginalPath(uri); - if (path == null) { - return false; - } - File file = new File(path); - long size = file.length(); - if (size == 0 || size >= Config.IMAGE_MAX_SIZE ) { - return false; - } - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - try { - BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options); - if (options == null || options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) { - return false; - } - return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); - } catch (FileNotFoundException e) { - return false; - } - } - - public String getOriginalPath(Uri uri) { - return FileUtils.getPath(mXmppConnectionService,uri); - } - - public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { - Log.d(Config.LOGTAG,"copy file ("+uri.toString()+") to private storage "+file.getAbsolutePath()); - file.getParentFile().mkdirs(); - OutputStream os = null; - InputStream is = null; - try { - file.createNewFile(); - os = new FileOutputStream(file); - is = mXmppConnectionService.getContentResolver().openInputStream(uri); - byte[] buffer = new byte[1024]; - int length; - while ((length = is.read(buffer)) > 0) { - try { - os.write(buffer, 0, length); - } catch (IOException e) { - throw new FileWriterException(); - } - } - try { - os.flush(); - } catch (IOException e) { - throw new FileWriterException(); - } - } catch(FileNotFoundException e) { - throw new FileCopyException(R.string.error_file_not_found); - } catch(FileWriterException e) { - throw new FileCopyException(R.string.error_unable_to_create_temporary_file); - } catch (IOException e) { - e.printStackTrace(); - throw new FileCopyException(R.string.error_io_exception); - } finally { - close(os); - close(is); - } - } - - public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { - String mime = mXmppConnectionService.getContentResolver().getType(uri); - Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime="+mime+")"); - String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); - if (extension == null) { - extension = getExtensionFromUri(uri); - } - String filename = fileDateFormat.format(new Date(message.getTimeSent()))+"_"+message.getUuid().substring(0,4); - message.setRelativeFilePath(filename + "." + extension); - copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); - } - - private String getExtensionFromUri(Uri uri) { - String[] projection = {MediaStore.MediaColumns.DATA}; - String filename = null; - Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - filename = cursor.getString(0); - } - } catch (Exception e) { - filename = null; - } finally { - cursor.close(); - } - } - int pos = filename == null ? -1 : filename.lastIndexOf('.'); - return pos > 0 ? filename.substring(pos+1) : null; - } - - private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException { - file.getParentFile().mkdirs(); - InputStream is = null; - OutputStream os = null; - try { - if (!file.exists() && !file.createNewFile()) { - throw new FileCopyException(R.string.error_unable_to_create_temporary_file); - } - is = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - throw new FileCopyException(R.string.error_not_an_image_file); - } - Bitmap originalBitmap; - BitmapFactory.Options options = new BitmapFactory.Options(); - int inSampleSize = (int) Math.pow(2, sampleSize); - Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); - options.inSampleSize = inSampleSize; - originalBitmap = BitmapFactory.decodeStream(is, null, options); - is.close(); - if (originalBitmap == null) { - throw new FileCopyException(R.string.error_not_an_image_file); - } - Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE); - int rotation = getRotation(image); - scaledBitmap = rotate(scaledBitmap, rotation); - boolean targetSizeReached = false; - int quality = Config.IMAGE_QUALITY; - while(!targetSizeReached) { - os = new FileOutputStream(file); - boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os); - if (!success) { - throw new FileCopyException(R.string.error_compressing_image); - } - os.flush(); - targetSizeReached = file.length() <= Config.IMAGE_MAX_SIZE || quality <= 50; - quality -= 5; - } - scaledBitmap.recycle(); - } catch (FileNotFoundException e) { - throw new FileCopyException(R.string.error_file_not_found); - } catch (IOException e) { - e.printStackTrace(); - throw new FileCopyException(R.string.error_io_exception); - } catch (SecurityException e) { - throw new FileCopyException(R.string.error_security_exception_during_image_copy); - } catch (OutOfMemoryError e) { - ++sampleSize; - if (sampleSize <= 3) { - copyImageToPrivateStorage(file, image, sampleSize); - } else { - throw new FileCopyException(R.string.error_out_of_memory); - } - } finally { - close(os); - close(is); - } - } - - public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException { - Log.d(Config.LOGTAG,"copy image ("+image.toString()+") to private storage "+file.getAbsolutePath()); - copyImageToPrivateStorage(file, image, 0); - } - - public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException { - String filename = fileDateFormat.format(new Date(message.getTimeSent()))+"_"+message.getUuid().substring(0,4); - switch(Config.IMAGE_FORMAT) { - case JPEG: - message.setRelativeFilePath(filename+".jpg"); - break; - case PNG: - message.setRelativeFilePath(filename+".png"); - break; - case WEBP: - message.setRelativeFilePath(filename+".webp"); - break; - } - copyImageToPrivateStorage(getFile(message), image); - updateFileParams(message); - } - - private int getRotation(File file) { - return getRotation(Uri.parse("file://"+file.getAbsolutePath())); - } - - private int getRotation(Uri image) { - InputStream is = null; - try { - is = mXmppConnectionService.getContentResolver().openInputStream(image); - return ExifHelper.getOrientation(is); - } catch (FileNotFoundException e) { - return 0; - } finally { - close(is); - } - } - - public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException { - final String uuid = message.getUuid(); - final LruCache<String,Bitmap> cache = mXmppConnectionService.getBitmapCache(); - Bitmap thumbnail = cache.get(uuid); - if ((thumbnail == null) && (!cacheOnly)) { - synchronized (cache) { - thumbnail = cache.get(uuid); - if (thumbnail != null) { - return thumbnail; - } - DownloadableFile file = getFile(message); - if (file.getMimeType().startsWith("video/")) { - thumbnail = getVideoPreview(file, size); - } else { - Bitmap fullsize = getFullsizeImagePreview(file, size); - if (fullsize == null) { - throw new FileNotFoundException(); - } - thumbnail = resize(fullsize, size); - thumbnail = rotate(thumbnail, getRotation(file)); - } - this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail); - } - } - return thumbnail; - } - - private Bitmap getFullsizeImagePreview(File file, int size) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(file, size); - try { - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); - } catch (OutOfMemoryError e) { - options.inSampleSize *= 2; - return BitmapFactory.decodeFile(file.getAbsolutePath(), options); - } - } - - private Bitmap getVideoPreview(File file, int size) { - MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); - Bitmap frame; - try { - metadataRetriever.setDataSource(file.getAbsolutePath()); - frame = metadataRetriever.getFrameAtTime(0); - metadataRetriever.release(); - frame = resize(frame, size); - } catch(IllegalArgumentException | NullPointerException e) { - frame = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888); - frame.eraseColor(0xff000000); - } - Canvas canvas = new Canvas(frame); - Bitmap play = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), R.drawable.play_video); - float x = (frame.getWidth() - play.getWidth()) / 2.0f; - float y = (frame.getHeight() - play.getHeight()) / 2.0f; - canvas.drawBitmap(play,x,y,null); - return frame; - } - - private static String getTakePhotoPath() { - return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)+"/Camera/"; - } - - public Uri getTakePhotoUri() { - File file = new File(getTakePhotoPath()+"IMG_" + fileDateFormat.format(new Date()) + ".jpg"); - file.getParentFile().mkdirs(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return FileProvider.getUriForFile(mXmppConnectionService, CONVERSATIONS_FILE_PROVIDER, file); - } else { - return Uri.fromFile(file); - } - } - - public static Uri getIndexableTakePhotoUri(Uri original) { - if ("file".equals(original.getScheme())) { - return original; - } else { - List<String> segments = original.getPathSegments(); - return Uri.parse("file://"+getTakePhotoPath()+segments.get(segments.size() - 1)); - } - } - - public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { - try { - Avatar avatar = new Avatar(); - Bitmap bm = cropCenterSquare(image, size); - if (bm == null) { - return null; - } - ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream( - mByteArrayOutputStream, Base64.DEFAULT); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - DigestOutputStream mDigestOutputStream = new DigestOutputStream( - mBase64OutputStream, digest); - if (!bm.compress(format, 75, mDigestOutputStream)) { - return null; - } - mDigestOutputStream.flush(); - mDigestOutputStream.close(); - avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); - avatar.image = new String(mByteArrayOutputStream.toByteArray()); - return avatar; - } catch (NoSuchAlgorithmException e) { - return null; - } catch (IOException e) { - return null; - } - } - - public Avatar getStoredPepAvatar(String hash) { - if (hash == null) { - return null; - } - Avatar avatar = new Avatar(); - File file = new File(getAvatarPath(hash)); - FileInputStream is = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - is = new FileInputStream(file); - ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); - Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest); - byte[] buffer = new byte[4096]; - int length; - while ((length = is.read(buffer)) > 0) { - os.write(buffer, 0, length); - } - os.flush(); - os.close(); - avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); - avatar.image = new String(mByteArrayOutputStream.toByteArray()); - avatar.height = options.outHeight; - avatar.width = options.outWidth; - return avatar; - } catch (IOException e) { - return null; - } catch (NoSuchAlgorithmException e) { - return null; - } finally { - close(is); - } - } - - public boolean isAvatarCached(Avatar avatar) { - File file = new File(getAvatarPath(avatar.getFilename())); - return file.exists(); - } - - public boolean save(Avatar avatar) { - File file; - if (isAvatarCached(avatar)) { - file = new File(getAvatarPath(avatar.getFilename())); - } else { - String filename = getAvatarPath(avatar.getFilename()); - file = new File(filename + ".tmp"); - file.getParentFile().mkdirs(); - OutputStream os = null; - try { - file.createNewFile(); - os = new FileOutputStream(file); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest); - mDigestOutputStream.write(avatar.getImageAsBytes()); - mDigestOutputStream.flush(); - mDigestOutputStream.close(); - String sha1sum = CryptoHelper.bytesToHex(digest.digest()); - if (sha1sum.equals(avatar.sha1sum)) { - file.renameTo(new File(filename)); - } else { - Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); - file.delete(); - return false; - } - } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) { - return false; - } finally { - close(os); - } - } - avatar.size = file.length(); - return true; - } - - public String getAvatarPath(String avatar) { - return mXmppConnectionService.getFilesDir().getAbsolutePath()+ "/avatars/" + avatar; - } - - public Uri getAvatarUri(String avatar) { - return Uri.parse("file:" + getAvatarPath(avatar)); - } - - public Bitmap cropCenterSquare(Uri image, int size) { - if (image == null) { - return null; - } - InputStream is = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(image, size); - is = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - return null; - } - Bitmap input = BitmapFactory.decodeStream(is, null, options); - if (input == null) { - return null; - } else { - input = rotate(input, getRotation(image)); - return cropCenterSquare(input, size); - } - } catch (SecurityException e) { - return null; // happens for example on Android 6.0 if contacts permissions get revoked - } catch (FileNotFoundException e) { - return null; - } finally { - close(is); - } - } - - public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { - if (image == null) { - return null; - } - InputStream is = null; - try { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth)); - is = mXmppConnectionService.getContentResolver().openInputStream(image); - if (is == null) { - return null; - } - Bitmap source = BitmapFactory.decodeStream(is, null, options); - if (source == null) { - return null; - } - int sourceWidth = source.getWidth(); - int sourceHeight = source.getHeight(); - float xScale = (float) newWidth / sourceWidth; - float yScale = (float) newHeight / sourceHeight; - float scale = Math.max(xScale, yScale); - float scaledWidth = scale * sourceWidth; - float scaledHeight = scale * sourceHeight; - float left = (newWidth - scaledWidth) / 2; - float top = (newHeight - scaledHeight) / 2; - - RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); - Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(dest); - canvas.drawBitmap(source, null, targetRect, null); - if (source != null && !source.isRecycled()) { - source.recycle(); - } - return dest; - } catch (SecurityException e) { - return null; //android 6.0 with revoked permissions for example - } catch (FileNotFoundException e) { - return null; - } finally { - close(is); - } - } - - public Bitmap cropCenterSquare(Bitmap input, int size) { - int w = input.getWidth(); - int h = input.getHeight(); - - float scale = Math.max((float) size / h, (float) size / w); - - float outWidth = scale * w; - float outHeight = scale * h; - float left = (size - outWidth) / 2; - float top = (size - outHeight) / 2; - RectF target = new RectF(left, top, left + outWidth, top + outHeight); - - Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(output); - canvas.drawBitmap(input, null, target, null); - if (input != null && !input.isRecycled()) { - input.recycle(); - } - return output; - } - - private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options); - return calcSampleSize(options, size); - } - - private static int calcSampleSize(File image, int size) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(image.getAbsolutePath(), options); - return calcSampleSize(options, size); - } - - public static int calcSampleSize(BitmapFactory.Options options, int size) { - int height = options.outHeight; - int width = options.outWidth; - int inSampleSize = 1; - - if (height > size || width > size) { - int halfHeight = height / 2; - int halfWidth = width / 2; - - while ((halfHeight / inSampleSize) > size - && (halfWidth / inSampleSize) > size) { - inSampleSize *= 2; - } - } - return inSampleSize; - } - - public Uri getJingleFileUri(Message message) { - File file = getFile(message); - return Uri.parse("file://" + file.getAbsolutePath()); - } - - public void updateFileParams(Message message) { - updateFileParams(message,null); - } - - public void updateFileParams(Message message, URL url) { - DownloadableFile file = getFile(message); - final String mime = file.getMimeType(); - boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); - boolean video = mime != null && mime.startsWith("video/"); - if (image || video) { - try { - Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file); - if (url == null) { - message.setBody(Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height); - } else { - message.setBody(url.toString() + "|" + Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height); - } - return; - } catch (NotAVideoFile notAVideoFile) { - Log.d(Config.LOGTAG,"file with mime type "+file.getMimeType()+" was not a video file"); - //fall threw - } - } - if (url != null) { - message.setBody(url.toString()+"|"+Long.toString(file.getSize())); - } else { - message.setBody(Long.toString(file.getSize())); - } - - } - - private Dimensions getImageDimensions(File file) { - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeFile(file.getAbsolutePath(), options); - int rotation = getRotation(file); - boolean rotated = rotation == 90 || rotation == 270; - int imageHeight = rotated ? options.outWidth : options.outHeight; - int imageWidth = rotated ? options.outHeight : options.outWidth; - return new Dimensions(imageHeight, imageWidth); - } - - private Dimensions getVideoDimensions(File file) throws NotAVideoFile { - MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); - try { - metadataRetriever.setDataSource(file.getAbsolutePath()); - } catch (Exception e) { - throw new NotAVideoFile(); - } - String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); - if (hasVideo == null) { - throw new NotAVideoFile(); - } - int rotation = extractRotationFromMediaRetriever(metadataRetriever); - boolean rotated = rotation == 90 || rotation == 270; - int height; - try { - String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); - height = Integer.parseInt(h); - } catch (Exception e) { - height = -1; - } - int width; - try { - String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); - width = Integer.parseInt(w); - } catch (Exception e) { - width = -1; - } - metadataRetriever.release(); - Log.d(Config.LOGTAG,"extracted video dims "+width+"x"+height); - return rotated ? new Dimensions(width, height) : new Dimensions(height, width); - } - - private int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) { - int rotation; - if (Build.VERSION.SDK_INT >= 17) { - String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); - try { - rotation = Integer.parseInt(r); - } catch (Exception e) { - rotation = 0; - } - } else { - rotation = 0; - } - return rotation; - } - - private class Dimensions { - public final int width; - public final int height; - - public Dimensions(int height, int width) { - this.width = width; - this.height = height; - } - } - - private class NotAVideoFile extends Exception { - - } - - public class FileCopyException extends Exception { - private static final long serialVersionUID = -1010013599132881427L; - private int resId; - - public FileCopyException(int resId) { - this.resId = resId; - } - - public int getResId() { - return resId; - } - } - - public Bitmap getAvatar(String avatar, int size) { - if (avatar == null) { - return null; - } - Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); - if (bm == null) { - return null; - } - return bm; - } - - public boolean isFileAvailable(Message message) { - return getFile(message).exists(); - } - - public static void close(Closeable stream) { - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - } - } - } - - public static void close(Socket socket) { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - } - } - } - - - public static boolean weOwnFile(Context context, Uri uri) { - if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - return false; - } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return fileIsInFilesDir(context, uri); - } else { - return weOwnFileLollipop(uri); - } - } - - - /** - * This is more than hacky but probably way better than doing nothing - * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir - * and check against those as well - */ - private static boolean fileIsInFilesDir(Context context, Uri uri) { - try { - final String haystack = context.getFilesDir().getParentFile().getCanonicalPath(); - final String needle = new File(uri.getPath()).getCanonicalPath(); - return needle.startsWith(haystack); - } catch (IOException e) { - return false; - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private static boolean weOwnFileLollipop(Uri uri) { - try { - File file = new File(uri.getPath()); - FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor(); - StructStat st = Os.fstat(fd); - return st.st_uid == android.os.Process.myUid(); - } catch (FileNotFoundException e) { - return false; - } catch (Exception e) { - return true; - } - } - - public static Bitmap rotateBitmap(File file, Bitmap bitmap, int orientation) { - - if (orientation == 1) { - return bitmap; - } - - Matrix matrix = new Matrix(); - switch (orientation) { - case 2: - matrix.setScale(-1, 1); - break; - case 3: - matrix.setRotate(180); - break; - case 4: - matrix.setRotate(180); - matrix.postScale(-1, 1); - break; - case 5: - matrix.setRotate(90); - matrix.postScale(-1, 1); - break; - case 6: - matrix.setRotate(90); - break; - case 7: - matrix.setRotate(-90); - matrix.postScale(-1, 1); - break; - case 8: - matrix.setRotate(-90); - break; - default: - return bitmap; - } - - try { - Bitmap oriented = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - bitmap.recycle(); - return oriented; - } catch (OutOfMemoryError e) { - e.printStackTrace(); - return bitmap; - } - } + private static final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US); + + public static final String CONVERSATIONS_FILE_PROVIDER = "de.pixart.messenger.files"; + + private XmppConnectionService mXmppConnectionService; + + public FileBackend(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + private void createNoMedia() { + final File nomedia_files = new File(getConversationsFileDirectory() + ".nomedia"); + final File nomedia_audios = new File(getConversationsAudioDirectory() + ".nomedia"); + if (!nomedia_files.exists()) { + try { + nomedia_files.createNewFile(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not create nomedia file for files directory"); + } + } + if (!nomedia_audios.exists()) { + try { + nomedia_audios.createNewFile(); + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not create nomedia file for audio directory"); + } + } + } + + public void updateMediaScanner(File file) { + if (file.getAbsolutePath().startsWith(getConversationsImageDirectory()) + || file.getAbsolutePath().startsWith(getConversationsVideoDirectory())) { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + } else { + createNoMedia(); + } + } + + public boolean deleteFile(Message message) { + File file = getFile(message); + if (file.delete()) { + updateMediaScanner(file); + return true; + } else { + return false; + } + } + + public DownloadableFile getFile(Message message) { + return getFile(message, true); + } + + public DownloadableFile getFile(Message message, boolean decrypted) { + final boolean encrypted = !decrypted + && (message.getEncryption() == Message.ENCRYPTION_PGP + || message.getEncryption() == Message.ENCRYPTION_DECRYPTED); + final DownloadableFile file; + String path = message.getRelativeFilePath(); + if (path == null) { + String filename = fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4); + path = filename; + } + if (path.startsWith("/")) { + file = new DownloadableFile(path); + } else { + String mime = message.getMimeType(); + if (mime != null && mime.startsWith("image")) { + file = new DownloadableFile(getConversationsImageDirectory() + path); + } else if (mime != null && mime.startsWith("video")) { + file = new DownloadableFile(getConversationsVideoDirectory() + path); + } else if (mime != null && mime.startsWith("audio")) { + file = new DownloadableFile(getConversationsAudioDirectory() + path); + } else { + file = new DownloadableFile(getConversationsFileDirectory() + path); + } + } + if (encrypted) { + return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp"); + } else { + return file; + } + } + + private static long getFileSize(Context context, Uri uri) { + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE)); + } else { + return -1; + } + } + + public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) { + if (max <= 0) { + Log.d(Config.LOGTAG, "server did not report max file size for http upload"); + return true; //exception to be compatible with HTTP Upload < v0.2 + } + for (Uri uri : uris) { + if (FileBackend.getFileSize(context, uri) > max) { + Log.d(Config.LOGTAG, "not all files are under " + max + " bytes. suggesting falling back to jingle"); + return false; + } + } + return true; + } + + public static String getConversationsFileDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/files/"; + } + + public static String getConversationsImageDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/images/"; + } + + public static String getConversationsVideoDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/videos/"; + } + + public static String getConversationsAudioDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/audios/"; + } + + public static String getConversationsDirectory() { + return Environment.getExternalStorageDirectory().getAbsolutePath() + "/Pix-Art Messenger/"; + } + + public Bitmap resize(Bitmap originalBitmap, int size) { + int w = originalBitmap.getWidth(); + int h = originalBitmap.getHeight(); + if (Math.max(w, h) > size) { + int scalledW; + int scalledH; + if (w <= h) { + scalledW = (int) (w / ((double) h / size)); + scalledH = size; + } else { + scalledW = size; + scalledH = (int) (h / ((double) w / size)); + } + Bitmap result = Bitmap.createScaledBitmap(originalBitmap, scalledW, scalledH, true); + if (originalBitmap != null && !originalBitmap.isRecycled()) { + originalBitmap.recycle(); + } + return result; + } else { + return originalBitmap; + } + } + + public static Bitmap rotate(Bitmap bitmap, int degree) { + if (degree == 0) { + return bitmap; + } + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + Matrix mtx = new Matrix(); + mtx.postRotate(degree); + Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + return result; + } + + public boolean useImageAsIs(Uri uri) { + String path = getOriginalPath(uri); + if (path == null) { + return false; + } + File file = new File(path); + long size = file.length(); + if (size == 0 || size >= Config.IMAGE_MAX_SIZE) { + return false; + } + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + try { + BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options); + if (options == null || options.outMimeType == null || options.outHeight <= 0 || options.outWidth <= 0) { + return false; + } + return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase())); + } catch (FileNotFoundException e) { + return false; + } + } + + public String getOriginalPath(Uri uri) { + return FileUtils.getPath(mXmppConnectionService, uri); + } + + public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { + Log.d(Config.LOGTAG, "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); + file.getParentFile().mkdirs(); + OutputStream os = null; + InputStream is = null; + try { + file.createNewFile(); + os = new FileOutputStream(file); + is = mXmppConnectionService.getContentResolver().openInputStream(uri); + byte[] buffer = new byte[1024]; + int length; + while ((length = is.read(buffer)) > 0) { + try { + os.write(buffer, 0, length); + } catch (IOException e) { + throw new FileWriterException(); + } + } + try { + os.flush(); + } catch (IOException e) { + throw new FileWriterException(); + } + } catch (FileNotFoundException e) { + throw new FileCopyException(R.string.error_file_not_found); + } catch (FileWriterException e) { + throw new FileCopyException(R.string.error_unable_to_create_temporary_file); + } catch (IOException e) { + e.printStackTrace(); + throw new FileCopyException(R.string.error_io_exception); + } finally { + close(os); + close(is); + } + } + + public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException { + String mime = mXmppConnectionService.getContentResolver().getType(uri); + Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")"); + String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime); + if (extension == null) { + extension = getExtensionFromUri(uri); + } + String filename = fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4); + message.setRelativeFilePath(filename + "." + extension); + copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri); + } + + private String getExtensionFromUri(Uri uri) { + String[] projection = {MediaStore.MediaColumns.DATA}; + String filename = null; + Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + filename = cursor.getString(0); + } + } catch (Exception e) { + filename = null; + } finally { + cursor.close(); + } + } + int pos = filename == null ? -1 : filename.lastIndexOf('.'); + return pos > 0 ? filename.substring(pos + 1) : null; + } + + private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException { + file.getParentFile().mkdirs(); + InputStream is = null; + OutputStream os = null; + try { + if (!file.exists() && !file.createNewFile()) { + throw new FileCopyException(R.string.error_unable_to_create_temporary_file); + } + is = mXmppConnectionService.getContentResolver().openInputStream(image); + if (is == null) { + throw new FileCopyException(R.string.error_not_an_image_file); + } + Bitmap originalBitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + int inSampleSize = (int) Math.pow(2, sampleSize); + Log.d(Config.LOGTAG, "reading bitmap with sample size " + inSampleSize); + options.inSampleSize = inSampleSize; + originalBitmap = BitmapFactory.decodeStream(is, null, options); + is.close(); + if (originalBitmap == null) { + throw new FileCopyException(R.string.error_not_an_image_file); + } + Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE); + int rotation = getRotation(image); + scaledBitmap = rotate(scaledBitmap, rotation); + boolean targetSizeReached = false; + int quality = Config.IMAGE_QUALITY; + while (!targetSizeReached) { + os = new FileOutputStream(file); + boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, quality, os); + if (!success) { + throw new FileCopyException(R.string.error_compressing_image); + } + os.flush(); + targetSizeReached = file.length() <= Config.IMAGE_MAX_SIZE || quality <= 50; + quality -= 5; + } + scaledBitmap.recycle(); + } catch (FileNotFoundException e) { + throw new FileCopyException(R.string.error_file_not_found); + } catch (IOException e) { + e.printStackTrace(); + throw new FileCopyException(R.string.error_io_exception); + } catch (SecurityException e) { + throw new FileCopyException(R.string.error_security_exception_during_image_copy); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize <= 3) { + copyImageToPrivateStorage(file, image, sampleSize); + } else { + throw new FileCopyException(R.string.error_out_of_memory); + } + } finally { + close(os); + close(is); + } + } + + public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException { + Log.d(Config.LOGTAG, "copy image (" + image.toString() + ") to private storage " + file.getAbsolutePath()); + copyImageToPrivateStorage(file, image, 0); + } + + public void copyImageToPrivateStorage(Message message, Uri image) throws FileCopyException { + String filename = fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4); + switch (Config.IMAGE_FORMAT) { + case JPEG: + message.setRelativeFilePath(filename + ".jpg"); + break; + case PNG: + message.setRelativeFilePath(filename + ".png"); + break; + case WEBP: + message.setRelativeFilePath(filename + ".webp"); + break; + } + copyImageToPrivateStorage(getFile(message), image); + updateFileParams(message); + } + + private int getRotation(File file) { + return getRotation(Uri.parse("file://" + file.getAbsolutePath())); + } + + private int getRotation(Uri image) { + InputStream is = null; + try { + is = mXmppConnectionService.getContentResolver().openInputStream(image); + return ExifHelper.getOrientation(is); + } catch (FileNotFoundException e) { + return 0; + } finally { + close(is); + } + } + + public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException { + final String uuid = message.getUuid(); + final LruCache<String, Bitmap> cache = mXmppConnectionService.getBitmapCache(); + Bitmap thumbnail = cache.get(uuid); + if ((thumbnail == null) && (!cacheOnly)) { + synchronized (cache) { + thumbnail = cache.get(uuid); + if (thumbnail != null) { + return thumbnail; + } + DownloadableFile file = getFile(message); + if (file.getMimeType().startsWith("video/")) { + thumbnail = getVideoPreview(file, size); + } else { + Bitmap fullsize = getFullsizeImagePreview(file, size); + if (fullsize == null) { + throw new FileNotFoundException(); + } + thumbnail = resize(fullsize, size); + thumbnail = rotate(thumbnail, getRotation(file)); + } + this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail); + } + } + return thumbnail; + } + + private Bitmap getFullsizeImagePreview(File file, int size) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(file, size); + try { + return BitmapFactory.decodeFile(file.getAbsolutePath(), options); + } catch (OutOfMemoryError e) { + options.inSampleSize *= 2; + return BitmapFactory.decodeFile(file.getAbsolutePath(), options); + } + } + + private Bitmap getVideoPreview(File file, int size) { + MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); + Bitmap frame; + try { + metadataRetriever.setDataSource(file.getAbsolutePath()); + frame = metadataRetriever.getFrameAtTime(0); + metadataRetriever.release(); + frame = resize(frame, size); + } catch (IllegalArgumentException | NullPointerException e) { + frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + frame.eraseColor(0xff000000); + } + Canvas canvas = new Canvas(frame); + Bitmap play = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), R.drawable.play_video); + float x = (frame.getWidth() - play.getWidth()) / 2.0f; + float y = (frame.getHeight() - play.getHeight()) / 2.0f; + canvas.drawBitmap(play, x, y, null); + return frame; + } + + private static String getTakePhotoPath() { + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + "/Camera/"; + } + + public Uri getTakePhotoUri() { + File file = new File(getTakePhotoPath() + "IMG_" + fileDateFormat.format(new Date()) + ".jpg"); + file.getParentFile().mkdirs(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + return FileProvider.getUriForFile(mXmppConnectionService, CONVERSATIONS_FILE_PROVIDER, file); + } else { + return Uri.fromFile(file); + } + } + + public static Uri getIndexableTakePhotoUri(Uri original) { + if ("file".equals(original.getScheme())) { + return original; + } else { + List<String> segments = original.getPathSegments(); + return Uri.parse("file://" + getTakePhotoPath() + segments.get(segments.size() - 1)); + } + } + + public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { + try { + Avatar avatar = new Avatar(); + Bitmap bm = cropCenterSquare(image, size); + if (bm == null) { + return null; + } + ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); + Base64OutputStream mBase64OutputStream = new Base64OutputStream( + mByteArrayOutputStream, Base64.DEFAULT); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mBase64OutputStream, digest); + if (!bm.compress(format, 75, mDigestOutputStream)) { + return null; + } + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); + avatar.image = new String(mByteArrayOutputStream.toByteArray()); + return avatar; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (IOException e) { + return null; + } + } + + public Avatar getStoredPepAvatar(String hash) { + if (hash == null) { + return null; + } + Avatar avatar = new Avatar(); + File file = new File(getAvatarPath(hash)); + FileInputStream is = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + is = new FileInputStream(file); + ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); + Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest); + byte[] buffer = new byte[4096]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + os.flush(); + os.close(); + avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); + avatar.image = new String(mByteArrayOutputStream.toByteArray()); + avatar.height = options.outHeight; + avatar.width = options.outWidth; + return avatar; + } catch (IOException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } finally { + close(is); + } + } + + public boolean isAvatarCached(Avatar avatar) { + File file = new File(getAvatarPath(avatar.getFilename())); + return file.exists(); + } + + public boolean save(Avatar avatar) { + File file; + if (isAvatarCached(avatar)) { + file = new File(getAvatarPath(avatar.getFilename())); + } else { + String filename = getAvatarPath(avatar.getFilename()); + file = new File(filename + ".tmp"); + file.getParentFile().mkdirs(); + OutputStream os = null; + try { + file.createNewFile(); + os = new FileOutputStream(file); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest); + mDigestOutputStream.write(avatar.getImageAsBytes()); + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + } else { + Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); + file.delete(); + return false; + } + } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) { + return false; + } finally { + close(os); + } + } + avatar.size = file.length(); + return true; + } + + public String getAvatarPath(String avatar) { + return mXmppConnectionService.getFilesDir().getAbsolutePath() + "/avatars/" + avatar; + } + + public Uri getAvatarUri(String avatar) { + return Uri.parse("file:" + getAvatarPath(avatar)); + } + + public Bitmap cropCenterSquare(Uri image, int size) { + if (image == null) { + return null; + } + InputStream is = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, size); + is = mXmppConnectionService.getContentResolver().openInputStream(image); + if (is == null) { + return null; + } + Bitmap input = BitmapFactory.decodeStream(is, null, options); + if (input == null) { + return null; + } else { + input = rotate(input, getRotation(image)); + return cropCenterSquare(input, size); + } + } catch (SecurityException e) { + return null; // happens for example on Android 6.0 if contacts permissions get revoked + } catch (FileNotFoundException e) { + return null; + } finally { + close(is); + } + } + + public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { + if (image == null) { + return null; + } + InputStream is = null; + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, Math.max(newHeight, newWidth)); + is = mXmppConnectionService.getContentResolver().openInputStream(image); + if (is == null) { + return null; + } + Bitmap source = BitmapFactory.decodeStream(is, null, options); + if (source == null) { + return null; + } + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + float xScale = (float) newWidth / sourceWidth; + float yScale = (float) newHeight / sourceHeight; + float scale = Math.max(xScale, yScale); + float scaledWidth = scale * sourceWidth; + float scaledHeight = scale * sourceHeight; + float left = (newWidth - scaledWidth) / 2; + float top = (newHeight - scaledHeight) / 2; + + RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); + Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(dest); + canvas.drawBitmap(source, null, targetRect, null); + if (source != null && !source.isRecycled()) { + source.recycle(); + } + return dest; + } catch (SecurityException e) { + return null; //android 6.0 with revoked permissions for example + } catch (FileNotFoundException e) { + return null; + } finally { + close(is); + } + } + + public Bitmap cropCenterSquare(Bitmap input, int size) { + int w = input.getWidth(); + int h = input.getHeight(); + + float scale = Math.max((float) size / h, (float) size / w); + + float outWidth = scale * w; + float outHeight = scale * h; + float left = (size - outWidth) / 2; + float top = (size - outHeight) / 2; + RectF target = new RectF(left, top, left + outWidth, top + outHeight); + + Bitmap output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + canvas.drawBitmap(input, null, target, null); + if (input != null && !input.isRecycled()) { + input.recycle(); + } + return output; + } + + private int calcSampleSize(Uri image, int size) throws FileNotFoundException, SecurityException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(image), null, options); + return calcSampleSize(options, size); + } + + private static int calcSampleSize(File image, int size) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(image.getAbsolutePath(), options); + return calcSampleSize(options, size); + } + + public static int calcSampleSize(BitmapFactory.Options options, int size) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (height > size || width > size) { + int halfHeight = height / 2; + int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) > size + && (halfWidth / inSampleSize) > size) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + public Uri getJingleFileUri(Message message) { + File file = getFile(message); + return Uri.parse("file://" + file.getAbsolutePath()); + } + + public void updateFileParams(Message message) { + updateFileParams(message, null); + } + + public void updateFileParams(Message message, URL url) { + DownloadableFile file = getFile(message); + final String mime = file.getMimeType(); + boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/")); + boolean video = mime != null && mime.startsWith("video/"); + if (image || video) { + try { + Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file); + if (url == null) { + message.setBody(Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height); + } else { + message.setBody(url.toString() + "|" + Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height); + } + return; + } catch (NotAVideoFile notAVideoFile) { + Log.d(Config.LOGTAG, "file with mime type " + file.getMimeType() + " was not a video file"); + //fall threw + } + } + if (url != null) { + message.setBody(url.toString() + "|" + Long.toString(file.getSize())); + } else { + message.setBody(Long.toString(file.getSize())); + } + + } + + private Dimensions getImageDimensions(File file) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int rotation = getRotation(file); + boolean rotated = rotation == 90 || rotation == 270; + int imageHeight = rotated ? options.outWidth : options.outHeight; + int imageWidth = rotated ? options.outHeight : options.outWidth; + return new Dimensions(imageHeight, imageWidth); + } + + private Dimensions getVideoDimensions(File file) throws NotAVideoFile { + MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); + try { + metadataRetriever.setDataSource(file.getAbsolutePath()); + } catch (Exception e) { + throw new NotAVideoFile(); + } + String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO); + if (hasVideo == null) { + throw new NotAVideoFile(); + } + int rotation = extractRotationFromMediaRetriever(metadataRetriever); + boolean rotated = rotation == 90 || rotation == 270; + int height; + try { + String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + height = Integer.parseInt(h); + } catch (Exception e) { + height = -1; + } + int width; + try { + String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); + width = Integer.parseInt(w); + } catch (Exception e) { + width = -1; + } + metadataRetriever.release(); + Log.d(Config.LOGTAG, "extracted video dims " + width + "x" + height); + return rotated ? new Dimensions(width, height) : new Dimensions(height, width); + } + + private int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) { + int rotation; + if (Build.VERSION.SDK_INT >= 17) { + String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + try { + rotation = Integer.parseInt(r); + } catch (Exception e) { + rotation = 0; + } + } else { + rotation = 0; + } + return rotation; + } + + private class Dimensions { + public final int width; + public final int height; + + public Dimensions(int height, int width) { + this.width = width; + this.height = height; + } + } + + private class NotAVideoFile extends Exception { + + } + + public class FileCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public FileCopyException(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } + } + + public Bitmap getAvatar(String avatar, int size) { + if (avatar == null) { + return null; + } + Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); + if (bm == null) { + return null; + } + return bm; + } + + public boolean isFileAvailable(Message message) { + return getFile(message).exists(); + } + + public static void close(Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + + public static void close(Socket socket) { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + } + } + } + + + public static boolean weOwnFile(Context context, Uri uri) { + if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + return false; + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return fileIsInFilesDir(context, uri); + } else { + return weOwnFileLollipop(uri); + } + } + + + /** + * This is more than hacky but probably way better than doing nothing + * Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir + * and check against those as well + */ + private static boolean fileIsInFilesDir(Context context, Uri uri) { + try { + final String haystack = context.getFilesDir().getParentFile().getCanonicalPath(); + final String needle = new File(uri.getPath()).getCanonicalPath(); + return needle.startsWith(haystack); + } catch (IOException e) { + return false; + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static boolean weOwnFileLollipop(Uri uri) { + try { + File file = new File(uri.getPath()); + FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor(); + StructStat st = Os.fstat(fd); + return st.st_uid == android.os.Process.myUid(); + } catch (FileNotFoundException e) { + return false; + } catch (Exception e) { + return true; + } + } + + public static Bitmap rotateBitmap(File file, Bitmap bitmap, int orientation) { + + if (orientation == 1) { + return bitmap; + } + + Matrix matrix = new Matrix(); + switch (orientation) { + case 2: + matrix.setScale(-1, 1); + break; + case 3: + matrix.setRotate(180); + break; + case 4: + matrix.setRotate(180); + matrix.postScale(-1, 1); + break; + case 5: + matrix.setRotate(90); + matrix.postScale(-1, 1); + break; + case 6: + matrix.setRotate(90); + break; + case 7: + matrix.setRotate(-90); + matrix.postScale(-1, 1); + break; + case 8: + matrix.setRotate(-90); + break; + default: + return bitmap; + } + + try { + Bitmap oriented = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + bitmap.recycle(); + return oriented; + } catch (OutOfMemoryError e) { + e.printStackTrace(); + return bitmap; + } + } } diff --git a/src/main/java/de/pixart/messenger/persistance/OnPhoneContactsMerged.java b/src/main/java/de/pixart/messenger/persistance/OnPhoneContactsMerged.java index 85e852b5d..d5253f227 100644 --- a/src/main/java/de/pixart/messenger/persistance/OnPhoneContactsMerged.java +++ b/src/main/java/de/pixart/messenger/persistance/OnPhoneContactsMerged.java @@ -1,5 +1,5 @@ package de.pixart.messenger.persistance; public interface OnPhoneContactsMerged { - public void phoneContactsMerged(); + public void phoneContactsMerged(); } |