| /* |
| * Copyright (C) 2014 Samsung System LSI |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.bluetooth.map; |
| |
| import java.io.Closeable; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.StringWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Calendar; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.obex.ResponseCodes; |
| |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import android.Manifest; |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentProviderClient; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.IntentFilter.MalformedMimeTypeException; |
| import android.content.pm.PackageManager; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Binder; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.ParcelFileDescriptor; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.provider.BaseColumns; |
| import com.android.bluetooth.mapapi.BluetoothMapContract; |
| import com.android.bluetooth.mapapi.BluetoothMapContract.MessageColumns; |
| import android.provider.Telephony; |
| import android.provider.Telephony.Mms; |
| import android.provider.Telephony.MmsSms; |
| import android.provider.Telephony.Sms; |
| import android.provider.Telephony.Sms.Inbox; |
| import android.telephony.PhoneStateListener; |
| import android.telephony.ServiceState; |
| import android.telephony.SmsManager; |
| import android.telephony.SmsMessage; |
| import android.telephony.TelephonyManager; |
| import android.text.format.DateUtils; |
| import android.util.Log; |
| import android.util.Xml; |
| import android.os.Looper; |
| |
| import com.android.bluetooth.map.BluetoothMapUtils.TYPE; |
| import com.android.bluetooth.map.BluetoothMapbMessageMms.MimePart; |
| import com.google.android.mms.pdu.PduHeaders; |
| |
| public class BluetoothMapContentObserver { |
| private static final String TAG = "BluetoothMapContentObserver"; |
| |
| private static final boolean D = BluetoothMapService.DEBUG; |
| private static final boolean V = BluetoothMapService.VERBOSE; |
| |
| private static final String EVENT_TYPE_DELETE = "MessageDeleted"; |
| private static final String EVENT_TYPE_SHIFT = "MessageShift"; |
| private static final String EVENT_TYPE_NEW = "NewMessage"; |
| private static final String EVENT_TYPE_DELEVERY_SUCCESS = "DeliverySuccess"; |
| private static final String EVENT_TYPE_SENDING_SUCCESS = "SendingSuccess"; |
| private static final String EVENT_TYPE_SENDING_FAILURE = "SendingFailure"; |
| private static final String EVENT_TYPE_DELIVERY_FAILURE = "DeliveryFailure"; |
| |
| |
| private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS; |
| |
| private Context mContext; |
| private ContentResolver mResolver; |
| private ContentProviderClient mProviderClient = null; |
| private BluetoothMnsObexClient mMnsClient; |
| private BluetoothMapMasInstance mMasInstance = null; |
| private int mMasId; |
| private boolean mEnableSmsMms = false; |
| private boolean mObserverRegistered = false; |
| private BluetoothMapEmailSettingsItem mAccount; |
| private String mAuthority = null; |
| |
| private BluetoothMapFolderElement mFolders = |
| new BluetoothMapFolderElement("DUMMY", null); // Will be set by the MAS when generated. |
| private Uri mMessageUri = null; |
| |
| public static final int DELETED_THREAD_ID = -1; |
| |
| // X-Mms-Message-Type field types. These are from PduHeaders.java |
| public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84; |
| |
| // Text only MMS converted to SMS if sms parts less than or equal to defined count |
| private static final int CONVERT_MMS_TO_SMS_PART_COUNT = 10; |
| |
| private TYPE mSmsType; |
| |
| private static void close(Closeable c) { |
| try { |
| if (c != null) c.close(); |
| } catch (IOException e) { |
| } |
| } |
| |
| static final String[] SMS_PROJECTION = new String[] { |
| Sms._ID, |
| Sms.THREAD_ID, |
| Sms.ADDRESS, |
| Sms.BODY, |
| Sms.DATE, |
| Sms.READ, |
| Sms.TYPE, |
| Sms.STATUS, |
| Sms.LOCKED, |
| Sms.ERROR_CODE |
| }; |
| |
| static final String[] SMS_PROJECTION_SHORT = new String[] { |
| Sms._ID, |
| Sms.THREAD_ID, |
| Sms.TYPE |
| }; |
| |
| static final String[] MMS_PROJECTION_SHORT = new String[] { |
| Mms._ID, |
| Mms.THREAD_ID, |
| Mms.MESSAGE_TYPE, |
| Mms.MESSAGE_BOX |
| }; |
| |
| static final String[] EMAIL_PROJECTION_SHORT = new String[] { |
| BluetoothMapContract.MessageColumns._ID, |
| BluetoothMapContract.MessageColumns.FOLDER_ID, |
| BluetoothMapContract.MessageColumns.FLAG_READ |
| }; |
| |
| |
| public BluetoothMapContentObserver(final Context context, |
| BluetoothMnsObexClient mnsClient, |
| BluetoothMapMasInstance masInstance, |
| BluetoothMapEmailSettingsItem account, |
| boolean enableSmsMms) throws RemoteException { |
| mContext = context; |
| mResolver = mContext.getContentResolver(); |
| mAccount = account; |
| mMasInstance = masInstance; |
| mMasId = mMasInstance.getMasId(); |
| if(account != null) { |
| mAuthority = Uri.parse(account.mBase_uri).getAuthority(); |
| mMessageUri = Uri.parse(account.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE); |
| mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); |
| if (mProviderClient == null) { |
| throw new RemoteException("Failed to acquire provider for " + mAuthority); |
| } |
| mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); |
| } |
| |
| mEnableSmsMms = enableSmsMms; |
| mSmsType = getSmsType(); |
| mMnsClient = mnsClient; |
| } |
| |
| /** |
| * Set the folder structure to be used for this instance. |
| * @param folderStructure |
| */ |
| public void setFolderStructure(BluetoothMapFolderElement folderStructure) { |
| this.mFolders = folderStructure; |
| } |
| |
| private TYPE getSmsType() { |
| TYPE smsType = null; |
| TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); |
| |
| if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) { |
| smsType = TYPE.SMS_GSM; |
| } else if (tm.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { |
| smsType = TYPE.SMS_CDMA; |
| } |
| |
| return smsType; |
| } |
| |
| private final ContentObserver mObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { |
| @Override |
| public void onChange(boolean selfChange) { |
| onChange(selfChange, null); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange, Uri uri) { |
| if (V) Log.d(TAG, "onChange on thread: " + Thread.currentThread().getId() |
| + " Uri: " + uri.toString() + " selfchange: " + selfChange); |
| |
| handleMsgListChanges(uri); |
| } |
| }; |
| |
| private static final String folderSms[] = { |
| "", |
| BluetoothMapContract.FOLDER_NAME_INBOX, |
| BluetoothMapContract.FOLDER_NAME_SENT, |
| BluetoothMapContract.FOLDER_NAME_DRAFT, |
| BluetoothMapContract.FOLDER_NAME_OUTBOX, |
| BluetoothMapContract.FOLDER_NAME_OUTBOX, |
| BluetoothMapContract.FOLDER_NAME_OUTBOX, |
| BluetoothMapContract.FOLDER_NAME_INBOX, |
| BluetoothMapContract.FOLDER_NAME_INBOX, |
| }; |
| |
| private static final String folderMms[] = { |
| "", |
| BluetoothMapContract.FOLDER_NAME_INBOX, |
| BluetoothMapContract.FOLDER_NAME_SENT, |
| BluetoothMapContract.FOLDER_NAME_DRAFT, |
| BluetoothMapContract.FOLDER_NAME_OUTBOX, |
| }; |
| |
| private class Event { |
| String eventType; |
| long handle; |
| String folder; |
| String oldFolder; |
| TYPE msgType; |
| |
| final static String PATH = "telecom/msg/"; |
| |
| public Event(String eventType, long handle, String folder, |
| String oldFolder, TYPE msgType) { |
| |
| this.eventType = eventType; |
| this.handle = handle; |
| if (folder != null) { |
| if(msgType == TYPE.EMAIL) { |
| this.folder = folder; |
| } else { |
| this.folder = PATH + folder; |
| } |
| } else { |
| this.folder = null; |
| } |
| if (oldFolder != null) { |
| if(msgType == TYPE.EMAIL) { |
| this.oldFolder = oldFolder; |
| } else { |
| this.oldFolder = PATH + oldFolder; |
| } |
| } else { |
| this.oldFolder = null; |
| } |
| this.msgType = msgType; |
| } |
| |
| public byte[] encode() throws UnsupportedEncodingException { |
| StringWriter sw = new StringWriter(); |
| XmlSerializer xmlEvtReport = Xml.newSerializer(); |
| try { |
| xmlEvtReport.setOutput(sw); |
| xmlEvtReport.startDocument(null, null); |
| xmlEvtReport.text("\r\n"); |
| xmlEvtReport.startTag("", "MAP-event-report"); |
| xmlEvtReport.attribute("", "version", "1.0"); |
| |
| xmlEvtReport.startTag("", "event"); |
| xmlEvtReport.attribute("", "type", eventType); |
| xmlEvtReport.attribute("", "handle", BluetoothMapUtils.getMapHandle(handle, msgType)); |
| if (folder != null) { |
| xmlEvtReport.attribute("", "folder", folder); |
| } |
| if (oldFolder != null) { |
| xmlEvtReport.attribute("", "old_folder", oldFolder); |
| } |
| xmlEvtReport.attribute("", "msg_type", msgType.name()); |
| xmlEvtReport.endTag("", "event"); |
| |
| xmlEvtReport.endTag("", "MAP-event-report"); |
| xmlEvtReport.endDocument(); |
| } catch (IllegalArgumentException e) { |
| if(D) Log.w(TAG,e); |
| } catch (IllegalStateException e) { |
| if(D) Log.w(TAG,e); |
| } catch (IOException e) { |
| if(D) Log.w(TAG,e); |
| } |
| |
| if (V) Log.d(TAG, sw.toString()); |
| |
| return sw.toString().getBytes("UTF-8"); |
| } |
| } |
| |
| private class Msg { |
| long id; |
| int type; // Used as folder for SMS/MMS |
| int threadId; // Used for SMS/MMS at delete |
| long folderId = -1; // Email folder ID |
| long oldFolderId = -1; // Used for email undelete |
| boolean localInitiatedSend = false; // Used for MMS to filter out events |
| boolean transparent = false; // Used for EMAIL to delete message sent with transparency |
| |
| public Msg(long id, int type, int threadId) { |
| this.id = id; |
| this.type = type; |
| this.threadId = threadId; |
| } |
| public Msg(long id, long folderId) { |
| this.id = id; |
| this.folderId = folderId; |
| } |
| |
| /* Eclipse generated hashCode() and equals() to make |
| * hashMap lookup work independent of whether the obj |
| * is used for email or SMS/MMS and whether or not the |
| * oldFolder is set. */ |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + (int) (id ^ (id >>> 32)); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) |
| return true; |
| if (obj == null) |
| return false; |
| if (getClass() != obj.getClass()) |
| return false; |
| Msg other = (Msg) obj; |
| if (id != other.id) |
| return false; |
| return true; |
| } |
| } |
| |
| private Map<Long, Msg> mMsgListSms = new HashMap<Long, Msg>(); |
| |
| private Map<Long, Msg> mMsgListMms = new HashMap<Long, Msg>(); |
| |
| private Map<Long, Msg> mMsgListEmail = new HashMap<Long, Msg>(); |
| |
| public int setNotificationRegistration(int notificationStatus) throws RemoteException { |
| // Forward the request to the MNS thread as a message - including the MAS instance ID. |
| if(D) Log.d(TAG,"setNotificationRegistration() enter"); |
| Handler mns = mMnsClient.getMessageHandler(); |
| if(mns != null) { |
| Message msg = mns.obtainMessage(); |
| msg.what = BluetoothMnsObexClient.MSG_MNS_NOTIFICATION_REGISTRATION; |
| msg.arg1 = mMasId; |
| msg.arg2 = notificationStatus; |
| mns.sendMessageDelayed(msg, 10); // Send message without forcing a context switch |
| /* Some devices - e.g. PTS needs to get the unregister confirm before we actually |
| * disconnect the MNS. */ |
| if(D) Log.d(TAG,"setNotificationRegistration() MSG_MNS_NOTIFICATION_REGISTRATION send to MNS"); |
| } else { |
| // This should not happen except at shutdown. |
| if(D) Log.d(TAG,"setNotificationRegistration() Unable to send registration request"); |
| return ResponseCodes.OBEX_HTTP_UNAVAILABLE; |
| } |
| if(notificationStatus == BluetoothMapAppParams.NOTIFICATION_STATUS_YES) { |
| registerObserver(); |
| } else { |
| unregisterObserver(); |
| } |
| return ResponseCodes.OBEX_HTTP_OK; |
| } |
| |
| public void registerObserver() throws RemoteException{ |
| if (V) Log.d(TAG, "registerObserver"); |
| |
| if (mObserverRegistered) |
| return; |
| |
| /* Use MmsSms Uri since the Sms Uri is not notified on deletes */ |
| if(mEnableSmsMms){ |
| //this is sms/mms |
| mResolver.registerContentObserver(MmsSms.CONTENT_URI, false, mObserver); |
| mObserverRegistered = true; |
| } |
| if(mAccount != null) { |
| |
| mProviderClient = mResolver.acquireUnstableContentProviderClient(mAuthority); |
| if (mProviderClient == null) { |
| throw new RemoteException("Failed to acquire provider for " + mAuthority); |
| } |
| mProviderClient.setDetectNotResponding(PROVIDER_ANR_TIMEOUT); |
| |
| /* For URI's without account ID */ |
| Uri uri = Uri.parse(mAccount.mBase_uri_no_account + "/" + BluetoothMapContract.TABLE_MESSAGE); |
| if(D) Log.d(TAG, "Registering observer for: " + uri); |
| mResolver.registerContentObserver(uri, true, mObserver); |
| |
| /* For URI's with account ID - is handled the same way as without ID, but is |
| * only triggered for MAS instances with matching account ID. */ |
| uri = Uri.parse(mAccount.mBase_uri + "/" + BluetoothMapContract.TABLE_MESSAGE); |
| if(D) Log.d(TAG, "Registering observer for: " + uri); |
| mResolver.registerContentObserver(uri, true, mObserver); |
| mObserverRegistered = true; |
| } |
| initMsgList(); |
| } |
| |
| public void unregisterObserver() { |
| if (V) Log.d(TAG, "unregisterObserver"); |
| mResolver.unregisterContentObserver(mObserver); |
| mObserverRegistered = false; |
| if(mProviderClient != null){ |
| mProviderClient.release(); |
| mProviderClient = null; |
| } |
| } |
| |
| private void sendEvent(Event evt) { |
| Log.d(TAG, "sendEvent: " + evt.eventType + " " + evt.handle + " " |
| + evt.folder + " " + evt.oldFolder + " " + evt.msgType.name()); |
| |
| if (mMnsClient == null || mMnsClient.isConnected() == false) { |
| Log.d(TAG, "sendEvent: No MNS client registered or connected- don't send event"); |
| return; |
| } |
| |
| try { |
| mMnsClient.sendEvent(evt.encode(), mMasId); |
| } catch (UnsupportedEncodingException ex) { |
| /* do nothing */ |
| } |
| } |
| |
| private void initMsgList() throws RemoteException { |
| if (V) Log.d(TAG, "initMsgList"); |
| |
| if(mEnableSmsMms) { |
| |
| HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); |
| |
| Cursor c = mResolver.query(Sms.CONTENT_URI, |
| SMS_PROJECTION_SHORT, null, null, null); |
| |
| try { |
| while (c != null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(Sms._ID)); |
| int type = c.getInt(c.getColumnIndex(Sms.TYPE)); |
| int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); |
| |
| Msg msg = new Msg(id, type, threadId); |
| msgListSms.put(id, msg); |
| } |
| } finally { |
| close(c); |
| } |
| |
| synchronized(mMsgListSms) { |
| mMsgListSms.clear(); |
| mMsgListSms = msgListSms; |
| } |
| |
| HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); |
| |
| c = mResolver.query(Mms.CONTENT_URI, MMS_PROJECTION_SHORT, null, null, null); |
| |
| try { |
| while (c != null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(Mms._ID)); |
| int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); |
| int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); |
| |
| Msg msg = new Msg(id, type, threadId); |
| msgListMms.put(id, msg); |
| } |
| } finally { |
| close(c); |
| } |
| |
| synchronized(mMsgListMms) { |
| mMsgListMms.clear(); |
| mMsgListMms = msgListMms; |
| } |
| } |
| |
| if(mAccount != null) { |
| HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>(); |
| Uri uri = mMessageUri; |
| Cursor c = mProviderClient.query(uri, EMAIL_PROJECTION_SHORT, null, null, null); |
| |
| try { |
| while (c != null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(MessageColumns._ID)); |
| long folderId = c.getInt(c.getColumnIndex(BluetoothMapContract.MessageColumns.FOLDER_ID)); |
| |
| Msg msg = new Msg(id, folderId); |
| msgListEmail.put(id, msg); |
| } |
| } finally { |
| close(c); |
| } |
| |
| synchronized(mMsgListEmail) { |
| mMsgListEmail.clear(); |
| mMsgListEmail = msgListEmail; |
| } |
| } |
| } |
| |
| private void handleMsgListChangesSms() { |
| if (V) Log.d(TAG, "handleMsgListChangesSms"); |
| |
| HashMap<Long, Msg> msgListSms = new HashMap<Long, Msg>(); |
| |
| Cursor c = mResolver.query(Sms.CONTENT_URI, |
| SMS_PROJECTION_SHORT, null, null, null); |
| |
| synchronized(mMsgListSms) { |
| try { |
| while (c != null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(Sms._ID)); |
| int type = c.getInt(c.getColumnIndex(Sms.TYPE)); |
| int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); |
| |
| Msg msg = mMsgListSms.remove(id); |
| |
| /* We must filter out any actions made by the MCE, hence do not send e.g. a message |
| * deleted and/or MessageShift for messages deleted by the MCE. */ |
| |
| if (msg == null) { |
| /* New message */ |
| msg = new Msg(id, type, threadId); |
| msgListSms.put(id, msg); |
| |
| /* Incoming message from the network */ |
| Event evt = new Event(EVENT_TYPE_NEW, id, folderSms[type], |
| null, mSmsType); |
| sendEvent(evt); |
| } else { |
| /* Existing message */ |
| if (type != msg.type) { |
| Log.d(TAG, "new type: " + type + " old type: " + msg.type); |
| String oldFolder = folderSms[msg.type]; |
| String newFolder = folderSms[type]; |
| // Filter out the intermediate outbox steps |
| if(!oldFolder.equals(newFolder)) { |
| Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[type], |
| oldFolder, mSmsType); |
| sendEvent(evt); |
| } |
| msg.type = type; |
| } else if(threadId != msg.threadId) { |
| Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type |
| + "\n threadId: " + threadId + " old threadId: " + msg.threadId); |
| if(threadId == DELETED_THREAD_ID) { // Message deleted |
| Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED, |
| folderSms[msg.type], mSmsType); |
| sendEvent(evt); |
| msg.threadId = threadId; |
| } else { // Undelete |
| Event evt = new Event(EVENT_TYPE_SHIFT, id, folderSms[msg.type], |
| BluetoothMapContract.FOLDER_NAME_DELETED, mSmsType); |
| sendEvent(evt); |
| msg.threadId = threadId; |
| } |
| } |
| msgListSms.put(id, msg); |
| } |
| } |
| } finally { |
| close(c); |
| } |
| |
| for (Msg msg : mMsgListSms.values()) { |
| Event evt = new Event(EVENT_TYPE_DELETE, msg.id, |
| BluetoothMapContract.FOLDER_NAME_DELETED, |
| folderSms[msg.type], mSmsType); |
| sendEvent(evt); |
| } |
| |
| mMsgListSms = msgListSms; |
| } |
| } |
| |
| private void handleMsgListChangesMms() { |
| if (V) Log.d(TAG, "handleMsgListChangesMms"); |
| |
| HashMap<Long, Msg> msgListMms = new HashMap<Long, Msg>(); |
| |
| Cursor c = mResolver.query(Mms.CONTENT_URI, |
| MMS_PROJECTION_SHORT, null, null, null); |
| |
| synchronized(mMsgListMms) { |
| try { |
| while (c != null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(Mms._ID)); |
| int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); |
| int mtype = c.getInt(c.getColumnIndex(Mms.MESSAGE_TYPE)); |
| int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); |
| |
| Msg msg = mMsgListMms.remove(id); |
| |
| /* We must filter out any actions made by the MCE, hence do not send e.g. a message |
| * deleted and/or MessageShift for messages deleted by the MCE. */ |
| |
| if (msg == null) { |
| /* New message - only notify on retrieve conf */ |
| if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_INBOX) && |
| mtype != MESSAGE_TYPE_RETRIEVE_CONF) { |
| continue; |
| } |
| |
| msg = new Msg(id, type, threadId); |
| msgListMms.put(id, msg); |
| |
| /* Incoming message from the network */ |
| Event evt = new Event(EVENT_TYPE_NEW, id, folderMms[type], |
| null, TYPE.MMS); |
| sendEvent(evt); |
| } else { |
| /* Existing message */ |
| if (type != msg.type) { |
| Log.d(TAG, "new type: " + type + " old type: " + msg.type); |
| Event evt; |
| if(msg.localInitiatedSend == false) { |
| // Only send events about local initiated changes |
| evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[type], |
| folderMms[msg.type], TYPE.MMS); |
| sendEvent(evt); |
| } |
| msg.type = type; |
| |
| if (folderMms[type].equals(BluetoothMapContract.FOLDER_NAME_SENT) |
| && msg.localInitiatedSend == true) { |
| msg.localInitiatedSend = false; // Stop tracking changes for this message |
| evt = new Event(EVENT_TYPE_SENDING_SUCCESS, id, |
| folderSms[type], null, TYPE.MMS); |
| sendEvent(evt); |
| } |
| } else if(threadId != msg.threadId) { |
| Log.d(TAG, "Message delete change: type: " + type + " old type: " + msg.type |
| + "\n threadId: " + threadId + " old threadId: " + msg.threadId); |
| if(threadId == DELETED_THREAD_ID) { // Message deleted |
| Event evt = new Event(EVENT_TYPE_DELETE, id, BluetoothMapContract.FOLDER_NAME_DELETED, |
| folderMms[msg.type], TYPE.MMS); |
| sendEvent(evt); |
| msg.threadId = threadId; |
| } else { // Undelete |
| Event evt = new Event(EVENT_TYPE_SHIFT, id, folderMms[msg.type], |
| BluetoothMapContract.FOLDER_NAME_DELETED, TYPE.MMS); |
| sendEvent(evt); |
| msg.threadId = threadId; |
| } |
| } |
| msgListMms.put(id, msg); |
| } |
| } |
| } finally { |
| close(c); |
| } |
| |
| for (Msg msg : mMsgListMms.values()) { |
| Event evt = new Event(EVENT_TYPE_DELETE, msg.id, |
| BluetoothMapContract.FOLDER_NAME_DELETED, |
| folderMms[msg.type], TYPE.MMS); |
| sendEvent(evt); |
| } |
| mMsgListMms = msgListMms; |
| } |
| } |
| |
| private void handleMsgListChangesEmail(Uri uri) throws RemoteException{ |
| if (V) Log.v(TAG, "handleMsgListChangesEmail uri: " + uri.toString()); |
| |
| // TODO: Change observer to handle accountId and message ID if present |
| |
| HashMap<Long, Msg> msgListEmail = new HashMap<Long, Msg>(); |
| |
| Cursor c = mProviderClient.query(mMessageUri, EMAIL_PROJECTION_SHORT, null, null, null); |
| |
| synchronized(mMsgListEmail) { |
| try { |
| while (c != null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(BluetoothMapContract.MessageColumns._ID)); |
| int folderId = c.getInt(c.getColumnIndex( |
| BluetoothMapContract.MessageColumns.FOLDER_ID)); |
| Msg msg = mMsgListEmail.remove(id); |
| BluetoothMapFolderElement folderElement = mFolders.getEmailFolderById(folderId); |
| String newFolder; |
| if(folderElement != null) { |
| newFolder = folderElement.getFullPath(); |
| } else { |
| newFolder = "unknown"; // This can happen if a new folder is created while connected |
| } |
| |
| /* We must filter out any actions made by the MCE, hence do not send e.g. a message |
| * deleted and/or MessageShift for messages deleted by the MCE. */ |
| |
| if (msg == null) { |
| /* New message */ |
| msg = new Msg(id, folderId); |
| msgListEmail.put(id, msg); |
| Event evt = new Event(EVENT_TYPE_NEW, id, newFolder, |
| null, TYPE.EMAIL); |
| sendEvent(evt); |
| } else { |
| /* Existing message */ |
| if (folderId != msg.folderId) { |
| if (D) Log.d(TAG, "new folderId: " + folderId + " old folderId: " + msg.folderId); |
| BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId); |
| String oldFolder; |
| if(oldFolderElement != null) { |
| oldFolder = oldFolderElement.getFullPath(); |
| } else { |
| // This can happen if a new folder is created while connected |
| oldFolder = "unknown"; |
| } |
| BluetoothMapFolderElement deletedFolder = |
| mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED); |
| BluetoothMapFolderElement sentFolder = |
| mFolders.getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_SENT); |
| /* |
| * If the folder is now 'deleted', send a deleted-event in stead of a shift |
| * or if message is sent initiated by MAP Client, then send sending-success |
| * otherwise send folderShift |
| */ |
| if(deletedFolder != null && deletedFolder.getEmailFolderId() == folderId) { |
| Event evt = new Event(EVENT_TYPE_DELETE, msg.id, newFolder, |
| oldFolder, TYPE.EMAIL); |
| sendEvent(evt); |
| } else if(sentFolder != null |
| && sentFolder.getEmailFolderId() == folderId |
| && msg.localInitiatedSend == true) { |
| if(msg.transparent) { |
| mResolver.delete(ContentUris.withAppendedId(mMessageUri, id), null, null); |
| } else { |
| msg.localInitiatedSend = false; |
| Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, |
| oldFolder, null, TYPE.EMAIL); |
| sendEvent(evt); |
| } |
| } else { |
| Event evt = new Event(EVENT_TYPE_SHIFT, id, newFolder, |
| oldFolder, TYPE.EMAIL); |
| sendEvent(evt); |
| } |
| msg.folderId = folderId; |
| } |
| msgListEmail.put(id, msg); |
| } |
| } |
| } finally { |
| close(c); |
| } |
| |
| // For all messages no longer in the database send a delete notification |
| for (Msg msg : mMsgListEmail.values()) { |
| BluetoothMapFolderElement oldFolderElement = mFolders.getEmailFolderById(msg.folderId); |
| String oldFolder; |
| if(oldFolderElement != null) { |
| oldFolder = oldFolderElement.getFullPath(); |
| } else { |
| oldFolder = "unknown"; |
| } |
| /* Some e-mail clients delete the message after sending, and creates a new message in sent. |
| * We cannot track the message anymore, hence send both a send success and delete message. |
| */ |
| if(msg.localInitiatedSend == true) { |
| msg.localInitiatedSend = false; |
| // If message is send with transparency don't set folder as message is deleted |
| if (msg.transparent) |
| oldFolder = null; |
| Event evt = new Event(EVENT_TYPE_SENDING_SUCCESS, msg.id, oldFolder, null, TYPE.EMAIL); |
| sendEvent(evt); |
| } |
| /* As this message deleted is only send on a real delete - don't set folder. |
| * - only send delete event if message is not sent with transparency |
| */ |
| if (!msg.transparent) { |
| |
| Event evt = new Event(EVENT_TYPE_DELETE, msg.id, null, oldFolder, TYPE.EMAIL); |
| sendEvent(evt); |
| } |
| } |
| mMsgListEmail = msgListEmail; |
| } |
| } |
| |
| private void handleMsgListChanges(Uri uri) { |
| if(uri.getAuthority().equals(mAuthority)) { |
| try { |
| handleMsgListChangesEmail(uri); |
| }catch(RemoteException e){ |
| mMasInstance.restartObexServerSession(); |
| Log.w(TAG, "Problems contacting the ContentProvider in mas Instance "+mMasId+" restaring ObexServerSession"); |
| } |
| |
| } else { |
| handleMsgListChangesSms(); |
| handleMsgListChangesMms(); |
| } |
| } |
| |
| private boolean setEmailMessageStatusDelete(BluetoothMapFolderElement mCurrentFolder, |
| String uriStr, long handle, int status) { |
| boolean res = false; |
| Uri uri = Uri.parse(uriStr + BluetoothMapContract.TABLE_MESSAGE); |
| |
| int updateCount = 0; |
| ContentValues contentValues = new ContentValues(); |
| BluetoothMapFolderElement deleteFolder = mFolders. |
| getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_DELETED); |
| contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); |
| synchronized(mMsgListEmail) { |
| Msg msg = mMsgListEmail.get(handle); |
| if (status == BluetoothMapAppParams.STATUS_VALUE_YES) { |
| /* Set deleted folder id */ |
| long folderId = -1; |
| if(deleteFolder != null) { |
| folderId = deleteFolder.getEmailFolderId(); |
| } |
| contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID,folderId); |
| updateCount = mResolver.update(uri, contentValues, null, null); |
| /* The race between updating the value in our cached values and the database |
| * is handled by the synchronized statement. */ |
| if(updateCount > 0) { |
| res = true; |
| if (msg != null) { |
| msg.oldFolderId = msg.folderId; |
| // Update the folder ID to avoid triggering an event for MCE initiated actions. |
| msg.folderId = folderId; |
| } |
| if(D) Log.d(TAG, "Deleted MSG: " + handle + " from folderId: " + folderId); |
| } else { |
| Log.w(TAG, "Msg: " + handle + " - Set delete status " + status |
| + " failed for folderId " + folderId); |
| } |
| } else if (status == BluetoothMapAppParams.STATUS_VALUE_NO) { |
| /* Undelete message. move to old folder if we know it, |
| * else move to inbox - as dictated by the spec. */ |
| if(msg != null && deleteFolder != null && |
| msg.folderId == deleteFolder.getEmailFolderId()) { |
| /* Only modify messages in the 'Deleted' folder */ |
| long folderId = -1; |
| if (msg != null && msg.oldFolderId != -1) { |
| folderId = msg.oldFolderId; |
| } else { |
| BluetoothMapFolderElement inboxFolder = mCurrentFolder. |
| getEmailFolderByName(BluetoothMapContract.FOLDER_NAME_INBOX); |
| if(inboxFolder != null) { |
| folderId = inboxFolder.getEmailFolderId(); |
| } |
| if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox."); |
| } |
| contentValues.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); |
| updateCount = mResolver.update(uri, contentValues, null, null); |
| if(updateCount > 0) { |
| res = true; |
| // Update the folder ID to avoid triggering an event for MCE initiated actions. |
| msg.folderId = folderId; |
| } else { |
| if(D)Log.d(TAG,"We did not delete the message, hence the old folder is unknown. Moving to inbox."); |
| } |
| } |
| } |
| if(V) { |
| BluetoothMapFolderElement folderElement; |
| String folderName = "unknown"; |
| if (msg != null) { |
| folderElement = mCurrentFolder.getEmailFolderById(msg.folderId); |
| if(folderElement != null) { |
| folderName = folderElement.getName(); |
| } |
| } |
| Log.d(TAG,"setEmailMessageStatusDelete: " + handle + " from " + folderName |
| + " status: " + status); |
| } |
| } |
| if(res == false) { |
| Log.w(TAG, "Set delete status " + status + " failed."); |
| } |
| return res; |
| } |
| |
| private void updateThreadId(Uri uri, String valueString, long threadId) { |
| ContentValues contentValues = new ContentValues(); |
| contentValues.put(valueString, threadId); |
| mResolver.update(uri, contentValues, null, null); |
| } |
| |
| private boolean deleteMessageMms(long handle) { |
| boolean res = false; |
| Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); |
| Cursor c = mResolver.query(uri, null, null, null, null); |
| |
| try { |
| if (c != null && c.moveToFirst()) { |
| /* Move to deleted folder, or delete if already in deleted folder */ |
| int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); |
| if (threadId != DELETED_THREAD_ID) { |
| /* Set deleted thread id */ |
| synchronized(mMsgListMms) { |
| Msg msg = mMsgListMms.get(handle); |
| if(msg != null) { // This will always be the case |
| msg.threadId = DELETED_THREAD_ID; |
| } |
| } |
| updateThreadId(uri, Mms.THREAD_ID, DELETED_THREAD_ID); |
| } else { |
| /* Delete from observer message list to avoid delete notifications */ |
| synchronized(mMsgListMms) { |
| mMsgListMms.remove(handle); |
| } |
| /* Delete message */ |
| mResolver.delete(uri, null, null); |
| } |
| res = true; |
| } |
| } finally { |
| close(c); |
| } |
| |
| return res; |
| } |
| |
| private boolean unDeleteMessageMms(long handle) { |
| boolean res = false; |
| Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); |
| Cursor c = mResolver.query(uri, null, null, null, null); |
| |
| try { |
| if (c != null && c.moveToFirst()) { |
| int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); |
| if (threadId == DELETED_THREAD_ID) { |
| /* Restore thread id from address, or if no thread for address |
| * create new thread by insert and remove of fake message */ |
| String address; |
| long id = c.getLong(c.getColumnIndex(Mms._ID)); |
| int msgBox = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); |
| if (msgBox == Mms.MESSAGE_BOX_INBOX) { |
| address = BluetoothMapContent.getAddressMms(mResolver, id, |
| BluetoothMapContent.MMS_FROM); |
| } else { |
| address = BluetoothMapContent.getAddressMms(mResolver, id, |
| BluetoothMapContent.MMS_TO); |
| } |
| Set<String> recipients = new HashSet<String>(); |
| recipients.addAll(Arrays.asList(address)); |
| Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); |
| synchronized(mMsgListMms) { |
| Msg msg = mMsgListMms.get(handle); |
| if(msg != null) { // This will always be the case |
| msg.threadId = oldThreadId.intValue(); |
| } |
| } |
| updateThreadId(uri, Mms.THREAD_ID, oldThreadId); |
| } else { |
| Log.d(TAG, "Message not in deleted folder: handle " + handle |
| + " threadId " + threadId); |
| } |
| res = true; |
| } |
| } finally { |
| close(c); |
| } |
| |
| return res; |
| } |
| |
| private boolean deleteMessageSms(long handle) { |
| boolean res = false; |
| Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); |
| Cursor c = mResolver.query(uri, null, null, null, null); |
| |
| try { |
| if (c != null && c.moveToFirst()) { |
| /* Move to deleted folder, or delete if already in deleted folder */ |
| int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); |
| if (threadId != DELETED_THREAD_ID) { |
| synchronized(mMsgListSms) { |
| Msg msg = mMsgListSms.get(handle); |
| if(msg != null) { // This will always be the case |
| msg.threadId = DELETED_THREAD_ID; |
| } |
| } |
| /* Set deleted thread id */ |
| updateThreadId(uri, Sms.THREAD_ID, DELETED_THREAD_ID); |
| } else { |
| /* Delete from observer message list to avoid delete notifications */ |
| synchronized(mMsgListSms) { |
| mMsgListSms.remove(handle); |
| } |
| /* Delete message */ |
| mResolver.delete(uri, null, null); |
| } |
| res = true; |
| } |
| } finally { |
| close(c); |
| } |
| |
| return res; |
| } |
| |
| private boolean unDeleteMessageSms(long handle) { |
| boolean res = false; |
| Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, handle); |
| Cursor c = mResolver.query(uri, null, null, null, null); |
| |
| try { |
| if (c != null && c.moveToFirst()) { |
| int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); |
| if (threadId == DELETED_THREAD_ID) { |
| String address = c.getString(c.getColumnIndex(Sms.ADDRESS)); |
| Set<String> recipients = new HashSet<String>(); |
| recipients.addAll(Arrays.asList(address)); |
| Long oldThreadId = Telephony.Threads.getOrCreateThreadId(mContext, recipients); |
| synchronized(mMsgListSms) { |
| Msg msg = mMsgListSms.get(handle); |
| if(msg != null) { // This will always be the case |
| msg.threadId = oldThreadId.intValue(); // The threadId is specified as an int, so it is safe to truncate |
| } |
| } |
| updateThreadId(uri, Sms.THREAD_ID, oldThreadId); |
| } else { |
| Log.d(TAG, "Message not in deleted folder: handle " + handle |
| + " threadId " + threadId); |
| } |
| res = true; |
| } |
| } finally { |
| close(c); |
| } |
| |
| return res; |
| } |
| |
| public boolean setMessageStatusDeleted(long handle, TYPE type, |
| BluetoothMapFolderElement mCurrentFolder, String uriStr, int statusValue) { |
| boolean res = false; |
| if (D) Log.d(TAG, "setMessageStatusDeleted: handle " + handle |
| + " type " + type + " value " + statusValue); |
| |
| if (type == TYPE.EMAIL) { |
| res = setEmailMessageStatusDelete(mCurrentFolder, uriStr, handle, statusValue); |
| } else { |
| if (statusValue == BluetoothMapAppParams.STATUS_VALUE_YES) { |
| if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { |
| res = deleteMessageSms(handle); |
| } else if (type == TYPE.MMS) { |
| res = deleteMessageMms(handle); |
| } |
| } else if (statusValue == BluetoothMapAppParams.STATUS_VALUE_NO) { |
| if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { |
| res = unDeleteMessageSms(handle); |
| } else if (type == TYPE.MMS) { |
| res = unDeleteMessageMms(handle); |
| } |
| } |
| } |
| |
| return res; |
| } |
| |
| /** |
| * |
| * @param handle |
| * @param type |
| * @param uriStr |
| * @param statusValue |
| * @return true at success |
| */ |
| public boolean setMessageStatusRead(long handle, TYPE type, String uriStr, int statusValue) throws RemoteException{ |
| int count = 0; |
| |
| if (D) Log.d(TAG, "setMessageStatusRead: handle " + handle |
| + " type " + type + " value " + statusValue); |
| |
| /* Approved MAP spec errata 3445 states that read status initiated */ |
| /* by the MCE shall change the MSE read status. */ |
| |
| if (type == TYPE.SMS_GSM || type == TYPE.SMS_CDMA) { |
| Uri uri = Sms.Inbox.CONTENT_URI;//ContentUris.withAppendedId(Sms.CONTENT_URI, handle); |
| ContentValues contentValues = new ContentValues(); |
| contentValues.put(Sms.READ, statusValue); |
| contentValues.put(Sms.SEEN, statusValue); |
| String where = Sms._ID+"="+handle; |
| String values = contentValues.toString(); |
| if (D) Log.d(TAG, " -> SMS Uri: " + uri.toString() + " Where " + where + " values " + values); |
| count = mResolver.update(uri, contentValues, where, null); |
| if (D) Log.d(TAG, " -> "+count +" rows updated!"); |
| |
| } else if (type == TYPE.MMS) { |
| Uri uri = ContentUris.withAppendedId(Mms.CONTENT_URI, handle); |
| if (D) Log.d(TAG, " -> MMS Uri: " + uri.toString()); |
| ContentValues contentValues = new ContentValues(); |
| contentValues.put(Mms.READ, statusValue); |
| count = mResolver.update(uri, contentValues, null, null); |
| if (D) Log.d(TAG, " -> "+count +" rows updated!"); |
| |
| } if (type == TYPE.EMAIL) { |
| Uri uri = mMessageUri; |
| ContentValues contentValues = new ContentValues(); |
| contentValues.put(BluetoothMapContract.MessageColumns.FLAG_READ, statusValue); |
| contentValues.put(BluetoothMapContract.MessageColumns._ID, handle); |
| count = mProviderClient.update(uri, contentValues, null, null); |
| } |
| |
| return (count > 0); |
| } |
| |
| private class PushMsgInfo { |
| long id; |
| int transparent; |
| int retry; |
| String phone; |
| Uri uri; |
| long timestamp; |
| int parts; |
| int partsSent; |
| int partsDelivered; |
| boolean resend; |
| boolean sendInProgress; |
| boolean failedSent; // Set to true if a single part sent fail is received. |
| int statusDelivered; // Set to != 0 if a single part deliver fail is received. |
| |
| public PushMsgInfo(long id, int transparent, |
| int retry, String phone, Uri uri) { |
| this.id = id; |
| this.transparent = transparent; |
| this.retry = retry; |
| this.phone = phone; |
| this.uri = uri; |
| this.resend = false; |
| this.sendInProgress = false; |
| this.failedSent = false; |
| this.statusDelivered = 0; /* Assume success */ |
| this.timestamp = 0; |
| }; |
| } |
| |
| private Map<Long, PushMsgInfo> mPushMsgList = |
| Collections.synchronizedMap(new HashMap<Long, PushMsgInfo>()); |
| |
| public long pushMessage(BluetoothMapbMessage msg, BluetoothMapFolderElement folderElement, |
| BluetoothMapAppParams ap, String emailBaseUri) |
| throws IllegalArgumentException, RemoteException, IOException { |
| if (D) Log.d(TAG, "pushMessage"); |
| ArrayList<BluetoothMapbMessage.vCard> recipientList = msg.getRecipients(); |
| int transparent = (ap.getTransparent() == BluetoothMapAppParams.INVALID_VALUE_PARAMETER) ? |
| 0 : ap.getTransparent(); |
| int retry = ap.getRetry(); |
| int charset = ap.getCharset(); |
| long handle = -1; |
| long folderId = -1; |
| |
| if (recipientList == null) { |
| if (D) Log.d(TAG, "empty recipient list"); |
| return -1; |
| } |
| |
| if ( msg.getType().equals(TYPE.EMAIL) ) { |
| /* Write the message to the database */ |
| String msgBody = ((BluetoothMapbMessageEmail) msg).getEmailBody(); |
| if (V) { |
| int length = msgBody.length(); |
| Log.v(TAG, "pushMessage: message string length = " + length); |
| String messages[] = msgBody.split("\r\n"); |
| Log.v(TAG, "pushMessage: messages count=" + messages.length); |
| for(int i = 0; i < messages.length; i++) { |
| Log.v(TAG, "part " + i + ":" + messages[i]); |
| } |
| } |
| FileOutputStream os = null; |
| ParcelFileDescriptor fdOut = null; |
| Uri uriInsert = Uri.parse(emailBaseUri + BluetoothMapContract.TABLE_MESSAGE); |
| if (D) Log.d(TAG, "pushMessage - uriInsert= " + uriInsert.toString() + |
| ", intoFolder id=" + folderElement.getEmailFolderId()); |
| |
| synchronized(mMsgListEmail) { |
| // Now insert the empty message into folder |
| ContentValues values = new ContentValues(); |
| folderId = folderElement.getEmailFolderId(); |
| values.put(BluetoothMapContract.MessageColumns.FOLDER_ID, folderId); |
| Uri uriNew = mProviderClient.insert(uriInsert, values); |
| if (D) Log.d(TAG, "pushMessage - uriNew= " + uriNew.toString()); |
| handle = Long.parseLong(uriNew.getLastPathSegment()); |
| |
| try { |
| fdOut = mProviderClient.openFile(uriNew, "w"); |
| os = new FileOutputStream(fdOut.getFileDescriptor()); |
| // Write Email to DB |
| os.write(msgBody.getBytes(), 0, msgBody.getBytes().length); |
| } catch (FileNotFoundException e) { |
| Log.w(TAG, e); |
| throw(new IOException("Unable to open file stream")); |
| } catch (NullPointerException e) { |
| Log.w(TAG, e); |
| throw(new IllegalArgumentException("Unable to parse message.")); |
| } finally { |
| try { |
| if(os != null) |
| os.close(); |
| } catch (IOException e) {Log.w(TAG, e);} |
| try { |
| if(fdOut != null) |
| fdOut.close(); |
| } catch (IOException e) {Log.w(TAG, e);} |
| } |
| |
| /* Extract the data for the inserted message, and store in local mirror, to |
| * avoid sending a NewMessage Event. */ |
| Msg newMsg = new Msg(handle, folderId); |
| newMsg.transparent = (transparent == 1) ? true : false; |
| if ( folderId == folderElement.getEmailFolderByName( |
| BluetoothMapContract.FOLDER_NAME_OUTBOX).getEmailFolderId() ) { |
| newMsg.localInitiatedSend = true; |
| } |
| mMsgListEmail.put(handle, newMsg); |
| } |
| } else { // type SMS_* of MMS |
| for (BluetoothMapbMessage.vCard recipient : recipientList) { |
| if(recipient.getEnvLevel() == 0) // Only send the message to the top level recipient |
| { |
| /* Only send to first address */ |
| String phone = recipient.getFirstPhoneNumber(); |
| String email = recipient.getFirstEmail(); |
| String folder = folderElement.getName(); |
| boolean read = false; |
| boolean deliveryReport = true; |
| String msgBody = null; |
| |
| /* If MMS contains text only and the size is less than ten SMS's |
| * then convert the MMS to type SMS and then proceed |
| */ |
| if (msg.getType().equals(TYPE.MMS) && |
| (((BluetoothMapbMessageMms) msg).getTextOnly() == true)) { |
| msgBody = ((BluetoothMapbMessageMms) msg).getMessageAsText(); |
| SmsManager smsMng = SmsManager.getDefault(); |
| ArrayList<String> parts = smsMng.divideMessage(msgBody); |
| int smsParts = parts.size(); |
| if (smsParts <= CONVERT_MMS_TO_SMS_PART_COUNT ) { |
| if (D) Log.d(TAG, "pushMessage - converting MMS to SMS, sms parts=" + smsParts ); |
| msg.setType(mSmsType); |
| } else { |
| if (D) Log.d(TAG, "pushMessage - MMS text only but to big to convert to SMS"); |
| msgBody = null; |
| } |
| |
| } |
| |
| if (msg.getType().equals(TYPE.MMS)) { |
| /* Send message if folder is outbox else just store in draft*/ |
| handle = sendMmsMessage(folder, phone, (BluetoothMapbMessageMms)msg); |
| } else if (msg.getType().equals(TYPE.SMS_GSM) || |
| msg.getType().equals(TYPE.SMS_CDMA) ) { |
| /* Add the message to the database */ |
| if(msgBody == null) |
| msgBody = ((BluetoothMapbMessageSms) msg).getSmsBody(); |
| |
| /* We need to lock the SMS list while updating the database, to avoid sending |
| * events on MCE initiated operation. */ |
| Uri contentUri = Uri.parse(Sms.CONTENT_URI+ "/" + folder); |
| Uri uri; |
| synchronized(mMsgListSms) { |
| uri = Sms.addMessageToUri(mResolver, contentUri, phone, msgBody, |
| "", System.currentTimeMillis(), read, deliveryReport); |
| |
| if(V) Log.v(TAG, "Sms.addMessageToUri() returned: " + uri); |
| if (uri == null) { |
| if (D) Log.d(TAG, "pushMessage - failure on add to uri " + contentUri); |
| return -1; |
| } |
| Cursor c = mResolver.query(uri, SMS_PROJECTION_SHORT, null, null, null); |
| try { |
| /* Extract the data for the inserted message, and store in local mirror, to |
| * avoid sending a NewMessage Event. */ |
| if (c != null && c.moveToFirst()) { |
| long id = c.getLong(c.getColumnIndex(Sms._ID)); |
| int type = c.getInt(c.getColumnIndex(Sms.TYPE)); |
| int threadId = c.getInt(c.getColumnIndex(Sms.THREAD_ID)); |
| Msg newMsg = new Msg(id, type, threadId); |
| mMsgListSms.put(id, newMsg); |
| } else { |
| return -1; // This can only happen, if the message is deleted just as it is added |
| } |
| } finally { |
| close(c); |
| } |
| |
| handle = Long.parseLong(uri.getLastPathSegment()); |
| |
| /* Send message if folder is outbox */ |
| if (folder.equals(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { |
| PushMsgInfo msgInfo = new PushMsgInfo(handle, transparent, |
| retry, phone, uri); |
| mPushMsgList.put(handle, msgInfo); |
| sendMessage(msgInfo, msgBody); |
| if(V) Log.v(TAG, "sendMessage returned..."); |
| } |
| /* sendMessage causes the message to be deleted and reinserted, hence we need to lock |
| * the list while this is happening. */ |
| } |
| } else { |
| if (D) Log.d(TAG, "pushMessage - failure on type " ); |
| return -1; |
| } |
| } |
| } |
| } |
| |
| /* If multiple recipients return handle of last */ |
| return handle; |
| } |
| |
| public long sendMmsMessage(String folder, String to_address, BluetoothMapbMessageMms msg) { |
| /* |
| *strategy: |
| *1) parse message into parts |
| *if folder is outbox/drafts: |
| *2) push message to draft |
| *if folder is outbox: |
| *3) move message to outbox (to trigger the mms app to add msg to pending_messages list) |
| *4) send intent to mms app in order to wake it up. |
| *else if folder !outbox: |
| *1) push message to folder |
| * */ |
| if (folder != null && (folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX) |
| || folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_DRAFT))) { |
| long handle = pushMmsToFolder(Mms.MESSAGE_BOX_DRAFTS, to_address, msg); |
| /* if invalid handle (-1) then just return the handle - else continue sending (if folder is outbox) */ |
| if (BluetoothMapAppParams.INVALID_VALUE_PARAMETER != handle && folder.equalsIgnoreCase(BluetoothMapContract.FOLDER_NAME_OUTBOX)) { |
| moveDraftToOutbox(handle); |
| Intent sendIntent = new Intent("android.intent.action.MMS_SEND_OUTBOX_MSG"); |
| if (D) Log.d(TAG, "broadcasting intent: "+sendIntent.toString()); |
| mContext.sendBroadcast(sendIntent); |
| } |
| return handle; |
| } else { |
| /* not allowed to push mms to anything but outbox/draft */ |
| throw new IllegalArgumentException("Cannot push message to other folders than outbox/draft"); |
| } |
| } |
| |
| private void moveDraftToOutbox(long handle) { |
| /*Move message by changing the msg_box value in the content provider database */ |
| if (handle != -1) return; |
| |
| String whereClause = " _id= " + handle; |
| Uri uri = Mms.CONTENT_URI; |
| Cursor queryResult = mResolver.query(uri, null, whereClause, null, null); |
| try { |
| if (queryResult != null && queryResult.moveToFirst()) { |
| ContentValues data = new ContentValues(); |
| /* set folder to be outbox */ |
| data.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX); |
| mResolver.update(uri, data, whereClause, null); |
| if (D) Log.d(TAG, "Moved draft MMS to outbox"); |
| } else { |
| if (D) Log.d(TAG, "Could not move draft to outbox "); |
| } |
| } finally { |
| queryResult.close(); |
| } |
| } |
| |
| private long pushMmsToFolder(int folder, String to_address, BluetoothMapbMessageMms msg) { |
| /** |
| * strategy: |
| * 1) parse msg into parts + header |
| * 2) create thread id (abuse the ease of adding an SMS to get id for thread) |
| * 3) push parts into content://mms/parts/ table |
| * 3) |
| */ |
| |
| ContentValues values = new ContentValues(); |
| values.put(Mms.MESSAGE_BOX, folder); |
| values.put(Mms.READ, 0); |
| values.put(Mms.SEEN, 0); |
| if(msg.getSubject() != null) { |
| values.put(Mms.SUBJECT, msg.getSubject()); |
| } else { |
| values.put(Mms.SUBJECT, ""); |
| } |
| |
| if(msg.getSubject() != null && msg.getSubject().length() > 0) { |
| values.put(Mms.SUBJECT_CHARSET, 106); |
| } |
| values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related"); |
| values.put(Mms.EXPIRY, 604800); |
| values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR); |
| values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ); |
| values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION); |
| values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL); |
| values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO); |
| values.put(Mms.TRANSACTION_ID, "T"+ Long.toHexString(System.currentTimeMillis())); |
| values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO); |
| values.put(Mms.LOCKED, 0); |
| if(msg.getTextOnly() == true) |
| values.put(Mms.TEXT_ONLY, true); |
| values.put(Mms.MESSAGE_SIZE, msg.getSize()); |
| |
| // Get thread id |
| Set<String> recipients = new HashSet<String>(); |
| recipients.addAll(Arrays.asList(to_address)); |
| values.put(Mms.THREAD_ID, Telephony.Threads.getOrCreateThreadId(mContext, recipients)); |
| Uri uri = Mms.CONTENT_URI; |
| |
| synchronized (mMsgListMms) { |
| uri = mResolver.insert(uri, values); |
| |
| if (uri == null) { |
| // unable to insert MMS |
| Log.e(TAG, "Unabled to insert MMS " + values + "Uri: " + uri); |
| return -1; |
| } |
| /* As we already have all the values we need, we could skip the query, but |
| doing the query ensures we get any changes made by the content provider |
| at insert. */ |
| Cursor c = mResolver.query(uri, MMS_PROJECTION_SHORT, null, null, null); |
| try { |
| if (c != null && c.moveToFirst()) { |
| long id = c.getLong(c.getColumnIndex(Mms._ID)); |
| int type = c.getInt(c.getColumnIndex(Mms.MESSAGE_BOX)); |
| int threadId = c.getInt(c.getColumnIndex(Mms.THREAD_ID)); |
| |
| /* We must filter out any actions made by the MCE. Add the new message to |
| * the list of known messages. */ |
| |
| Msg newMsg = new Msg(id, type, threadId); |
| newMsg.localInitiatedSend = true; |
| mMsgListMms.put(id, newMsg); |
| } |
| } finally { |
| close(c); |
| } |
| } // Done adding changes, unlock access to mMsgListMms to allow sending MMS events again |
| |
| long handle = Long.parseLong(uri.getLastPathSegment()); |
| if (V) Log.v(TAG, " NEW URI " + uri.toString()); |
| |
| try { |
| if(msg.getMimeParts() == null) { |
| /* Perhaps this message have been deleted, and no longer have any content, but only headers */ |
| Log.w(TAG, "No MMS parts present..."); |
| } else { |
| if(V) Log.v(TAG, "Adding " + msg.getMimeParts().size() + " parts to the data base."); |
| int count = 0; |
| for(MimePart part : msg.getMimeParts()) { |
| ++count; |
| values.clear(); |
| if(part.mContentType != null && part.mContentType.toUpperCase().contains("TEXT")) { |
| values.put(Mms.Part.CONTENT_TYPE, "text/plain"); |
| values.put(Mms.Part.CHARSET, 106); |
| if(part.mPartName != null) { |
| values.put(Mms.Part.FILENAME, part.mPartName); |
| values.put(Mms.Part.NAME, part.mPartName); |
| } else { |
| values.put(Mms.Part.FILENAME, "text_" + count +".txt"); |
| values.put(Mms.Part.NAME, "text_" + count +".txt"); |
| } |
| // Ensure we have "ci" set |
| if(part.mContentId != null) { |
| values.put(Mms.Part.CONTENT_ID, part.mContentId); |
| } else { |
| if(part.mPartName != null) { |
| values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); |
| } else { |
| values.put(Mms.Part.CONTENT_ID, "<text_" + count + ">"); |
| } |
| } |
| // Ensure we have "cl" set |
| if(part.mContentLocation != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); |
| } else { |
| if(part.mPartName != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".txt"); |
| } else { |
| values.put(Mms.Part.CONTENT_LOCATION, "text_" + count + ".txt"); |
| } |
| } |
| |
| if(part.mContentDisposition != null) { |
| values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); |
| } |
| values.put(Mms.Part.TEXT, part.getDataAsString()); |
| uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); |
| uri = mResolver.insert(uri, values); |
| if(V) Log.v(TAG, "Added TEXT part"); |
| |
| } else if (part.mContentType != null && part.mContentType.toUpperCase().contains("SMIL")){ |
| |
| values.put(Mms.Part.SEQ, -1); |
| values.put(Mms.Part.CONTENT_TYPE, "application/smil"); |
| if(part.mContentId != null) { |
| values.put(Mms.Part.CONTENT_ID, part.mContentId); |
| } else { |
| values.put(Mms.Part.CONTENT_ID, "<smil_" + count + ">"); |
| } |
| if(part.mContentLocation != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); |
| } else { |
| values.put(Mms.Part.CONTENT_LOCATION, "smil_" + count + ".xml"); |
| } |
| |
| if(part.mContentDisposition != null) |
| values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); |
| values.put(Mms.Part.FILENAME, "smil.xml"); |
| values.put(Mms.Part.NAME, "smil.xml"); |
| values.put(Mms.Part.TEXT, new String(part.mData, "UTF-8")); |
| |
| uri = Uri.parse(Mms.CONTENT_URI+ "/" + handle + "/part"); |
| uri = mResolver.insert(uri, values); |
| if (V) Log.v(TAG, "Added SMIL part"); |
| |
| }else /*VIDEO/AUDIO/IMAGE*/ { |
| writeMmsDataPart(handle, part, count); |
| if (V) Log.v(TAG, "Added OTHER part"); |
| } |
| if (uri != null){ |
| if (V) Log.v(TAG, "Added part with content-type: "+ part.mContentType + " to Uri: " + uri.toString()); |
| } |
| } |
| } |
| } catch (UnsupportedEncodingException e) { |
| Log.w(TAG, e); |
| } catch (IOException e) { |
| Log.w(TAG, e); |
| } |
| |
| values.clear(); |
| values.put(Mms.Addr.CONTACT_ID, "null"); |
| values.put(Mms.Addr.ADDRESS, "insert-address-token"); |
| values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_FROM); |
| values.put(Mms.Addr.CHARSET, 106); |
| |
| uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); |
| uri = mResolver.insert(uri, values); |
| if (uri != null && V){ |
| Log.v(TAG, " NEW URI " + uri.toString()); |
| } |
| |
| values.clear(); |
| values.put(Mms.Addr.CONTACT_ID, "null"); |
| values.put(Mms.Addr.ADDRESS, to_address); |
| values.put(Mms.Addr.TYPE, BluetoothMapContent.MMS_TO); |
| values.put(Mms.Addr.CHARSET, 106); |
| |
| uri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/addr"); |
| uri = mResolver.insert(uri, values); |
| if (uri != null && V){ |
| Log.v(TAG, " NEW URI " + uri.toString()); |
| } |
| return handle; |
| } |
| |
| |
| private void writeMmsDataPart(long handle, MimePart part, int count) throws IOException{ |
| ContentValues values = new ContentValues(); |
| values.put(Mms.Part.MSG_ID, handle); |
| if(part.mContentType != null) { |
| values.put(Mms.Part.CONTENT_TYPE, part.mContentType); |
| } else { |
| Log.w(TAG, "MMS has no CONTENT_TYPE for part " + count); |
| } |
| if(part.mContentId != null) { |
| values.put(Mms.Part.CONTENT_ID, part.mContentId); |
| } else { |
| if(part.mPartName != null) { |
| values.put(Mms.Part.CONTENT_ID, "<" + part.mPartName + ">"); |
| } else { |
| values.put(Mms.Part.CONTENT_ID, "<part_" + count + ">"); |
| } |
| } |
| |
| if(part.mContentLocation != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mContentLocation); |
| } else { |
| if(part.mPartName != null) { |
| values.put(Mms.Part.CONTENT_LOCATION, part.mPartName + ".dat"); |
| } else { |
| values.put(Mms.Part.CONTENT_LOCATION, "part_" + count + ".dat"); |
| } |
| } |
| if(part.mContentDisposition != null) |
| values.put(Mms.Part.CONTENT_DISPOSITION, part.mContentDisposition); |
| if(part.mPartName != null) { |
| values.put(Mms.Part.FILENAME, part.mPartName); |
| values.put(Mms.Part.NAME, part.mPartName); |
| } else { |
| /* We must set at least one part identifier */ |
| values.put(Mms.Part.FILENAME, "part_" + count + ".dat"); |
| values.put(Mms.Part.NAME, "part_" + count + ".dat"); |
| } |
| Uri partUri = Uri.parse(Mms.CONTENT_URI + "/" + handle + "/part"); |
| Uri res = mResolver.insert(partUri, values); |
| |
| // Add data to part |
| OutputStream os = mResolver.openOutputStream(res); |
| os.write(part.mData); |
| os.close(); |
| } |
| |
| |
| public void sendMessage(PushMsgInfo msgInfo, String msgBody) { |
| |
| SmsManager smsMng = SmsManager.getDefault(); |
| ArrayList<String> parts = smsMng.divideMessage(msgBody); |
| msgInfo.parts = parts.size(); |
| // We add a time stamp to differentiate delivery reports from each other for resent messages |
| msgInfo.timestamp = Calendar.getInstance().getTime().getTime(); |
| msgInfo.partsDelivered = 0; |
| msgInfo.partsSent = 0; |
| |
| ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(msgInfo.parts); |
| ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(msgInfo.parts); |
| |
| /* We handle the SENT intent in the MAP service, as this object |
| * is destroyed at disconnect, hence if a disconnect occur while sending |
| * a message, there is no intent handler to move the message from outbox |
| * to the correct folder. |
| * The correct solution would be to create a service that will start based on |
| * the intent, if BT is turned off. */ |
| |
| for (int i = 0; i < msgInfo.parts; i++) { |
| Intent intentDelivery, intentSent; |
| |
| intentDelivery = new Intent(ACTION_MESSAGE_DELIVERY, null); |
| /* Add msgId and part number to ensure the intents are different, and we |
| * thereby get an intent for each msg part. |
| * setType is needed to create different intents for each message id/ time stamp, |
| * as the extras are not used when comparing. */ |
| intentDelivery.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); |
| intentDelivery.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); |
| intentDelivery.putExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, msgInfo.timestamp); |
| PendingIntent pendingIntentDelivery = PendingIntent.getBroadcast(mContext, 0, |
| intentDelivery, PendingIntent.FLAG_UPDATE_CURRENT); |
| |
| intentSent = new Intent(ACTION_MESSAGE_SENT, null); |
| /* Add msgId and part number to ensure the intents are different, and we |
| * thereby get an intent for each msg part. |
| * setType is needed to create different intents for each message id/ time stamp, |
| * as the extras are not used when comparing. */ |
| intentSent.setType("message/" + Long.toString(msgInfo.id) + msgInfo.timestamp + i); |
| intentSent.putExtra(EXTRA_MESSAGE_SENT_HANDLE, msgInfo.id); |
| intentSent.putExtra(EXTRA_MESSAGE_SENT_URI, msgInfo.uri.toString()); |
| intentSent.putExtra(EXTRA_MESSAGE_SENT_RETRY, msgInfo.retry); |
| intentSent.putExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, msgInfo.transparent); |
| |
| PendingIntent pendingIntentSent = PendingIntent.getBroadcast(mContext, 0, |
| intentSent, PendingIntent.FLAG_UPDATE_CURRENT); |
| |
| // We use the same pending intent for all parts, but do not set the one shot flag. |
| deliveryIntents.add(pendingIntentDelivery); |
| sentIntents.add(pendingIntentSent); |
| } |
| |
| Log.d(TAG, "sendMessage to " + msgInfo.phone); |
| |
| smsMng.sendMultipartTextMessage(msgInfo.phone, null, parts, sentIntents, |
| deliveryIntents); |
| } |
| |
| private static final String ACTION_MESSAGE_DELIVERY = |
| "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY"; |
| public static final String ACTION_MESSAGE_SENT = |
| "com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_SENT"; |
| |
| public static final String EXTRA_MESSAGE_SENT_HANDLE = "HANDLE"; |
| public static final String EXTRA_MESSAGE_SENT_RESULT = "result"; |
| public static final String EXTRA_MESSAGE_SENT_URI = "uri"; |
| public static final String EXTRA_MESSAGE_SENT_RETRY = "retry"; |
| public static final String EXTRA_MESSAGE_SENT_TRANSPARENT = "transparent"; |
| public static final String EXTRA_MESSAGE_SENT_TIMESTAMP = "timestamp"; |
| |
| private SmsBroadcastReceiver mSmsBroadcastReceiver = new SmsBroadcastReceiver(); |
| |
| private boolean mInitialized = false; |
| |
| private class SmsBroadcastReceiver extends BroadcastReceiver { |
| private final String[] ID_PROJECTION = new String[] { Sms._ID }; |
| private final Uri UPDATE_STATUS_URI = Uri.withAppendedPath(Sms.CONTENT_URI, "/status"); |
| |
| public void register() { |
| Handler handler = new Handler(Looper.getMainLooper()); |
| |
| IntentFilter intentFilter = new IntentFilter(); |
| intentFilter.addAction(ACTION_MESSAGE_DELIVERY); |
| try{ |
| intentFilter.addDataType("message/*"); |
| } catch (MalformedMimeTypeException e) { |
| Log.e(TAG, "Wrong mime type!!!", e); |
| } |
| |
| mContext.registerReceiver(this, intentFilter, null, handler); |
| } |
| |
| public void unregister() { |
| try { |
| mContext.unregisterReceiver(this); |
| } catch (IllegalArgumentException e) { |
| /* do nothing */ |
| } |
| } |
| |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| long handle = intent.getLongExtra(EXTRA_MESSAGE_SENT_HANDLE, -1); |
| PushMsgInfo msgInfo = mPushMsgList.get(handle); |
| |
| Log.d(TAG, "onReceive: action" + action); |
| |
| if (msgInfo == null) { |
| Log.d(TAG, "onReceive: no msgInfo found for handle " + handle); |
| return; |
| } |
| |
| if (action.equals(ACTION_MESSAGE_DELIVERY)) { |
| long timestamp = intent.getLongExtra(EXTRA_MESSAGE_SENT_TIMESTAMP, 0); |
| int status = -1; |
| if(msgInfo.timestamp == timestamp) { |
| msgInfo.partsDelivered++; |
| byte[] pdu = intent.getByteArrayExtra("pdu"); |
| String format = intent.getStringExtra("format"); |
| |
| SmsMessage message = SmsMessage.createFromPdu(pdu, format); |
| if (message == null) { |
| Log.d(TAG, "actionMessageDelivery: Can't get message from pdu"); |
| return; |
| } |
| status = message.getStatus(); |
| if(status != 0/*0 is success*/) { |
| msgInfo.statusDelivered = status; |
| } |
| } |
| if (msgInfo.partsDelivered == msgInfo.parts) { |
| actionMessageDelivery(context, intent, msgInfo); |
| } |
| } else { |
| Log.d(TAG, "onReceive: Unknown action " + action); |
| } |
| } |
| |
| private void actionMessageDelivery(Context context, Intent intent, PushMsgInfo msgInfo) { |
| Uri messageUri = intent.getData(); |
| msgInfo.sendInProgress = false; |
| |
| Cursor cursor = mResolver.query(msgInfo.uri, ID_PROJECTION, null, null, null); |
| |
| try { |
| if (cursor.moveToFirst()) { |
| int messageId = cursor.getInt(0); |
| |
| Uri updateUri = ContentUris.withAppendedId(UPDATE_STATUS_URI, messageId); |
| |
| if(D) Log.d(TAG, "actionMessageDelivery: uri=" + messageUri + ", status=" + msgInfo.statusDelivered); |
| |
| ContentValues contentValues = new ContentValues(2); |
| |
| contentValues.put(Sms.STATUS, msgInfo.statusDelivered); |
| contentValues.put(Inbox.DATE_SENT, System.currentTimeMillis()); |
| mResolver.update(updateUri, contentValues, null, null); |
| } else { |
| Log.d(TAG, "Can't find message for status update: " + messageUri); |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| if (msgInfo.statusDelivered == 0) { |
| Event evt = new Event(EVENT_TYPE_DELEVERY_SUCCESS, msgInfo.id, |
| folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); |
| sendEvent(evt); |
| } else { |
| Event evt = new Event(EVENT_TYPE_SENDING_FAILURE, msgInfo.id, |
| folderSms[Sms.MESSAGE_TYPE_SENT], null, mSmsType); |
| sendEvent(evt); |
| } |
| |
| mPushMsgList.remove(msgInfo.id); |
| } |
| } |
| |
| static public void actionMessageSentDisconnected(Context context, Intent intent, int result) { |
| /* Check permission for message deletion. */ |
| if (context.checkCallingOrSelfPermission(android.Manifest.permission.WRITE_SMS) |
| != PackageManager.PERMISSION_GRANTED) { |
| Log.w(TAG, "actionMessageSentDisconnected: Not allowed to delete SMS/MMS messages"); |
| return; |
| } |
| |
| boolean delete = false; |
| //int retry = intent.getIntExtra(EXTRA_MESSAGE_SENT_RETRY, 0); |
| int transparent = intent.getIntExtra(EXTRA_MESSAGE_SENT_TRANSPARENT, 0); |
| String uriString = intent.getStringExtra(EXTRA_MESSAGE_SENT_URI); |
| if(uriString == null) { |
| // Nothing we can do about it, just bail out |
| return; |
| } |
| Uri uri = Uri.parse(uriString); |
| |
| if (result == Activity.RESULT_OK) { |
| Log.d(TAG, "actionMessageSentDisconnected: result OK"); |
| if (transparent == 0) { |
| if (!Sms.moveMessageToFolder(context, uri, |
| Sms.MESSAGE_TYPE_SENT, 0)) { |
| Log.d(TAG, "Failed to move " + uri + " to SENT"); |
| } |
| } else { |
| delete = true; |
| } |
| } else { |
| /*if (retry == 1) { |
| The retry feature only works while connected, else we fail the send, |
| * and move the message to failed, to let the user/app resend manually later. |
| } else */{ |
| if (transparent == 0) { |
| if (!Sms.moveMessageToFolder(context, uri, |
| Sms.MESSAGE_TYPE_FAILED, 0)) { |
| Log.d(TAG, "Failed to move " + uri + " to FAILED"); |
| } |
| } else { |
| delete = true; |
| } |
| } |
| } |
| |
| if (delete) { |
| /* Delete from DB */ |
| ContentResolver resolver = context.getContentResolver(); |
| if (resolver != null) { |
| resolver.delete(uri, null, null); |
| } else { |
| Log.w(TAG, "Unable to get resolver"); |
| } |
| } |
| } |
| |
| private void registerPhoneServiceStateListener() { |
| TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); |
| tm.listen(mPhoneListener, PhoneStateListener.LISTEN_SERVICE_STATE); |
| } |
| |
| private void unRegisterPhoneServiceStateListener() { |
| TelephonyManager tm = (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE); |
| tm.listen(mPhoneListener, PhoneStateListener.LISTEN_NONE); |
| } |
| |
| private void resendPendingMessages() { |
| /* Send pending messages in outbox */ |
| String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; |
| Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null); |
| |
| try { |
| while (c!= null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(Sms._ID)); |
| String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); |
| PushMsgInfo msgInfo = mPushMsgList.get(id); |
| if (msgInfo == null || msgInfo.resend == false || msgInfo.sendInProgress == true) { |
| continue; |
| } |
| msgInfo.sendInProgress = true; |
| sendMessage(msgInfo, msgBody); |
| } |
| } finally { |
| close(c); |
| } |
| } |
| |
| private void failPendingMessages() { |
| /* Move pending messages from outbox to failed */ |
| String where = "type = " + Sms.MESSAGE_TYPE_OUTBOX; |
| Cursor c = mResolver.query(Sms.CONTENT_URI, SMS_PROJECTION, where, null, null); |
| if (c == null) return; |
| |
| try { |
| while (c!= null && c.moveToNext()) { |
| long id = c.getLong(c.getColumnIndex(Sms._ID)); |
| String msgBody = c.getString(c.getColumnIndex(Sms.BODY)); |
| PushMsgInfo msgInfo = mPushMsgList.get(id); |
| if (msgInfo == null || msgInfo.resend == false) { |
| continue; |
| } |
| Sms.moveMessageToFolder(mContext, msgInfo.uri, |
| Sms.MESSAGE_TYPE_FAILED, 0); |
| } |
| } finally { |
| close(c); |
| } |
| } |
| |
| private void removeDeletedMessages() { |
| /* Remove messages from virtual "deleted" folder (thread_id -1) */ |
| mResolver.delete(Sms.CONTENT_URI, |
| "thread_id = " + DELETED_THREAD_ID, null); |
| } |
| |
| private PhoneStateListener mPhoneListener = new PhoneStateListener() { |
| @Override |
| public void onServiceStateChanged(ServiceState serviceState) { |
| Log.d(TAG, "Phone service state change: " + serviceState.getState()); |
| if (serviceState.getState() == ServiceState.STATE_IN_SERVICE) { |
| resendPendingMessages(); |
| } |
| } |
| }; |
| |
| public void init() { |
| mSmsBroadcastReceiver.register(); |
| registerPhoneServiceStateListener(); |
| mInitialized = true; |
| } |
| |
| public void deinit() { |
| mInitialized = false; |
| unregisterObserver(); |
| mSmsBroadcastReceiver.unregister(); |
| unRegisterPhoneServiceStateListener(); |
| failPendingMessages(); |
| removeDeletedMessages(); |
| } |
| |
| public boolean handleSmsSendIntent(Context context, Intent intent){ |
| if(mInitialized) { |
| mSmsBroadcastReceiver.onReceive(context, intent); |
| return true; |
| } |
| return false; |
| } |
| } |