Home | History | Annotate | Download | only in transaction
      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