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 java.io.IOException; 21 22import android.content.ContentValues; 23import android.content.Context; 24import android.database.Cursor; 25import android.database.sqlite.SqliteWrapper; 26import android.net.Uri; 27import android.provider.Telephony.Mms; 28import android.provider.Telephony.Mms.Inbox; 29import android.text.TextUtils; 30import android.util.Log; 31 32import com.android.mms.LogTag; 33import com.android.mms.MmsConfig; 34import com.android.mms.ui.MessageUtils; 35import com.android.mms.ui.MessagingPreferenceActivity; 36import com.android.mms.util.DownloadManager; 37import com.android.mms.util.Recycler; 38import com.android.mms.widget.MmsWidgetProvider; 39import com.google.android.mms.MmsException; 40import com.google.android.mms.pdu.AcknowledgeInd; 41import com.google.android.mms.pdu.EncodedStringValue; 42import com.google.android.mms.pdu.PduComposer; 43import com.google.android.mms.pdu.PduHeaders; 44import com.google.android.mms.pdu.PduParser; 45import com.google.android.mms.pdu.PduPersister; 46import com.google.android.mms.pdu.RetrieveConf; 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(resp).parse(); 141 if (null == retrieveConf) { 142 throw new MmsException("Invalid M-Retrieve.conf PDU."); 143 } 144 145 Uri msgUri = null; 146 if (isDuplicateMessage(mContext, retrieveConf)) { 147 // Mark this transaction as failed to prevent duplicate 148 // notification to user. 149 mTransactionState.setState(TransactionState.FAILED); 150 mTransactionState.setContentUri(mUri); 151 } else { 152 // Store M-Retrieve.conf into Inbox 153 PduPersister persister = PduPersister.getPduPersister(mContext); 154 msgUri = persister.persist(retrieveConf, Inbox.CONTENT_URI, true, 155 MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext), null); 156 157 // Use local time instead of PDU time 158 ContentValues values = new ContentValues(1); 159 values.put(Mms.DATE, System.currentTimeMillis() / 1000L); 160 SqliteWrapper.update(mContext, mContext.getContentResolver(), 161 msgUri, values, null, null); 162 163 // The M-Retrieve.conf has been successfully downloaded. 164 mTransactionState.setState(TransactionState.SUCCESS); 165 mTransactionState.setContentUri(msgUri); 166 // Remember the location the message was downloaded from. 167 // Since it's not critical, it won't fail the transaction. 168 // Copy over the locked flag from the M-Notification.ind in case 169 // the user locked the message before activating the download. 170 updateContentLocation(mContext, msgUri, mContentLocation, mLocked); 171 } 172 173 // Delete the corresponding M-Notification.ind. 174 SqliteWrapper.delete(mContext, mContext.getContentResolver(), 175 mUri, null, null); 176 177 if (msgUri != null) { 178 // Have to delete messages over limit *after* the delete above. Otherwise, 179 // it would be counted as part of the total. 180 Recycler.getMmsRecycler().deleteOldMessagesInSameThreadAsMessage(mContext, msgUri); 181 MmsWidgetProvider.notifyDatasetChanged(mContext); 182 } 183 184 // Send ACK to the Proxy-Relay to indicate we have fetched the 185 // MM successfully. 186 // Don't mark the transaction as failed if we failed to send it. 187 sendAcknowledgeInd(retrieveConf); 188 } catch (Throwable t) { 189 Log.e(TAG, Log.getStackTraceString(t)); 190 } finally { 191 if (mTransactionState.getState() != TransactionState.SUCCESS) { 192 mTransactionState.setState(TransactionState.FAILED); 193 mTransactionState.setContentUri(mUri); 194 Log.e(TAG, "Retrieval failed."); 195 } 196 notifyObservers(); 197 } 198 } 199 200 private static boolean isDuplicateMessage(Context context, RetrieveConf rc) { 201 byte[] rawMessageId = rc.getMessageId(); 202 if (rawMessageId != null) { 203 String messageId = new String(rawMessageId); 204 String selection = "(" + Mms.MESSAGE_ID + " = ? AND " 205 + Mms.MESSAGE_TYPE + " = ?)"; 206 String[] selectionArgs = new String[] { messageId, 207 String.valueOf(PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF) }; 208 209 Cursor cursor = SqliteWrapper.query( 210 context, context.getContentResolver(), 211 Mms.CONTENT_URI, new String[] { Mms._ID, Mms.SUBJECT, Mms.SUBJECT_CHARSET }, 212 selection, selectionArgs, null); 213 214 if (cursor != null) { 215 try { 216 if (cursor.getCount() > 0) { 217 // A message with identical message ID and type found. 218 // Do some additional checks to be sure it's a duplicate. 219 return isDuplicateMessageExtra(cursor, rc); 220 } 221 } finally { 222 cursor.close(); 223 } 224 } 225 } 226 return false; 227 } 228 229 private static boolean isDuplicateMessageExtra(Cursor cursor, RetrieveConf rc) { 230 // Compare message subjects, taking encoding into account 231 EncodedStringValue encodedSubjectReceived = null; 232 EncodedStringValue encodedSubjectStored = null; 233 String subjectReceived = null; 234 String subjectStored = null; 235 String subject = null; 236 237 encodedSubjectReceived = rc.getSubject(); 238 if (encodedSubjectReceived != null) { 239 subjectReceived = encodedSubjectReceived.getString(); 240 } 241 242 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { 243 int subjectIdx = cursor.getColumnIndex(Mms.SUBJECT); 244 int charsetIdx = cursor.getColumnIndex(Mms.SUBJECT_CHARSET); 245 subject = cursor.getString(subjectIdx); 246 int charset = cursor.getInt(charsetIdx); 247 if (subject != null) { 248 encodedSubjectStored = new EncodedStringValue(charset, PduPersister 249 .getBytes(subject)); 250 } 251 if (encodedSubjectStored == null && encodedSubjectReceived == null) { 252 // Both encoded subjects are null - return true 253 return true; 254 } else if (encodedSubjectStored != null && encodedSubjectReceived != null) { 255 subjectStored = encodedSubjectStored.getString(); 256 if (!TextUtils.isEmpty(subjectStored) && !TextUtils.isEmpty(subjectReceived)) { 257 // Both decoded subjects are non-empty - compare them 258 return subjectStored.equals(subjectReceived); 259 } else if (TextUtils.isEmpty(subjectStored) && TextUtils.isEmpty(subjectReceived)) { 260 // Both decoded subjects are "" - return true 261 return true; 262 } 263 } 264 } 265 266 return false; 267 } 268 269 private void sendAcknowledgeInd(RetrieveConf rc) throws MmsException, IOException { 270 // Send M-Acknowledge.ind to MMSC if required. 271 // If the Transaction-ID isn't set in the M-Retrieve.conf, it means 272 // the MMS proxy-relay doesn't require an ACK. 273 byte[] tranId = rc.getTransactionId(); 274 if (tranId != null) { 275 // Create M-Acknowledge.ind 276 AcknowledgeInd acknowledgeInd = new AcknowledgeInd( 277 PduHeaders.CURRENT_MMS_VERSION, tranId); 278 279 // insert the 'from' address per spec 280 String lineNumber = MessageUtils.getLocalNumber(); 281 acknowledgeInd.setFrom(new EncodedStringValue(lineNumber)); 282 283 // Pack M-Acknowledge.ind and send it 284 if(MmsConfig.getNotifyWapMMSC()) { 285 sendPdu(new PduComposer(mContext, acknowledgeInd).make(), mContentLocation); 286 } else { 287 sendPdu(new PduComposer(mContext, acknowledgeInd).make()); 288 } 289 } 290 } 291 292 private static void updateContentLocation(Context context, Uri uri, 293 String contentLocation, 294 boolean locked) { 295 ContentValues values = new ContentValues(2); 296 values.put(Mms.CONTENT_LOCATION, contentLocation); 297 values.put(Mms.LOCKED, locked); // preserve the state of the M-Notification.ind lock. 298 SqliteWrapper.update(context, context.getContentResolver(), 299 uri, values, null, null); 300 } 301 302 @Override 303 public int getType() { 304 return RETRIEVE_TRANSACTION; 305 } 306} 307