package cn.edu.tsinghua.hpc.tmms.syncaction; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Timer; import java.util.TimerTask; import org.apache.http.client.ClientProtocolException; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.AbstractCursor; import android.database.Cursor; import android.database.CursorIndexOutOfBoundsException; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.provider.BaseColumns; import android.provider.Telephony.Mms; import android.provider.Telephony.MmsSms; import android.provider.Telephony.MmsSms.PendingMessages; import android.provider.Telephony.Sms; import android.provider.Telephony.Sms.Conversations; import android.util.Log; import cn.edu.tsinghua.hpc.syncbroker.ElementNotFound; import cn.edu.tsinghua.hpc.syncbroker.MessageType; import cn.edu.tsinghua.hpc.syncbroker.SMSRecord; import cn.edu.tsinghua.hpc.syncbroker.SMSType; import cn.edu.tsinghua.hpc.syncbroker.SyncRecord; import cn.edu.tsinghua.hpc.tmms.util.TTelephony.TMms; import cn.edu.tsinghua.hpc.tmms.util.TTelephony.TSms; /** * A mutable cursor implementation backed by an array of {@code Object}s. Use * {@link #newRow()} to add rows. Automatically expands internal capacity as * needed. */ public class TMmsSmsCursor extends AbstractCursor { private String[] columnNames; private static List<Entry<Long, SMSRecord>> data = new ArrayList<Entry<Long, SMSRecord>>();; private static int mPageNo = 1; private static int mCountPerPage = 5; private Context mContext; // faked _ID's offset public static long BIG_OFFSET = 100000L; public static int CACHE_WINDOW_SIZE = 30; /** * Constructs a new cursor with the given initial capacity. * * @param columnNames * names of the columns, the ordering of which determines column * ordering elsewhere in this cursor * @param initialCapacity * in rows */ public TMmsSmsCursor(Context ctx, String[] columnNames) { this.mContext = ctx; this.columnNames = columnNames; } public boolean deleteRow() { try { data.remove(mPos); return true; } catch (Exception ex) { return false; } } public static boolean updateByGuid(int guid, SMSRecord cs) { return false; } public void clearAllSms(ContentResolver resolver) { data.clear(); currentCachedSMS.clear(); resolver.delete(TSms.CONTENT_URI, "sync_state = '" + SyncState.SYNC_STATE_TMP + "'", null); resolver.delete(TMms.CONTENT_URI, "sync_state = '" + SyncState.SYNC_STATE_TMP + "'", null); mPageNo = 1; } public boolean update(SMSRecord cs) { return false; } @Override public boolean requery() { // XXX not supported now // do nothing now // throw new UnsupportedOperationException(); return true; } private SMSRecord getSMSRecordByIndex(int index) { return data.get(index).getValue(); } private List<SyncRecord> currentCachedSMS = new ArrayList<SyncRecord>(); /** * temporily insert the Mms-SMS back to database */ public void cacheSomeMms(List<Long> ids, ContentResolver resolver) { Log.d("Mms", "cacheSomeMms"); Looper.prepare(); while (currentCachedSMS.size() + ids.size() > CACHE_WINDOW_SIZE) { SMSRecord s = (SMSRecord) currentCachedSMS.get(0); if (s.getMtype() == MessageType.SMS) { resolver.delete(TSms.CONTENT_URI, "_ID is " + s._id + " AND sync_state = '" + SyncState.SYNC_STATE_TMP + "'", null); } else { resolver.delete(TMms.CONTENT_URI, "_ID is " + s._id + " AND sync_state = '" + SyncState.SYNC_STATE_TMP + "'", null); } s._id = SyncRecord.ID_IN_MEMORY; currentCachedSMS.remove(0); } // add useful people for (long id : ids) { Log.d("Mms", "cache id " + id); SMSRecord cs = getSMSRecordByIndex((int) (id - BIG_OFFSET)); if (cs._id == SyncRecord.ID_IN_MEMORY) { Uri uri = MmsUtils.insertOneMessageInto(mContext, cs, SyncState.SYNC_STATE_TMP); cs._id = (int) ContentUris.parseId(uri); currentCachedSMS.add(cs); } } } /** * temporily insert the Mms-SMS back to database */ public void cacheSomeRecords(List<SyncRecord> records, ContentResolver resolver) { Log.d("Mms", "cacheSomeRecords " + records.size()); while (currentCachedSMS.size() + records.size() > CACHE_WINDOW_SIZE) { SMSRecord s = (SMSRecord) currentCachedSMS.get(0); if (s.getMtype() == MessageType.SMS) { resolver.delete(TSms.CONTENT_URI, "_ID is " + s._id + " AND sync_state = '" + SyncState.SYNC_STATE_TMP + "'", null); } else { resolver.delete(TMms.CONTENT_URI, "_ID is " + s._id + " AND sync_state = '" + SyncState.SYNC_STATE_TMP + "'", null); } s._id = SyncRecord.ID_IN_MEMORY; currentCachedSMS.remove(0); } // add useful people for (SyncRecord sr : records) { Log.d("Mms", "cache content " + sr.getGuid()); SMSRecord cs = (SMSRecord) sr; if (cs._id == SyncRecord.ID_IN_MEMORY) { Uri uri = MmsUtils.insertOneMessageInto(mContext, cs, SyncState.SYNC_STATE_TMP); cs._id = (int) ContentUris.parseId(uri); currentCachedSMS.add(cs); } } } /** * Gets value at the given column for the current row. */ private Object get(int column) { if (column < 0 || column >= columnNames.length) { throw new CursorIndexOutOfBoundsException("Requested column: " + column + ", # of columns: " + columnNames.length); } if (mPos < 0) { throw new CursorIndexOutOfBoundsException("Before first row."); } if (mPos >= data.size()) { throw new CursorIndexOutOfBoundsException("After last row."); } SMSRecord cs = getSMSRecordByIndex(mPos); if (cs == null) { return null; } String c = this.columnNames[column]; Cursor mmsCursor = null; if (cs.getMtype() == MessageType.MMS) { if (cs._id == SMSRecord.ID_IN_MEMORY) { List<SyncRecord> sr = new ArrayList<SyncRecord>(); sr.add(cs); this.cacheSomeRecords(sr, mContext.getContentResolver()); } else { Uri mmsUri = Uri.withAppendedPath(TMms.CONTENT_URI, String .valueOf(cs._id)); mmsCursor = mContext.getContentResolver().query(mmsUri, MMS_PROJECTION, null, null, null); } } if (BaseColumns._ID.equals(c)) { if (cs._id == SMSRecord.ID_IN_MEMORY) { return mPos + BIG_OFFSET; } else { return cs._id; } } else if ("guid".equals(c)) { return cs.getGuid(); } else if (Conversations.THREAD_ID.equals(c)) { return -1; } else if (MmsSms.TYPE_DISCRIMINATOR_COLUMN.equals(c)) { if (cs.getMtype() == MessageType.SMS) { return "sms"; } else { return "mms"; } } else if (Sms.ADDRESS.equals(c)) { if (cs.getType() == SMSType.RECEIVE) { return cs.getFrom(); } else { return cs.getTo(); } } else if (Sms.BODY.equals(c)) { return cs.getBody(); } else if (Sms.DATE.equals(c)) { return cs.getDate().getTime(); } else if (Sms.READ.equals(c)) { return 1; } else if (Sms.TYPE.equals(c)) { if (cs.getType() == SMSType.RECEIVE) { return 1; } else { return 2; } } else if (Sms.STATUS.equals(c)) { return -1; } else if (Mms.SUBJECT.equals(c)) { return mmsCursor.getString(mmsCursor.getColumnIndex(Mms.SUBJECT)); } else if (Mms.SUBJECT_CHARSET.equals(c)) { return mmsCursor.getInt(mmsCursor .getColumnIndex(Mms.SUBJECT_CHARSET)); } else if (Mms.DATE.equals(c)) { return mmsCursor.getLong(mmsCursor.getColumnIndex(Mms.DATE)); } else if (Mms.READ.equals(c)) { return mmsCursor.getInt(mmsCursor.getColumnIndex(Mms.READ)); } else if (Mms.MESSAGE_TYPE.equals(c)) { return mmsCursor.getInt(mmsCursor.getColumnIndex(Mms.MESSAGE_TYPE)); } else if (Mms.MESSAGE_BOX.equals(c)) { return mmsCursor.getInt(mmsCursor.getColumnIndex(Mms.MESSAGE_BOX)); } else if (Mms.DELIVERY_REPORT.equals(c)) { return mmsCursor.getInt(mmsCursor .getColumnIndex(Mms.DELIVERY_REPORT)); } else if (Mms.READ_REPORT.equals(c)) { return mmsCursor.getInt(mmsCursor.getColumnIndex(Mms.READ_REPORT)); } else if (PendingMessages.ERROR_TYPE.equals(c)) { return mmsCursor.getInt(mmsCursor .getColumnIndex(PendingMessages.ERROR_TYPE)); } return null; } public static final String[] PROJECTION = new String[] { // TODO: should move this symbol into android.provider.Telephony. MmsSms.TYPE_DISCRIMINATOR_COLUMN, BaseColumns._ID, Conversations.THREAD_ID, // For SMS Sms.ADDRESS, Sms.BODY, Sms.DATE, Sms.READ, Sms.TYPE, Sms.STATUS, // For MMS Mms.SUBJECT, Mms.SUBJECT_CHARSET, Mms.DATE, Mms.READ, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.DELIVERY_REPORT, Mms.READ_REPORT, PendingMessages.ERROR_TYPE }; public static final String[] MMS_PROJECTION = new String[] { MmsSms.TYPE_DISCRIMINATOR_COLUMN, BaseColumns._ID, Conversations.THREAD_ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET, Mms.DATE, Mms.READ, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.DELIVERY_REPORT, Mms.READ_REPORT, PendingMessages.ERROR_TYPE }; // AbstractCursor implementation. @Override public int getCount() { return data.size(); } @Override public String[] getColumnNames() { return columnNames; } public void setCoulumnNames(String[] columns) { this.columnNames = columns; } @Override public String getString(int column) { Object value = get(column); if (value == null) return null; return value.toString(); } @Override public short getShort(int column) { Object value = get(column); if (value == null) return 0; if (value instanceof Number) return ((Number) value).shortValue(); return Short.parseShort(value.toString()); } @Override public int getInt(int column) { Object value = get(column); if (value == null) return 0; if (value instanceof Number) return ((Number) value).intValue(); return Integer.parseInt(value.toString()); } @Override public long getLong(int column) { Object value = get(column); if (value == null) return 0; if (value instanceof Number) return ((Number) value).longValue(); return Long.parseLong(value.toString()); } @Override public float getFloat(int column) { Object value = get(column); if (value == null) return 0.0f; if (value instanceof Number) return ((Number) value).floatValue(); return Float.parseFloat(value.toString()); } @Override public double getDouble(int column) { Object value = get(column); if (value == null) return 0.0d; if (value instanceof Number) return ((Number) value).doubleValue(); return Double.parseDouble(value.toString()); } @Override public boolean isNull(int column) { return get(column) == null; } public void retrieveArchivedSMS(String query, Handler handler) { new Timer().schedule(new RetriveAchivedSMSTimerTask(query, handler), 0); } private class RetriveAchivedSMSTimerTask extends TimerTask { int count; String queryString; Handler handler; public RetriveAchivedSMSTimerTask(String query, Handler handler) { count = mCountPerPage; queryString = query; this.handler = handler; } @Override public void run() { try { List<SyncRecord> result = SyncAction.retriveArchievedSMS( mContext, mPageNo, count, queryString); for (SyncRecord s : result) { data.add(new MapEntry(new Long(s.getGuid()), s)); } Log.d("Mms", "data size is " + result.size()); if (result.size() == 0) { } else if (result.size() < count) { mPageNo++; } else { mPageNo++; } if (this.handler != null) { handler.sendEmptyMessage(0); } cacheSomeRecords(result, mContext.getContentResolver()); } catch (ClientProtocolException e) { Log.d("Mms", "ClientProtocolException"); } catch (ElementNotFound e) { Log.d("Mms", "ElementNotFound"); } catch (IOException e) { Log.d("Mms", "IOException"); this.cancel(); } } } /** * this class is copied from java.util.MapEntry, for its visibility is * internal */ class MapEntry<K, V> implements Map.Entry<K, V>, Cloneable { K key; V value; MapEntry(K theKey) { key = theKey; } MapEntry(K theKey, V theValue) { key = theKey; value = theValue; } @Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { return null; } } @Override public boolean equals(Object object) { if (this == object) { return true; } if (object instanceof Map.Entry) { Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object; return (key == null ? entry.getKey() == null : key.equals(entry .getKey())) && (value == null ? entry.getValue() == null : value .equals(entry.getValue())); } return false; } public K getKey() { return key; } public V getValue() { return value; } @Override public int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); } public V setValue(V object) { V result = value; value = object; return result; } @Override public String toString() { return key + "=" + value; } } }