package org.yaxim.androidclient.data; import java.util.ArrayList; import org.yaxim.androidclient.util.LogConstants; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.provider.BaseColumns; import android.text.TextUtils; import android.os.Handler; import android.util.Log; public class RosterProvider extends ContentProvider { public static final String AUTHORITY = "org.yaxim.androidclient.provider.Roster"; public static final String TABLE_ROSTER = "roster"; public static final String TABLE_GROUPS = "groups"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_ROSTER); public static final Uri GROUPS_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_GROUPS); public static final String QUERY_ALIAS = "main_result"; private static final UriMatcher URI_MATCHER = new UriMatcher( UriMatcher.NO_MATCH); private static final int CONTACTS = 1; private static final int CONTACT_ID = 2; private static final int GROUPS = 3; private static final int GROUP_MEMBERS = 4; static { URI_MATCHER.addURI(AUTHORITY, "roster", CONTACTS); URI_MATCHER.addURI(AUTHORITY, "roster/#", CONTACT_ID); URI_MATCHER.addURI(AUTHORITY, "groups", GROUPS); URI_MATCHER.addURI(AUTHORITY, "groups/*", GROUP_MEMBERS); } private static final String TAG = "yaxim.RosterProvider"; private Runnable mNotifyChange = new Runnable() { public void run() { Log.d(TAG, "notifying change"); getContext().getContentResolver().notifyChange(CONTENT_URI, null); getContext().getContentResolver().notifyChange(GROUPS_URI, null); } }; private Handler mNotifyHandler = new Handler(); private SQLiteOpenHelper mOpenHelper; public RosterProvider() { } @Override public int delete(Uri url, String where, String[] whereArgs) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count; switch (URI_MATCHER.match(url)) { case CONTACTS: count = db.delete(TABLE_ROSTER, where, whereArgs); break; case CONTACT_ID: String segment = url.getPathSegments().get(1); if (TextUtils.isEmpty(where)) { where = "_id=" + segment; } else { where = "_id=" + segment + " AND (" + where + ")"; } count = db.delete(TABLE_ROSTER, where, whereArgs); break; default: throw new IllegalArgumentException("Cannot delete from URL: " + url); } getContext().getContentResolver().notifyChange(GROUPS_URI, null); notifyChange(); return count; } @Override public String getType(Uri url) { int match = URI_MATCHER.match(url); switch (match) { case CONTACTS: return RosterConstants.CONTENT_TYPE; case CONTACT_ID: return RosterConstants.CONTENT_ITEM_TYPE; default: throw new IllegalArgumentException("Unknown URL"); } } @Override public Uri insert(Uri url, ContentValues initialValues) { if (URI_MATCHER.match(url) != CONTACTS) { throw new IllegalArgumentException("Cannot insert into URL: " + url); } ContentValues values = (initialValues != null) ? new ContentValues( initialValues) : new ContentValues(); for (String colName : RosterConstants.getRequiredColumns()) { if (values.containsKey(colName) == false) { throw new IllegalArgumentException("Missing column: " + colName); } } SQLiteDatabase db = mOpenHelper.getWritableDatabase(); long rowId = db.insert(TABLE_ROSTER, RosterConstants.JID, values); if (rowId < 0) { throw new SQLException("Failed to insert row into " + url); } Uri noteUri = ContentUris.withAppendedId(CONTENT_URI, rowId); notifyChange(); return noteUri; } @Override public boolean onCreate() { mOpenHelper = new RosterDatabaseHelper(getContext()); return true; } @Override public Cursor query(Uri url, String[] projectionIn, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder(); int match = URI_MATCHER.match(url); String groupBy = null; switch (match) { case GROUPS: qBuilder.setTables(TABLE_ROSTER + " " + QUERY_ALIAS); groupBy = RosterConstants.GROUP; break; case GROUP_MEMBERS: qBuilder.setTables(TABLE_ROSTER + " " + QUERY_ALIAS); qBuilder.appendWhere(RosterConstants.GROUP + "="); qBuilder.appendWhere(url.getPathSegments().get(1)); break; case CONTACTS: qBuilder.setTables(TABLE_ROSTER + " " + QUERY_ALIAS); break; case CONTACT_ID: qBuilder.setTables(TABLE_ROSTER + " " + QUERY_ALIAS); qBuilder.appendWhere("_id="); qBuilder.appendWhere(url.getPathSegments().get(1)); break; default: throw new IllegalArgumentException("Unknown URL " + url); } String orderBy; if (TextUtils.isEmpty(sortOrder) && match == CONTACTS) { orderBy = RosterConstants.DEFAULT_SORT_ORDER; } else { orderBy = sortOrder; } SQLiteDatabase db = mOpenHelper.getReadableDatabase(); Cursor ret = qBuilder.query(db, projectionIn, selection, selectionArgs, groupBy, null, orderBy); if (ret == null) { infoLog("RosterProvider.query: failed"); } else { ret.setNotificationUri(getContext().getContentResolver(), url); } return ret; } @Override public int update(Uri url, ContentValues values, String where, String[] whereArgs) { int count; long rowId = 0; int match = URI_MATCHER.match(url); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); switch (match) { case CONTACTS: count = db.update(TABLE_ROSTER, values, where, whereArgs); break; case CONTACT_ID: String segment = url.getPathSegments().get(1); rowId = Long.parseLong(segment); count = db.update(TABLE_ROSTER, values, "_id=" + rowId, whereArgs); break; default: throw new UnsupportedOperationException("Cannot update URL: " + url); } notifyChange(); return count; } /* delay change notification, cancel previous attempts. * this implements rate throttling on fast update sequences */ long last_notify = 0; private void notifyChange() { mNotifyHandler.removeCallbacks(mNotifyChange); long ts = System.currentTimeMillis(); if (ts > last_notify + 500) mNotifyChange.run(); else mNotifyHandler.postDelayed(mNotifyChange, 200); last_notify = ts; } private static void infoLog(String data) { if (LogConstants.LOG_INFO) { Log.i(TAG, data); } } private static class RosterDatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "roster.db"; private static final int DATABASE_VERSION = 4; public RosterDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { infoLog("creating new roster table"); db.execSQL("CREATE TABLE " + TABLE_ROSTER + " (" + RosterConstants._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + RosterConstants.JID + " TEXT UNIQUE ON CONFLICT REPLACE, " + RosterConstants.ALIAS + " TEXT, " + RosterConstants.STATUS_MODE + " INTEGER, " + RosterConstants.STATUS_MESSAGE + " TEXT, " + RosterConstants.GROUP + " TEXT);"); db.execSQL("CREATE INDEX idx_roster_group ON " + TABLE_ROSTER + " (" + RosterConstants.GROUP + ")"); db.execSQL("CREATE INDEX idx_roster_alias ON " + TABLE_ROSTER + " (" + RosterConstants.ALIAS + ")"); db.execSQL("CREATE INDEX idx_roster_status ON " + TABLE_ROSTER + " (" + RosterConstants.STATUS_MODE + ")"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { infoLog("onUpgrade: from " + oldVersion + " to " + newVersion); switch (oldVersion) { default: db.execSQL("DROP TABLE IF EXISTS " + TABLE_GROUPS); db.execSQL("DROP TABLE IF EXISTS " + TABLE_ROSTER); onCreate(db); } } } public static final class RosterConstants implements BaseColumns { private RosterConstants() { } public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.yaxim.roster"; public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.yaxim.roster"; public static final String JID = "jid"; public static final String ALIAS = "alias"; public static final String STATUS_MODE = "status_mode"; public static final String STATUS_MESSAGE = "status_message"; public static final String GROUP = "roster_group"; public static final String DEFAULT_SORT_ORDER = STATUS_MODE + " DESC, " + ALIAS + " COLLATE NOCASE"; public static ArrayList<String> getRequiredColumns() { ArrayList<String> tmpList = new ArrayList<String>(); tmpList.add(JID); tmpList.add(ALIAS); tmpList.add(STATUS_MODE); tmpList.add(STATUS_MESSAGE); tmpList.add(GROUP); return tmpList; } } }