/* * Kontalk Android client * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org> * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.service.msgcenter; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jivesoftware.smack.roster.rosterstore.RosterStore; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; import android.text.TextUtils; import org.kontalk.util.Preferences; /** * A roster store backed by a SQLite database. * @author Daniele Ricci */ public class SQLiteRosterStore extends SQLiteOpenHelper implements RosterStore { private static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "roster.db"; private static final String TABLE_ROSTER = "roster"; private static final String CREATE_TABLE_ROSTER = "(" + "jid TEXT NOT NULL PRIMARY KEY," + "name TEXT NOT NULL," + "type TEXT NOT NULL," + "status TEXT,"+ "groups TEXT"+ ")"; private static final String SCHEMA_ROSTER = "CREATE TABLE " + TABLE_ROSTER + " " + CREATE_TABLE_ROSTER; private final Context mContext; private SQLiteStatement mInsertStatement; private final Object mInsertLock = new Object(); public SQLiteRosterStore(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(SCHEMA_ROSTER); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // this is the first version } public void onDestroy() { close(); } private SQLiteStatement prepareInsert(SQLiteDatabase db, RosterPacket.Item item) { if (mInsertStatement == null) { mInsertStatement = db.compileStatement("INSERT INTO " + TABLE_ROSTER + " VALUES(?, ?, ?, ?, ?)"); } else { mInsertStatement.clearBindings(); } int i = 0; mInsertStatement.bindString(++i, item.getUser()); mInsertStatement.bindString(++i, item.getName()); mInsertStatement.bindString(++i, item.getItemType() != null ? item.getItemType().toString() : RosterPacket.ItemType.none.toString()); RosterPacket.ItemStatus status = item.getItemStatus(); if (status != null) { mInsertStatement.bindString(++i, status.toString()); } else { mInsertStatement.bindNull(++i); } Set<String> groups = item.getGroupNames(); if (groups != null) { mInsertStatement.bindString(++i, TextUtils.join(",", groups)); } else { mInsertStatement.bindNull(++i); } return mInsertStatement; } @Override public Collection<RosterPacket.Item> getEntries() { SQLiteDatabase db = getReadableDatabase(); Cursor c = null; try { c = db.query(TABLE_ROSTER, null, null, null, null, null, null); if (c != null) { List<RosterPacket.Item> items = new ArrayList<>(c.getCount()); while (c.moveToNext()) { items.add(fromCursor(c)); } return items; } return null; } catch (SQLiteException e) { return null; } finally { if (c != null) { c.close(); } } } private RosterPacket.Item fromCursor(Cursor c) { String user = c.getString(0); String name = c.getString(1); RosterPacket.Item item = new RosterPacket.Item(user, name); String type = c.getString(2); if (type == null) type = RosterPacket.ItemType.none.toString(); item.setItemType(RosterPacket.ItemType.valueOf(type)); String status = c.getString(3); if (status != null) item.setItemStatus(RosterPacket.ItemStatus.fromString(status)); String groups = c.getString(4); if (groups != null) { StringTokenizer tokenizer = new StringTokenizer(groups, ","); while (tokenizer.hasMoreTokens()) { item.addGroupName(tokenizer.nextToken()); } } return item; } @Override public RosterPacket.Item getEntry(String bareJid) { SQLiteDatabase db = getReadableDatabase(); Cursor c = null; try { c = db.query(TABLE_ROSTER, null, "jid = ?", new String[] { bareJid }, null, null, null); if (c != null && c.moveToFirst()) { return fromCursor(c); } } catch (SQLiteException e) { return null; } finally { if (c != null) { c.close(); } } return null; } @Override public String getRosterVersion() { return Preferences.getRosterVersion(); } private boolean addEntry(SQLiteDatabase db, RosterPacket.Item item) { synchronized (mInsertLock) { try { SQLiteStatement stm = prepareInsert(db, item); stm.executeInsert(); } catch (SQLiteException e) { return false; } // insert was successful return true; } } @Override public boolean addEntry(RosterPacket.Item item, String version) { SQLiteDatabase db = getWritableDatabase(); return addEntry(db, item) && setRosterVersion(version); } @Override public boolean resetEntries(Collection<RosterPacket.Item> items, String version) { SQLiteDatabase db = getWritableDatabase(); beginTransaction(db); boolean success = false; try { db.execSQL("DELETE FROM " + TABLE_ROSTER); for (RosterPacket.Item item : items) { addEntry(db, item); } success = setTransactionSuccessful(db); if (success) { setRosterVersion(version); } } catch (SQLiteException e) { return false; } finally { endTransaction(db, success); } return false; } @Override public boolean removeEntry(String bareJid, String version) { SQLiteDatabase db = getWritableDatabase(); try { db.delete(TABLE_ROSTER, "jid = ?", new String[]{bareJid}); return setRosterVersion(version); } catch (SQLiteException e) { return false; } } private boolean setRosterVersion(String version) { return Preferences.setRosterVersion(version); } /* Transactions compatibility layer */ @TargetApi(android.os.Build.VERSION_CODES.HONEYCOMB) private void beginTransaction(SQLiteDatabase db) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) db.beginTransactionNonExclusive(); else // this is because API < 11 doesn't have beginTransactionNonExclusive() db.execSQL("BEGIN IMMEDIATE"); } private boolean setTransactionSuccessful(SQLiteDatabase db) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) db.setTransactionSuccessful(); return true; } private void endTransaction(SQLiteDatabase db, boolean success) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) db.endTransaction(); else db.execSQL(success ? "COMMIT" : "ROLLBACK"); } }