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