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;
21import java.util.ArrayList;
22
23import android.app.Service;
24import android.content.BroadcastReceiver;
25import android.content.ContentUris;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.database.Cursor;
30import android.net.ConnectivityManager;
31import android.net.NetworkInfo;
32import android.net.Uri;
33import android.os.Handler;
34import android.os.HandlerThread;
35import android.os.IBinder;
36import android.os.Looper;
37import android.os.Message;
38import android.os.PowerManager;
39import android.provider.Telephony.Mms;
40import android.provider.Telephony.MmsSms;
41import android.provider.Telephony.MmsSms.PendingMessages;
42import android.text.TextUtils;
43import android.util.Log;
44import android.widget.Toast;
45
46import com.android.internal.telephony.Phone;
47import com.android.internal.telephony.PhoneConstants;
48import com.android.mms.LogTag;
49import com.android.mms.R;
50import com.android.mms.util.DownloadManager;
51import com.android.mms.util.RateController;
52import com.google.android.mms.pdu.GenericPdu;
53import com.google.android.mms.pdu.NotificationInd;
54import com.google.android.mms.pdu.PduHeaders;
55import com.google.android.mms.pdu.PduParser;
56import com.google.android.mms.pdu.PduPersister;
57
58/**
59 * The TransactionService of the MMS Client is responsible for handling requests
60 * to initiate client-transactions sent from:
61 * <ul>
62 * <li>The Proxy-Relay (Through Push messages)</li>
63 * <li>The composer/viewer activities of the MMS Client (Through intents)</li>
64 * </ul>
65 * The TransactionService runs locally in the same process as the application.
66 * It contains a HandlerThread to which messages are posted from the
67 * intent-receivers of this application.
68 * <p/>
69 * <b>IMPORTANT</b>: This is currently the only instance in the system in
70 * which simultaneous connectivity to both the mobile data network and
71 * a Wi-Fi network is allowed. This makes the code for handling network
72 * connectivity somewhat different than it is in other applications. In
73 * particular, we want to be able to send or receive MMS messages when
74 * a Wi-Fi connection is active (which implies that there is no connection
75 * to the mobile data network). This has two main consequences:
76 * <ul>
77 * <li>Testing for current network connectivity ({@link android.net.NetworkInfo#isConnected()} is
78 * not sufficient. Instead, the correct test is for network availability
79 * ({@link android.net.NetworkInfo#isAvailable()}).</li>
80 * <li>If the mobile data network is not in the connected state, but it is available,
81 * we must initiate setup of the mobile data connection, and defer handling
82 * the MMS transaction until the connection is established.</li>
83 * </ul>
84 */
85public class TransactionService extends Service implements Observer {
86    private static final String TAG = "TransactionService";
87
88    /**
89     * Used to identify notification intents broadcasted by the
90     * TransactionService when a Transaction is completed.
91     */
92    public static final String TRANSACTION_COMPLETED_ACTION =
93            "android.intent.action.TRANSACTION_COMPLETED_ACTION";
94
95    /**
96     * Action for the Intent which is sent by Alarm service to launch
97     * TransactionService.
98     */
99    public static final String ACTION_ONALARM = "android.intent.action.ACTION_ONALARM";
100
101    /**
102     * Action for the Intent which is sent when the user turns on the auto-retrieve setting.
103     * This service gets started to auto-retrieve any undownloaded messages.
104     */
105    public static final String ACTION_ENABLE_AUTO_RETRIEVE
106            = "android.intent.action.ACTION_ENABLE_AUTO_RETRIEVE";
107
108    /**
109     * Used as extra key in notification intents broadcasted by the TransactionService
110     * when a Transaction is completed (TRANSACTION_COMPLETED_ACTION intents).
111     * Allowed values for this key are: TransactionState.INITIALIZED,
112     * TransactionState.SUCCESS, TransactionState.FAILED.
113     */
114    public static final String STATE = "state";
115
116    /**
117     * Used as extra key in notification intents broadcasted by the TransactionService
118     * when a Transaction is completed (TRANSACTION_COMPLETED_ACTION intents).
119     * Allowed values for this key are any valid content uri.
120     */
121    public static final String STATE_URI = "uri";
122
123    private static final int EVENT_TRANSACTION_REQUEST = 1;
124    private static final int EVENT_CONTINUE_MMS_CONNECTIVITY = 3;
125    private static final int EVENT_HANDLE_NEXT_PENDING_TRANSACTION = 4;
126    private static final int EVENT_NEW_INTENT = 5;
127    private static final int EVENT_QUIT = 100;
128
129    private static final int TOAST_MSG_QUEUED = 1;
130    private static final int TOAST_DOWNLOAD_LATER = 2;
131    private static final int TOAST_NONE = -1;
132
133    // How often to extend the use of the MMS APN while a transaction
134    // is still being processed.
135    private static final int APN_EXTENSION_WAIT = 30 * 1000;
136
137    private ServiceHandler mServiceHandler;
138    private Looper mServiceLooper;
139    private final ArrayList<Transaction> mProcessing  = new ArrayList<Transaction>();
140    private final ArrayList<Transaction> mPending  = new ArrayList<Transaction>();
141    private ConnectivityManager mConnMgr;
142    private ConnectivityBroadcastReceiver mReceiver;
143
144    private PowerManager.WakeLock mWakeLock;
145
146    public Handler mToastHandler = new Handler() {
147        @Override
148        public void handleMessage(Message msg) {
149            String str = null;
150
151            if (msg.what == TOAST_MSG_QUEUED) {
152                str = getString(R.string.message_queued);
153            } else if (msg.what == TOAST_DOWNLOAD_LATER) {
154                str = getString(R.string.download_later);
155            }
156
157            if (str != null) {
158                Toast.makeText(TransactionService.this, str,
159                        Toast.LENGTH_LONG).show();
160            }
161        }
162    };
163
164    @Override
165    public void onCreate() {
166        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
167            Log.v(TAG, "Creating TransactionService");
168        }
169
170        // Start up the thread running the service.  Note that we create a
171        // separate thread because the service normally runs in the process's
172        // main thread, which we don't want to block.
173        HandlerThread thread = new HandlerThread("TransactionService");
174        thread.start();
175
176        mServiceLooper = thread.getLooper();
177        mServiceHandler = new ServiceHandler(mServiceLooper);
178
179        mReceiver = new ConnectivityBroadcastReceiver();
180        IntentFilter intentFilter = new IntentFilter();
181        intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
182        registerReceiver(mReceiver, intentFilter);
183    }
184
185    @Override
186    public int onStartCommand(Intent intent, int flags, int startId) {
187        if (intent != null) {
188            Message msg = mServiceHandler.obtainMessage(EVENT_NEW_INTENT);
189            msg.arg1 = startId;
190            msg.obj = intent;
191            mServiceHandler.sendMessage(msg);
192        }
193        return Service.START_NOT_STICKY;
194    }
195
196    public void onNewIntent(Intent intent, int serviceId) {
197        mConnMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
198        boolean noNetwork = !isNetworkAvailable();
199
200        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
201            Log.v(TAG, "onNewIntent: serviceId: " + serviceId + ": " + intent.getExtras() +
202                    " intent=" + intent);
203            Log.v(TAG, "    networkAvailable=" + !noNetwork);
204        }
205
206        String action = intent.getAction();
207        if (ACTION_ONALARM.equals(action) || ACTION_ENABLE_AUTO_RETRIEVE.equals(action) ||
208                (intent.getExtras() == null)) {
209            // Scan database to find all pending operations.
210            Cursor cursor = PduPersister.getPduPersister(this).getPendingMessages(
211                    System.currentTimeMillis());
212            if (cursor != null) {
213                try {
214                    int count = cursor.getCount();
215
216                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
217                        Log.v(TAG, "onNewIntent: cursor.count=" + count + " action=" + action);
218                    }
219
220                    if (count == 0) {
221                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
222                            Log.v(TAG, "onNewIntent: no pending messages. Stopping service.");
223                        }
224                        RetryScheduler.setRetryAlarm(this);
225                        stopSelfIfIdle(serviceId);
226                        return;
227                    }
228
229                    int columnIndexOfMsgId = cursor.getColumnIndexOrThrow(PendingMessages.MSG_ID);
230                    int columnIndexOfMsgType = cursor.getColumnIndexOrThrow(
231                            PendingMessages.MSG_TYPE);
232
233                    while (cursor.moveToNext()) {
234                        int msgType = cursor.getInt(columnIndexOfMsgType);
235                        int transactionType = getTransactionType(msgType);
236                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
237                            Log.v(TAG, "onNewIntent: msgType=" + msgType + " transactionType=" +
238                                    transactionType);
239                        }
240                        if (noNetwork) {
241                            onNetworkUnavailable(serviceId, transactionType);
242                            return;
243                        }
244                        switch (transactionType) {
245                            case -1:
246                                break;
247                            case Transaction.RETRIEVE_TRANSACTION:
248                                // If it's a transiently failed transaction,
249                                // we should retry it in spite of current
250                                // downloading mode. If the user just turned on the auto-retrieve
251                                // option, we also retry those messages that don't have any errors.
252                                int failureType = cursor.getInt(
253                                        cursor.getColumnIndexOrThrow(
254                                                PendingMessages.ERROR_TYPE));
255                                DownloadManager downloadManager = DownloadManager.getInstance();
256                                boolean autoDownload = downloadManager.isAuto();
257                                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
258                                    Log.v(TAG, "onNewIntent: failureType=" + failureType +
259                                            " action=" + action + " isTransientFailure:" +
260                                            isTransientFailure(failureType) + " autoDownload=" +
261                                            autoDownload);
262                                }
263                                if (!autoDownload) {
264                                    // If autodownload is turned off, don't process the
265                                    // transaction.
266                                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
267                                        Log.v(TAG, "onNewIntent: skipping - autodownload off");
268                                    }
269                                    break;
270                                }
271                                // Logic is twisty. If there's no failure or the failure
272                                // is a non-permanent failure, we want to process the transaction.
273                                // Otherwise, break out and skip processing this transaction.
274                                if (!(failureType == MmsSms.NO_ERROR ||
275                                        isTransientFailure(failureType))) {
276                                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
277                                        Log.v(TAG, "onNewIntent: skipping - permanent error");
278                                    }
279                                    break;
280                                }
281                                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
282                                    Log.v(TAG, "onNewIntent: falling through and processing");
283                                }
284                               // fall-through
285                            default:
286                                Uri uri = ContentUris.withAppendedId(
287                                        Mms.CONTENT_URI,
288                                        cursor.getLong(columnIndexOfMsgId));
289                                TransactionBundle args = new TransactionBundle(
290                                        transactionType, uri.toString());
291                                // FIXME: We use the same startId for all MMs.
292                                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
293                                    Log.v(TAG, "onNewIntent: launchTransaction uri=" + uri);
294                                }
295                                launchTransaction(serviceId, args, false);
296                                break;
297                        }
298                    }
299                } finally {
300                    cursor.close();
301                }
302            } else {
303                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
304                    Log.v(TAG, "onNewIntent: no pending messages. Stopping service.");
305                }
306                RetryScheduler.setRetryAlarm(this);
307                stopSelfIfIdle(serviceId);
308            }
309        } else {
310            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
311                Log.v(TAG, "onNewIntent: launch transaction...");
312            }
313            // For launching NotificationTransaction and test purpose.
314            TransactionBundle args = new TransactionBundle(intent.getExtras());
315            launchTransaction(serviceId, args, noNetwork);
316        }
317    }
318
319    private void stopSelfIfIdle(int startId) {
320        synchronized (mProcessing) {
321            if (mProcessing.isEmpty() && mPending.isEmpty()) {
322                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
323                    Log.v(TAG, "stopSelfIfIdle: STOP!");
324                }
325
326                stopSelf(startId);
327            }
328        }
329    }
330
331    private static boolean isTransientFailure(int type) {
332        return type > MmsSms.NO_ERROR && type < MmsSms.ERR_TYPE_GENERIC_PERMANENT;
333    }
334
335    private boolean isNetworkAvailable() {
336        if (mConnMgr == null) {
337            return false;
338        } else {
339            NetworkInfo ni = mConnMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS);
340            return (ni == null ? false : ni.isAvailable());
341        }
342    }
343
344    private int getTransactionType(int msgType) {
345        switch (msgType) {
346            case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND:
347                return Transaction.RETRIEVE_TRANSACTION;
348            case PduHeaders.MESSAGE_TYPE_READ_REC_IND:
349                return Transaction.READREC_TRANSACTION;
350            case PduHeaders.MESSAGE_TYPE_SEND_REQ:
351                return Transaction.SEND_TRANSACTION;
352            default:
353                Log.w(TAG, "Unrecognized MESSAGE_TYPE: " + msgType);
354                return -1;
355        }
356    }
357
358    private void launchTransaction(int serviceId, TransactionBundle txnBundle, boolean noNetwork) {
359        if (noNetwork) {
360            Log.w(TAG, "launchTransaction: no network error!");
361            onNetworkUnavailable(serviceId, txnBundle.getTransactionType());
362            return;
363        }
364        Message msg = mServiceHandler.obtainMessage(EVENT_TRANSACTION_REQUEST);
365        msg.arg1 = serviceId;
366        msg.obj = txnBundle;
367
368        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
369            Log.v(TAG, "launchTransaction: sending message " + msg);
370        }
371        mServiceHandler.sendMessage(msg);
372    }
373
374    private void onNetworkUnavailable(int serviceId, int transactionType) {
375        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
376            Log.v(TAG, "onNetworkUnavailable: sid=" + serviceId + ", type=" + transactionType);
377        }
378
379        int toastType = TOAST_NONE;
380        if (transactionType == Transaction.RETRIEVE_TRANSACTION) {
381            toastType = TOAST_DOWNLOAD_LATER;
382        } else if (transactionType == Transaction.SEND_TRANSACTION) {
383            toastType = TOAST_MSG_QUEUED;
384        }
385        if (toastType != TOAST_NONE) {
386            mToastHandler.sendEmptyMessage(toastType);
387        }
388        stopSelf(serviceId);
389    }
390
391    @Override
392    public void onDestroy() {
393        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
394            Log.v(TAG, "Destroying TransactionService");
395        }
396        if (!mPending.isEmpty()) {
397            Log.w(TAG, "TransactionService exiting with transaction still pending");
398        }
399
400        releaseWakeLock();
401
402        unregisterReceiver(mReceiver);
403
404        mServiceHandler.sendEmptyMessage(EVENT_QUIT);
405    }
406
407    @Override
408    public IBinder onBind(Intent intent) {
409        return null;
410    }
411
412    /**
413     * Handle status change of Transaction (The Observable).
414     */
415    public void update(Observable observable) {
416        Transaction transaction = (Transaction) observable;
417        int serviceId = transaction.getServiceId();
418
419        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
420            Log.v(TAG, "update transaction " + serviceId);
421        }
422
423        try {
424            synchronized (mProcessing) {
425                mProcessing.remove(transaction);
426                if (mPending.size() > 0) {
427                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
428                        Log.v(TAG, "update: handle next pending transaction...");
429                    }
430                    Message msg = mServiceHandler.obtainMessage(
431                            EVENT_HANDLE_NEXT_PENDING_TRANSACTION,
432                            transaction.getConnectionSettings());
433                    mServiceHandler.sendMessage(msg);
434                }
435                else {
436                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
437                        Log.v(TAG, "update: endMmsConnectivity");
438                    }
439                    endMmsConnectivity();
440                }
441            }
442
443            Intent intent = new Intent(TRANSACTION_COMPLETED_ACTION);
444            TransactionState state = transaction.getState();
445            int result = state.getState();
446            intent.putExtra(STATE, result);
447
448            switch (result) {
449                case TransactionState.SUCCESS:
450                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
451                        Log.v(TAG, "Transaction complete: " + serviceId);
452                    }
453
454                    intent.putExtra(STATE_URI, state.getContentUri());
455
456                    // Notify user in the system-wide notification area.
457                    switch (transaction.getType()) {
458                        case Transaction.NOTIFICATION_TRANSACTION:
459                        case Transaction.RETRIEVE_TRANSACTION:
460                            // We're already in a non-UI thread called from
461                            // NotificationTransacation.run(), so ok to block here.
462                            long threadId = MessagingNotification.getThreadId(
463                                    this, state.getContentUri());
464                            MessagingNotification.blockingUpdateNewMessageIndicator(this,
465                                    threadId,
466                                    false);
467                            MessagingNotification.updateDownloadFailedNotification(this);
468                            break;
469                        case Transaction.SEND_TRANSACTION:
470                            RateController.getInstance().update();
471                            break;
472                    }
473                    break;
474                case TransactionState.FAILED:
475                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
476                        Log.v(TAG, "Transaction failed: " + serviceId);
477                    }
478                    break;
479                default:
480                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
481                        Log.v(TAG, "Transaction state unknown: " +
482                                serviceId + " " + result);
483                    }
484                    break;
485            }
486
487            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
488                Log.v(TAG, "update: broadcast transaction result " + result);
489            }
490            // Broadcast the result of the transaction.
491            sendBroadcast(intent);
492        } finally {
493            transaction.detach(this);
494            stopSelf(serviceId);
495        }
496    }
497
498    private synchronized void createWakeLock() {
499        // Create a new wake lock if we haven't made one yet.
500        if (mWakeLock == null) {
501            PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
502            mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MMS Connectivity");
503            mWakeLock.setReferenceCounted(false);
504        }
505    }
506
507    private void acquireWakeLock() {
508        // It's okay to double-acquire this because we are not using it
509        // in reference-counted mode.
510        mWakeLock.acquire();
511    }
512
513    private void releaseWakeLock() {
514        // Don't release the wake lock if it hasn't been created and acquired.
515        if (mWakeLock != null && mWakeLock.isHeld()) {
516            mWakeLock.release();
517        }
518    }
519
520    protected int beginMmsConnectivity() throws IOException {
521        // Take a wake lock so we don't fall asleep before the message is downloaded.
522        createWakeLock();
523
524        int result = mConnMgr.startUsingNetworkFeature(
525                ConnectivityManager.TYPE_MOBILE, Phone.FEATURE_ENABLE_MMS);
526
527        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
528            Log.v(TAG, "beginMmsConnectivity: result=" + result);
529        }
530
531        switch (result) {
532            case PhoneConstants.APN_ALREADY_ACTIVE:
533            case PhoneConstants.APN_REQUEST_STARTED:
534                acquireWakeLock();
535                return result;
536        }
537
538        throw new IOException("Cannot establish MMS connectivity");
539    }
540
541    protected void endMmsConnectivity() {
542        try {
543            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
544                Log.v(TAG, "endMmsConnectivity");
545            }
546
547            // cancel timer for renewal of lease
548            mServiceHandler.removeMessages(EVENT_CONTINUE_MMS_CONNECTIVITY);
549            if (mConnMgr != null) {
550                mConnMgr.stopUsingNetworkFeature(
551                        ConnectivityManager.TYPE_MOBILE,
552                        Phone.FEATURE_ENABLE_MMS);
553            }
554        } finally {
555            releaseWakeLock();
556        }
557    }
558
559    private final class ServiceHandler extends Handler {
560        public ServiceHandler(Looper looper) {
561            super(looper);
562        }
563
564        private String decodeMessage(Message msg) {
565            if (msg.what == EVENT_QUIT) {
566                return "EVENT_QUIT";
567            } else if (msg.what == EVENT_CONTINUE_MMS_CONNECTIVITY) {
568                return "EVENT_CONTINUE_MMS_CONNECTIVITY";
569            } else if (msg.what == EVENT_TRANSACTION_REQUEST) {
570                return "EVENT_TRANSACTION_REQUEST";
571            } else if (msg.what == EVENT_HANDLE_NEXT_PENDING_TRANSACTION) {
572                return "EVENT_HANDLE_NEXT_PENDING_TRANSACTION";
573            } else if (msg.what == EVENT_NEW_INTENT) {
574                return "EVENT_NEW_INTENT";
575            }
576            return "unknown message.what";
577        }
578
579        private String decodeTransactionType(int transactionType) {
580            if (transactionType == Transaction.NOTIFICATION_TRANSACTION) {
581                return "NOTIFICATION_TRANSACTION";
582            } else if (transactionType == Transaction.RETRIEVE_TRANSACTION) {
583                return "RETRIEVE_TRANSACTION";
584            } else if (transactionType == Transaction.SEND_TRANSACTION) {
585                return "SEND_TRANSACTION";
586            } else if (transactionType == Transaction.READREC_TRANSACTION) {
587                return "READREC_TRANSACTION";
588            }
589            return "invalid transaction type";
590        }
591
592        /**
593         * Handle incoming transaction requests.
594         * The incoming requests are initiated by the MMSC Server or by the
595         * MMS Client itself.
596         */
597        @Override
598        public void handleMessage(Message msg) {
599            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
600                Log.v(TAG, "Handling incoming message: " + msg + " = " + decodeMessage(msg));
601            }
602
603            Transaction transaction = null;
604
605            switch (msg.what) {
606                case EVENT_NEW_INTENT:
607                    onNewIntent((Intent)msg.obj, msg.arg1);
608                    break;
609
610                case EVENT_QUIT:
611                    getLooper().quit();
612                    return;
613
614                case EVENT_CONTINUE_MMS_CONNECTIVITY:
615                    synchronized (mProcessing) {
616                        if (mProcessing.isEmpty()) {
617                            return;
618                        }
619                    }
620
621                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
622                        Log.v(TAG, "handle EVENT_CONTINUE_MMS_CONNECTIVITY event...");
623                    }
624
625                    try {
626                        int result = beginMmsConnectivity();
627                        if (result != PhoneConstants.APN_ALREADY_ACTIVE) {
628                            Log.v(TAG, "Extending MMS connectivity returned " + result +
629                                    " instead of APN_ALREADY_ACTIVE");
630                            // Just wait for connectivity startup without
631                            // any new request of APN switch.
632                            return;
633                        }
634                    } catch (IOException e) {
635                        Log.w(TAG, "Attempt to extend use of MMS connectivity failed");
636                        return;
637                    }
638
639                    // Restart timer
640                    renewMmsConnectivity();
641                    return;
642
643                case EVENT_TRANSACTION_REQUEST:
644                    int serviceId = msg.arg1;
645                    try {
646                        TransactionBundle args = (TransactionBundle) msg.obj;
647                        TransactionSettings transactionSettings;
648
649                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
650                            Log.v(TAG, "EVENT_TRANSACTION_REQUEST MmscUrl=" +
651                                    args.getMmscUrl() + " proxy port: " + args.getProxyAddress());
652                        }
653
654                        // Set the connection settings for this transaction.
655                        // If these have not been set in args, load the default settings.
656                        String mmsc = args.getMmscUrl();
657                        if (mmsc != null) {
658                            transactionSettings = new TransactionSettings(
659                                    mmsc, args.getProxyAddress(), args.getProxyPort());
660                        } else {
661                            transactionSettings = new TransactionSettings(
662                                                    TransactionService.this, null);
663                        }
664
665                        int transactionType = args.getTransactionType();
666
667                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
668                            Log.v(TAG, "handle EVENT_TRANSACTION_REQUEST: transactionType=" +
669                                    transactionType + " " + decodeTransactionType(transactionType));
670                        }
671
672                        // Create appropriate transaction
673                        switch (transactionType) {
674                            case Transaction.NOTIFICATION_TRANSACTION:
675                                String uri = args.getUri();
676                                if (uri != null) {
677                                    transaction = new NotificationTransaction(
678                                            TransactionService.this, serviceId,
679                                            transactionSettings, uri);
680                                } else {
681                                    // Now it's only used for test purpose.
682                                    byte[] pushData = args.getPushData();
683                                    PduParser parser = new PduParser(pushData);
684                                    GenericPdu ind = parser.parse();
685
686                                    int type = PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
687                                    if ((ind != null) && (ind.getMessageType() == type)) {
688                                        transaction = new NotificationTransaction(
689                                                TransactionService.this, serviceId,
690                                                transactionSettings, (NotificationInd) ind);
691                                    } else {
692                                        Log.e(TAG, "Invalid PUSH data.");
693                                        transaction = null;
694                                        return;
695                                    }
696                                }
697                                break;
698                            case Transaction.RETRIEVE_TRANSACTION:
699                                transaction = new RetrieveTransaction(
700                                        TransactionService.this, serviceId,
701                                        transactionSettings, args.getUri());
702                                break;
703                            case Transaction.SEND_TRANSACTION:
704                                transaction = new SendTransaction(
705                                        TransactionService.this, serviceId,
706                                        transactionSettings, args.getUri());
707                                break;
708                            case Transaction.READREC_TRANSACTION:
709                                transaction = new ReadRecTransaction(
710                                        TransactionService.this, serviceId,
711                                        transactionSettings, args.getUri());
712                                break;
713                            default:
714                                Log.w(TAG, "Invalid transaction type: " + serviceId);
715                                transaction = null;
716                                return;
717                        }
718
719                        if (!processTransaction(transaction)) {
720                            transaction = null;
721                            return;
722                        }
723
724                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
725                            Log.v(TAG, "Started processing of incoming message: " + msg);
726                        }
727                    } catch (Exception ex) {
728                        Log.w(TAG, "Exception occurred while handling message: " + msg, ex);
729
730                        if (transaction != null) {
731                            try {
732                                transaction.detach(TransactionService.this);
733                                if (mProcessing.contains(transaction)) {
734                                    synchronized (mProcessing) {
735                                        mProcessing.remove(transaction);
736                                    }
737                                }
738                            } catch (Throwable t) {
739                                Log.e(TAG, "Unexpected Throwable.", t);
740                            } finally {
741                                // Set transaction to null to allow stopping the
742                                // transaction service.
743                                transaction = null;
744                            }
745                        }
746                    } finally {
747                        if (transaction == null) {
748                            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
749                                Log.v(TAG, "Transaction was null. Stopping self: " + serviceId);
750                            }
751                            endMmsConnectivity();
752                            stopSelf(serviceId);
753                        }
754                    }
755                    return;
756                case EVENT_HANDLE_NEXT_PENDING_TRANSACTION:
757                    processPendingTransaction(transaction, (TransactionSettings) msg.obj);
758                    return;
759                default:
760                    Log.w(TAG, "what=" + msg.what);
761                    return;
762            }
763        }
764
765        public void processPendingTransaction(Transaction transaction,
766                                               TransactionSettings settings) {
767
768            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
769                Log.v(TAG, "processPendingTxn: transaction=" + transaction);
770            }
771
772            int numProcessTransaction = 0;
773            synchronized (mProcessing) {
774                if (mPending.size() != 0) {
775                    transaction = mPending.remove(0);
776                }
777                numProcessTransaction = mProcessing.size();
778            }
779
780            if (transaction != null) {
781                if (settings != null) {
782                    transaction.setConnectionSettings(settings);
783                }
784
785                /*
786                 * Process deferred transaction
787                 */
788                try {
789                    int serviceId = transaction.getServiceId();
790
791                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
792                        Log.v(TAG, "processPendingTxn: process " + serviceId);
793                    }
794
795                    if (processTransaction(transaction)) {
796                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
797                            Log.v(TAG, "Started deferred processing of transaction  "
798                                    + transaction);
799                        }
800                    } else {
801                        transaction = null;
802                        stopSelf(serviceId);
803                    }
804                } catch (IOException e) {
805                    Log.w(TAG, e.getMessage(), e);
806                }
807            } else {
808                if (numProcessTransaction == 0) {
809                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
810                        Log.v(TAG, "processPendingTxn: no more transaction, endMmsConnectivity");
811                    }
812                    endMmsConnectivity();
813                }
814            }
815        }
816
817        /**
818         * Internal method to begin processing a transaction.
819         * @param transaction the transaction. Must not be {@code null}.
820         * @return {@code true} if process has begun or will begin. {@code false}
821         * if the transaction should be discarded.
822         * @throws IOException if connectivity for MMS traffic could not be
823         * established.
824         */
825        private boolean processTransaction(Transaction transaction) throws IOException {
826            // Check if transaction already processing
827            synchronized (mProcessing) {
828                for (Transaction t : mPending) {
829                    if (t.isEquivalent(transaction)) {
830                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
831                            Log.v(TAG, "Transaction already pending: " +
832                                    transaction.getServiceId());
833                        }
834                        return true;
835                    }
836                }
837                for (Transaction t : mProcessing) {
838                    if (t.isEquivalent(transaction)) {
839                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
840                            Log.v(TAG, "Duplicated transaction: " + transaction.getServiceId());
841                        }
842                        return true;
843                    }
844                }
845
846                /*
847                * Make sure that the network connectivity necessary
848                * for MMS traffic is enabled. If it is not, we need
849                * to defer processing the transaction until
850                * connectivity is established.
851                */
852                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
853                    Log.v(TAG, "processTransaction: call beginMmsConnectivity...");
854                }
855                int connectivityResult = beginMmsConnectivity();
856                if (connectivityResult == PhoneConstants.APN_REQUEST_STARTED) {
857                    mPending.add(transaction);
858                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
859                        Log.v(TAG, "processTransaction: connResult=APN_REQUEST_STARTED, " +
860                                "defer transaction pending MMS connectivity");
861                    }
862                    return true;
863                }
864
865                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
866                    Log.v(TAG, "Adding transaction to 'mProcessing' list: " + transaction);
867                }
868                mProcessing.add(transaction);
869            }
870
871            // Set a timer to keep renewing our "lease" on the MMS connection
872            sendMessageDelayed(obtainMessage(EVENT_CONTINUE_MMS_CONNECTIVITY),
873                               APN_EXTENSION_WAIT);
874
875            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
876                Log.v(TAG, "processTransaction: starting transaction " + transaction);
877            }
878
879            // Attach to transaction and process it
880            transaction.attach(TransactionService.this);
881            transaction.process();
882            return true;
883        }
884    }
885
886    private void renewMmsConnectivity() {
887        // Set a timer to keep renewing our "lease" on the MMS connection
888        mServiceHandler.sendMessageDelayed(
889                mServiceHandler.obtainMessage(EVENT_CONTINUE_MMS_CONNECTIVITY),
890                           APN_EXTENSION_WAIT);
891    }
892
893    private class ConnectivityBroadcastReceiver extends BroadcastReceiver {
894        @Override
895        public void onReceive(Context context, Intent intent) {
896            String action = intent.getAction();
897            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
898                Log.w(TAG, "ConnectivityBroadcastReceiver.onReceive() action: " + action);
899            }
900
901            if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
902                return;
903            }
904
905            NetworkInfo mmsNetworkInfo = null;
906
907            if (mConnMgr != null) {
908                mmsNetworkInfo = mConnMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS);
909            } else {
910                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
911                    Log.v(TAG, "mConnMgr is null, bail");
912                }
913            }
914
915            /*
916             * If we are being informed that connectivity has been established
917             * to allow MMS traffic, then proceed with processing the pending
918             * transaction, if any.
919             */
920
921            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
922                Log.v(TAG, "Handle ConnectivityBroadcastReceiver.onReceive(): " + mmsNetworkInfo);
923            }
924
925            // Check availability of the mobile network.
926            if ((mmsNetworkInfo == null)) {
927                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
928                    Log.v(TAG, "mms type is null, bail");
929                }
930            } else {
931                // This is a very specific fix to handle the case where the phone receives an
932                // incoming call during the time we're trying to setup the mms connection.
933                // When the call ends, restart the process of mms connectivity.
934                if (Phone.REASON_VOICE_CALL_ENDED.equals(mmsNetworkInfo.getReason())) {
935                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
936                        Log.v(TAG, "   reason is " + Phone.REASON_VOICE_CALL_ENDED +
937                                ", retrying mms connectivity");
938                    }
939                    renewMmsConnectivity();
940                    return;
941                }
942
943                if (mmsNetworkInfo.isConnected()) {
944                    TransactionSettings settings = new TransactionSettings(
945                            TransactionService.this, mmsNetworkInfo.getExtraInfo());
946                    // If this APN doesn't have an MMSC, wait for one that does.
947                    if (TextUtils.isEmpty(settings.getMmscUrl())) {
948                        Log.v(TAG, "   empty MMSC url, bail");
949                        return;
950                    }
951                    mServiceHandler.processPendingTransaction(null, settings);
952                } else {
953                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
954                        Log.v(TAG, "   TYPE_MOBILE_MMS not connected, bail");
955                    }
956
957                    // Retry mms connectivity once it's possible to connect
958                    if (mmsNetworkInfo.isAvailable()) {
959                        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
960                            Log.v(TAG, "   retrying mms connectivity for it's available");
961                        }
962                        renewMmsConnectivity();
963                    }
964                }
965            }
966        }
967    };
968}
969