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