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.MmsConfig;
33import com.android.mms.ui.MessageUtils;
34import com.android.mms.ui.MessagingPreferenceActivity;
35import com.android.mms.util.DownloadManager;
36import com.android.mms.util.Recycler;
37import com.android.mms.widget.MmsWidgetProvider;
38import com.google.android.mms.MmsException;
39import com.google.android.mms.pdu.AcknowledgeInd;
40import com.google.android.mms.pdu.EncodedStringValue;
41import com.google.android.mms.pdu.PduComposer;
42import com.google.android.mms.pdu.PduHeaders;
43import com.google.android.mms.pdu.PduParser;
44import com.google.android.mms.pdu.PduPersister;
45import 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 */
60public 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