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