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 static com.android.mms.transaction.TransactionState.FAILED;
21import static com.android.mms.transaction.TransactionState.INITIALIZED;
22import static com.android.mms.transaction.TransactionState.SUCCESS;
23import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF;
24import static com.google.android.mms.pdu.PduHeaders.STATUS_DEFERRED;
25import static com.google.android.mms.pdu.PduHeaders.STATUS_RETRIEVED;
26import static com.google.android.mms.pdu.PduHeaders.STATUS_UNRECOGNIZED;
27
28import java.io.IOException;
29
30import android.content.ContentValues;
31import android.content.Context;
32import android.database.sqlite.SqliteWrapper;
33import android.net.Uri;
34import android.provider.Telephony.Mms;
35import android.provider.Telephony.Threads;
36import android.provider.Telephony.Mms.Inbox;
37import android.telephony.TelephonyManager;
38import android.util.Log;
39
40import com.android.mms.LogTag;
41import com.android.mms.MmsApp;
42import com.android.mms.MmsConfig;
43import com.android.mms.ui.MessagingPreferenceActivity;
44import com.android.mms.util.DownloadManager;
45import com.android.mms.util.Recycler;
46import com.android.mms.widget.MmsWidgetProvider;
47import com.google.android.mms.MmsException;
48import com.google.android.mms.pdu.GenericPdu;
49import com.google.android.mms.pdu.NotificationInd;
50import com.google.android.mms.pdu.NotifyRespInd;
51import com.google.android.mms.pdu.PduComposer;
52import com.google.android.mms.pdu.PduHeaders;
53import com.google.android.mms.pdu.PduParser;
54import com.google.android.mms.pdu.PduPersister;
55
56/**
57 * The NotificationTransaction is responsible for handling multimedia
58 * message notifications (M-Notification.ind).  It:
59 *
60 * <ul>
61 * <li>Composes the notification response (M-NotifyResp.ind).
62 * <li>Sends the notification response to the MMSC server.
63 * <li>Stores the notification indication.
64 * <li>Notifies the TransactionService about succesful completion.
65 * </ul>
66 *
67 * NOTE: This MMS client handles all notifications with a <b>deferred
68 * retrieval</b> response.  The transaction service, upon succesful
69 * completion of this transaction, will trigger a retrieve transaction
70 * in case the client is in immediate retrieve mode.
71 */
72public class NotificationTransaction extends Transaction implements Runnable {
73    private static final String TAG = LogTag.TAG;
74    private static final boolean DEBUG = false;
75    private static final boolean LOCAL_LOGV = false;
76
77    private Uri mUri;
78    private NotificationInd mNotificationInd;
79    private String mContentLocation;
80
81    public NotificationTransaction(
82            Context context, int serviceId,
83            TransactionSettings connectionSettings, String uriString) {
84        super(context, serviceId, connectionSettings);
85
86        mUri = Uri.parse(uriString);
87
88        try {
89            mNotificationInd = (NotificationInd)
90                    PduPersister.getPduPersister(context).load(mUri);
91        } catch (MmsException e) {
92            Log.e(TAG, "Failed to load NotificationInd from: " + uriString, e);
93            throw new IllegalArgumentException();
94        }
95
96        mContentLocation = new String(mNotificationInd.getContentLocation());
97        mId = mContentLocation;
98
99        // Attach the transaction to the instance of RetryScheduler.
100        attach(RetryScheduler.getInstance(context));
101    }
102
103    /**
104     * This constructor is only used for test purposes.
105     */
106    public NotificationTransaction(
107            Context context, int serviceId,
108            TransactionSettings connectionSettings, NotificationInd ind) {
109        super(context, serviceId, connectionSettings);
110
111        try {
112            // Save the pdu. If we can start downloading the real pdu immediately, don't allow
113            // persist() to create a thread for the notificationInd because it causes UI jank.
114            mUri = PduPersister.getPduPersister(context).persist(
115                        ind, Inbox.CONTENT_URI, !allowAutoDownload(),
116                        MessagingPreferenceActivity.getIsGroupMmsEnabled(context), null);
117        } catch (MmsException e) {
118            Log.e(TAG, "Failed to save NotificationInd in constructor.", e);
119            throw new IllegalArgumentException();
120        }
121
122        mNotificationInd = ind;
123        mId = new String(mNotificationInd.getContentLocation());
124    }
125
126    /*
127     * (non-Javadoc)
128     * @see com.google.android.mms.pdu.Transaction#process()
129     */
130    @Override
131    public void process() {
132        new Thread(this, "NotificationTransaction").start();
133    }
134
135    public static boolean allowAutoDownload() {
136        DownloadManager downloadManager = DownloadManager.getInstance();
137        boolean autoDownload = downloadManager.isAuto();
138        boolean dataSuspended = (MmsApp.getApplication().getTelephonyManager().getDataState() ==
139                TelephonyManager.DATA_SUSPENDED);
140        return autoDownload && !dataSuspended;
141    }
142
143    public void run() {
144        DownloadManager downloadManager = DownloadManager.getInstance();
145        boolean autoDownload = allowAutoDownload();
146        try {
147            if (LOCAL_LOGV) {
148                Log.v(TAG, "Notification transaction launched: " + this);
149            }
150
151            // By default, we set status to STATUS_DEFERRED because we
152            // should response MMSC with STATUS_DEFERRED when we cannot
153            // download a MM immediately.
154            int status = STATUS_DEFERRED;
155            // Don't try to download when data is suspended, as it will fail, so defer download
156            if (!autoDownload) {
157                downloadManager.markState(mUri, DownloadManager.STATE_UNSTARTED);
158                sendNotifyRespInd(status);
159                return;
160            }
161
162            downloadManager.markState(mUri, DownloadManager.STATE_DOWNLOADING);
163
164            if (LOCAL_LOGV) {
165                Log.v(TAG, "Content-Location: " + mContentLocation);
166            }
167
168            byte[] retrieveConfData = null;
169            // We should catch exceptions here to response MMSC
170            // with STATUS_DEFERRED.
171            try {
172                retrieveConfData = getPdu(mContentLocation);
173            } catch (IOException e) {
174                mTransactionState.setState(FAILED);
175            }
176
177            if (retrieveConfData != null) {
178                GenericPdu pdu = new PduParser(retrieveConfData).parse();
179                if ((pdu == null) || (pdu.getMessageType() != MESSAGE_TYPE_RETRIEVE_CONF)) {
180                    Log.e(TAG, "Invalid M-RETRIEVE.CONF PDU. " +
181                            (pdu != null ? "message type: " + pdu.getMessageType() : "null pdu"));
182                    mTransactionState.setState(FAILED);
183                    status = STATUS_UNRECOGNIZED;
184                } else {
185                    // Save the received PDU (must be a M-RETRIEVE.CONF).
186                    PduPersister p = PduPersister.getPduPersister(mContext);
187                    Uri uri = p.persist(pdu, Inbox.CONTENT_URI, true,
188                            MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext), null);
189
190                    // Use local time instead of PDU time
191                    ContentValues values = new ContentValues(1);
192                    values.put(Mms.DATE, System.currentTimeMillis() / 1000L);
193                    SqliteWrapper.update(mContext, mContext.getContentResolver(),
194                            uri, values, null, null);
195
196                    // We have successfully downloaded the new MM. Delete the
197                    // M-NotifyResp.ind from Inbox.
198                    SqliteWrapper.delete(mContext, mContext.getContentResolver(),
199                                         mUri, null, null);
200                    Log.v(TAG, "NotificationTransaction received new mms message: " + uri);
201                    // Delete obsolete threads
202                    SqliteWrapper.delete(mContext, mContext.getContentResolver(),
203                            Threads.OBSOLETE_THREADS_URI, null, null);
204
205                    // Notify observers with newly received MM.
206                    mUri = uri;
207                    status = STATUS_RETRIEVED;
208                }
209            }
210
211            if (LOCAL_LOGV) {
212                Log.v(TAG, "status=0x" + Integer.toHexString(status));
213            }
214
215            // Check the status and update the result state of this Transaction.
216            switch (status) {
217                case STATUS_RETRIEVED:
218                    mTransactionState.setState(SUCCESS);
219                    break;
220                case STATUS_DEFERRED:
221                    // STATUS_DEFERRED, may be a failed immediate retrieval.
222                    if (mTransactionState.getState() == INITIALIZED) {
223                        mTransactionState.setState(SUCCESS);
224                    }
225                    break;
226            }
227
228            sendNotifyRespInd(status);
229
230            // Make sure this thread isn't over the limits in message count.
231            Recycler.getMmsRecycler().deleteOldMessagesInSameThreadAsMessage(mContext, mUri);
232            MmsWidgetProvider.notifyDatasetChanged(mContext);
233        } catch (Throwable t) {
234            Log.e(TAG, Log.getStackTraceString(t));
235        } finally {
236            mTransactionState.setContentUri(mUri);
237            if (!autoDownload) {
238                // Always mark the transaction successful for deferred
239                // download since any error here doesn't make sense.
240                mTransactionState.setState(SUCCESS);
241            }
242            if (mTransactionState.getState() != SUCCESS) {
243                mTransactionState.setState(FAILED);
244                Log.e(TAG, "NotificationTransaction failed.");
245            }
246            notifyObservers();
247        }
248    }
249
250    private void sendNotifyRespInd(int status) throws MmsException, IOException {
251        // Create the M-NotifyResp.ind
252        NotifyRespInd notifyRespInd = new NotifyRespInd(
253                PduHeaders.CURRENT_MMS_VERSION,
254                mNotificationInd.getTransactionId(),
255                status);
256
257        // Pack M-NotifyResp.ind and send it
258        if(MmsConfig.getNotifyWapMMSC()) {
259            sendPdu(new PduComposer(mContext, notifyRespInd).make(), mContentLocation);
260        } else {
261            sendPdu(new PduComposer(mContext, notifyRespInd).make());
262        }
263    }
264
265    @Override
266    public int getType() {
267        return NOTIFICATION_TRANSACTION;
268    }
269}
270