1 /* 2 * Copyright (C) 2007-2008 Esmertec AG. 3 * Copyright (C) 2007-2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.mms.transaction; 19 20 import java.io.IOException; 21 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.database.sqlite.SqliteWrapper; 26 import android.net.Uri; 27 import android.provider.Telephony.Mms; 28 import android.provider.Telephony.Mms.Inbox; 29 import android.text.TextUtils; 30 import android.util.Log; 31 32 import com.android.mms.MmsConfig; 33 import com.android.mms.ui.MessageUtils; 34 import com.android.mms.ui.MessagingPreferenceActivity; 35 import com.android.mms.util.DownloadManager; 36 import com.android.mms.util.Recycler; 37 import com.android.mms.widget.MmsWidgetProvider; 38 import com.google.android.mms.MmsException; 39 import com.google.android.mms.pdu.AcknowledgeInd; 40 import com.google.android.mms.pdu.EncodedStringValue; 41 import com.google.android.mms.pdu.PduComposer; 42 import com.google.android.mms.pdu.PduHeaders; 43 import com.google.android.mms.pdu.PduParser; 44 import com.google.android.mms.pdu.PduPersister; 45 import com.google.android.mms.pdu.RetrieveConf; 46 47 /** 48 * The RetrieveTransaction is responsible for retrieving multimedia 49 * messages (M-Retrieve.conf) from the MMSC server. It: 50 * 51 * <ul> 52 * <li>Sends a GET request to the MMSC server. 53 * <li>Retrieves the binary M-Retrieve.conf data and parses it. 54 * <li>Persists the retrieve multimedia message. 55 * <li>Determines whether an acknowledgement is required. 56 * <li>Creates appropriate M-Acknowledge.ind and sends it to MMSC server. 57 * <li>Notifies the TransactionService about succesful completion. 58 * </ul> 59 */ 60 public class RetrieveTransaction extends Transaction implements Runnable { 61 private static final String TAG = "RetrieveTransaction"; 62 private static final boolean DEBUG = false; 63 private static final boolean LOCAL_LOGV = false; 64 65 private final Uri mUri; 66 private final String mContentLocation; 67 private boolean mLocked; 68 69 static final String[] PROJECTION = new String[] { 70 Mms.CONTENT_LOCATION, 71 Mms.LOCKED 72 }; 73 74 // The indexes of the columns which must be consistent with above PROJECTION. 75 static final int COLUMN_CONTENT_LOCATION = 0; 76 static final int COLUMN_LOCKED = 1; 77 78 public RetrieveTransaction(Context context, int serviceId, 79 TransactionSettings connectionSettings, String uri) 80 throws MmsException { 81 super(context, serviceId, connectionSettings); 82 83 if (uri.startsWith("content://")) { 84 mUri = Uri.parse(uri); // The Uri of the M-Notification.ind 85 mId = mContentLocation = getContentLocation(context, mUri); 86 if (LOCAL_LOGV) { 87 Log.v(TAG, "X-Mms-Content-Location: " + mContentLocation); 88 } 89 } else { 90 throw new IllegalArgumentException( 91 "Initializing from X-Mms-Content-Location is abandoned!"); 92 } 93 94 // Attach the transaction to the instance of RetryScheduler. 95 attach(RetryScheduler.getInstance(context)); 96 } 97 98 private String getContentLocation(Context context, Uri uri) 99 throws MmsException { 100 Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), 101 uri, PROJECTION, null, null, null); 102 mLocked = false; 103 104 if (cursor != null) { 105 try { 106 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 107 // Get the locked flag from the M-Notification.ind so it can be transferred 108 // to the real message after the download. 109 mLocked = cursor.getInt(COLUMN_LOCKED) == 1; 110 return cursor.getString(COLUMN_CONTENT_LOCATION); 111 } 112 } finally { 113 cursor.close(); 114 } 115 } 116 117 throw new MmsException("Cannot get X-Mms-Content-Location from: " + uri); 118 } 119 120 /* 121 * (non-Javadoc) 122 * @see com.android.mms.transaction.Transaction#process() 123 */ 124 @Override 125 public void process() { 126 new Thread(this, "RetrieveTransaction").start(); 127 } 128 129 public void run() { 130 try { 131 // Change the downloading state of the M-Notification.ind. 132 DownloadManager.getInstance().markState( 133 mUri, DownloadManager.STATE_DOWNLOADING); 134 135 // Send GET request to MMSC and retrieve the response data. 136 byte[] resp = getPdu(mContentLocation); 137 138 // Parse M-Retrieve.conf 139 RetrieveConf retrieveConf = (RetrieveConf) new PduParser(resp).parse(); 140 if (null == retrieveConf) { 141 throw new MmsException("Invalid M-Retrieve.conf PDU."); 142 } 143 144 Uri msgUri = null; 145 if (isDuplicateMessage(mContext, retrieveConf)) { 146 // Mark this transaction as failed to prevent duplicate 147 // notification to user. 148 mTransactionState.setState(TransactionState.FAILED); 149 mTransactionState.setContentUri(mUri); 150 } else { 151 // Store M-Retrieve.conf into Inbox 152 PduPersister persister = PduPersister.getPduPersister(mContext); 153 msgUri = persister.persist(retrieveConf, Inbox.CONTENT_URI, true, 154 MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext), null); 155 156 // Use local time instead of PDU time 157 ContentValues values = new ContentValues(1); 158 values.put(Mms.DATE, System.currentTimeMillis() / 1000L); 159 SqliteWrapper.update(mContext, mContext.getContentResolver(), 160 msgUri, values, null, null); 161 162 // The M-Retrieve.conf has been successfully downloaded. 163 mTransactionState.setState(TransactionState.SUCCESS); 164 mTransactionState.setContentUri(msgUri); 165 // Remember the location the message was downloaded from. 166 // Since it's not critical, it won't fail the transaction. 167 // Copy over the locked flag from the M-Notification.ind in case 168 // the user locked the message before activating the download. 169 updateContentLocation(mContext, msgUri, mContentLocation, mLocked); 170 } 171 172 // Delete the corresponding M-Notification.ind. 173 SqliteWrapper.delete(mContext, mContext.getContentResolver(), 174 mUri, null, null); 175 176 if (msgUri != null) { 177 // Have to delete messages over limit *after* the delete above. Otherwise, 178 // it would be counted as part of the total. 179 Recycler.getMmsRecycler().deleteOldMessagesInSameThreadAsMessage(mContext, msgUri); 180 MmsWidgetProvider.notifyDatasetChanged(mContext); 181 } 182 183 // Send ACK to the Proxy-Relay to indicate we have fetched the 184 // MM successfully. 185 // Don't mark the transaction as failed if we failed to send it. 186 sendAcknowledgeInd(retrieveConf); 187 } catch (Throwable t) { 188 Log.e(TAG, Log.getStackTraceString(t)); 189 } finally { 190 if (mTransactionState.getState() != TransactionState.SUCCESS) { 191 mTransactionState.setState(TransactionState.FAILED); 192 mTransactionState.setContentUri(mUri); 193 Log.e(TAG, "Retrieval failed."); 194 } 195 notifyObservers(); 196 } 197 } 198 199 private static boolean isDuplicateMessage(Context context, RetrieveConf rc) { 200 byte[] rawMessageId = rc.getMessageId(); 201 if (rawMessageId != null) { 202 String messageId = new String(rawMessageId); 203 String selection = "(" + Mms.MESSAGE_ID + " = ? AND " 204 + Mms.MESSAGE_TYPE + " = ?)"; 205 String[] selectionArgs = new String[] { messageId, 206 String.valueOf(PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) }; 207 208 Cursor cursor = SqliteWrapper.query( 209 context, context.getContentResolver(), 210 Mms.CONTENT_URI, new String[] { Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET }, 211 selection, selectionArgs, null); 212 213 if (cursor != null) { 214 try { 215 if (cursor.getCount() > 0) { 216 // A message with identical message ID and type found. 217 // Do some additional checks to be sure it's a duplicate. 218 return isDuplicateMessageExtra(cursor, rc); 219 } 220 } finally { 221 cursor.close(); 222 } 223 } 224 } 225 return false; 226 } 227 228 private static boolean isDuplicateMessageExtra(Cursor cursor, RetrieveConf rc) { 229 // Compare message subjects, taking encoding into account 230 EncodedStringValue encodedSubjectReceived = null; 231 EncodedStringValue encodedSubjectStored = null; 232 String subjectReceived = null; 233 String subjectStored = null; 234 String subject = null; 235 236 encodedSubjectReceived = rc.getSubject(); 237 if (encodedSubjectReceived != null) { 238 subjectReceived = encodedSubjectReceived.getString(); 239 } 240 241 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 242 int subjectIdx = cursor.getColumnIndex(Mms.SUBJECT); 243 int charsetIdx = cursor.getColumnIndex(Mms.SUBJECT_CHARSET); 244 subject = cursor.getString(subjectIdx); 245 int charset = cursor.getInt(charsetIdx); 246 if (subject != null) { 247 encodedSubjectStored = new EncodedStringValue(charset, PduPersister 248 .getBytes(subject)); 249 } 250 if (encodedSubjectStored == null && encodedSubjectReceived == null) { 251 // Both encoded subjects are null - return true 252 return true; 253 } else if (encodedSubjectStored != null && encodedSubjectReceived != null) { 254 subjectStored = encodedSubjectStored.getString(); 255 if (!TextUtils.isEmpty(subjectStored) && !TextUtils.isEmpty(subjectReceived)) { 256 // Both decoded subjects are non-empty - compare them 257 return subjectStored.equals(subjectReceived); 258 } else if (TextUtils.isEmpty(subjectStored) && TextUtils.isEmpty(subjectReceived)) { 259 // Both decoded subjects are "" - return true 260 return true; 261 } 262 } 263 } 264 265 return false; 266 } 267 268 private void sendAcknowledgeInd(RetrieveConf rc) throws MmsException, IOException { 269 // Send M-Acknowledge.ind to MMSC if required. 270 // If the Transaction-ID isn't set in the M-Retrieve.conf, it means 271 // the MMS proxy-relay doesn't require an ACK. 272 byte[] tranId = rc.getTransactionId(); 273 if (tranId != null) { 274 // Create M-Acknowledge.ind 275 AcknowledgeInd acknowledgeInd = new AcknowledgeInd( 276 PduHeaders.CURRENT_MMS_VERSION, tranId); 277 278 // insert the 'from' address per spec 279 String lineNumber = MessageUtils.getLocalNumber(); 280 acknowledgeInd.setFrom(new EncodedStringValue(lineNumber)); 281 282 // Pack M-Acknowledge.ind and send it 283 if(MmsConfig.getNotifyWapMMSC()) { 284 sendPdu(new PduComposer(mContext, acknowledgeInd).make(), mContentLocation); 285 } else { 286 sendPdu(new PduComposer(mContext, acknowledgeInd).make()); 287 } 288 } 289 } 290 291 private static void updateContentLocation(Context context, Uri uri, 292 String contentLocation, 293 boolean locked) { 294 ContentValues values = new ContentValues(2); 295 values.put(Mms.CONTENT_LOCATION, contentLocation); 296 values.put(Mms.LOCKED, locked); // preserve the state of the M-Notification.ind lock. 297 SqliteWrapper.update(context, context.getContentResolver(), 298 uri, values, null, null); 299 } 300 301 @Override 302 public int getType() { 303 return RETRIEVE_TRANSACTION; 304 } 305 } 306