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