android 彩信接收到附件的下載原理分析

l_serein發表於2012-09-19

彩信收發巨集觀步驟:

a、 終端A向彩信中心(MMSC)傳送一條彩信,通過WAP閘道器POST到MMSC

b、 MMSC通過PushProxy閘道器,向SMSC傳送PUSH訊息,SMSC轉發到終端B

c、 終端B通過WAP閘道器利用GET方法從MMSC獲取一條彩信

d、 MMSC通過PushProxy閘道器和SNSC向終端A傳送一條傳送報告(delivery report)

從上面這個步驟可以看出,彩信的接收分兩個步驟:

1、接收到簡訊。

2、分析簡訊然後通過http來獲取彩信附件。

(續我的另一篇部落格:http://www.cnblogs.com/not-code/archive/2011/11/27/2265287.html

因此彩信第一步跟簡訊的接收流程一樣,在RILReceiver 接收到簡訊轉到processUnsolicited進行處理。

GSM方式(最近才知道簡訊的收發有兩種,一種就是通過GSM,另一種是通過CDMA):GSM其事件型別為RIL_UNSOL_RESPONSE_NEW_SMS。先呼叫responseString從Parcel中獲取資料,再使用 newFromCMT方法獲取SmsMessage物件,最後呼叫mSMSRegistrant的notifyRegistrant方法設定訊息型別 (what屬性為EVENT_NEW_SMS)並轉到SMSDispatcher進行處理。這個時候就會呼叫子類(GsmSMSDispatcher)的dispatchMessage方法處理。

 

/** {@inheritDoc} */ 
    protected int dispatchMessage(SmsMessageBase smsb) { 
        // If sms is null, means there was a parsing error. 
        if (smsb == null) { 
            return Intents.RESULT_SMS_GENERIC_ERROR; 
        } 
        SmsMessage sms = (SmsMessage) smsb; 
        boolean handled = false;

        if (sms.isTypeZero()) { 
            // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be 
            // Displayed/Stored/Notified. They should only be acknowledged. 
            Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack"); 
            return Intents.RESULT_SMS_HANDLED; 
        }

        // Special case the message waiting indicator messages 
        if (sms.isMWISetMessage()) { 
            mGsmPhone.updateMessageWaitingIndicator(true); 
            handled = sms.isMwiDontStore(); 
            if (Config.LOGD) { 
                Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled); 
            } 
        } else if (sms.isMWIClearMessage()) { 
            mGsmPhone.updateMessageWaitingIndicator(false); 
            handled = sms.isMwiDontStore(); 
            if (Config.LOGD) { 
                Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled); 
            } 
        }

        if (handled) { 
            return Intents.RESULT_SMS_HANDLED; 
        }

        if (!mStorageAvailable && (sms.getMessageClass() != MessageClass.CLASS_0)) { 
            // It's a storable message and there's no storage available.  Bail. 
            // (See TS 23.038 for a description of class 0 messages.) 
            return Intents.RESULT_SMS_OUT_OF_MEMORY; 
        }

        SmsHeader smsHeader = sms.getUserDataHeader(); 
         // See if message is partial or port addressed. 
        if ((smsHeader == null) || (smsHeader.concatRef == null)) { 
            // Message is not partial (not part of concatenated sequence). 
            byte[][] pdus = new byte[1][]; 
            pdus[0] = sms.getPdu();

            if (smsHeader != null && smsHeader.portAddrs != null) { 
                if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { 
                    return mWapPush.dispatchWapPdu(sms.getUserData()); 
                } else { 
                    // The message was sent to a port, so concoct a URI for it. 
                    dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort); 
                } 
            } else { 
                // Normal short and non-port-addressed message, dispatch it. 
                dispatchPdus(pdus); 
            } 
            return Activity.RESULT_OK; 
        } else { 
            // Process the message part. 
            return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs); 
        } 
    }

 

在這個方法裡,將會判斷接收到的簡訊是否長簡訊、是否彩信、是否普通簡訊。首先獲取SmsHeader, 
如果SmsHeader或SmsHeader.concatRef均不為空,說明是長簡訊,則呼叫processMessagePart將簡訊分段存入raw表,待所有分段都收到後,將其組裝。然後根據埠的不同,按照彩信通知(WapPushOverSms的dispatchWapPdu方法)、指定埠的彩信(dispatchPortAddressedPdus)、長簡訊(dispatchPdus)進行分發處理。

 

(從網上竊的一副圖~~)(左邊是GSM的處理,右邊是CDMA的處理,兩個處理類都是繼承了SMSDispatcher)

image

繼續分析彩信,如果是彩信就呼叫WapPushOverSmsdispatchWapPdu的方法。(該類的位置)

image

 


    public int dispatchWapPdu(byte[] pdu) {

        if (Config.DEBUG) Log.d(LOG_TAG, "Rx: " + IccUtils.bytesToHexString(pdu));

        int index = 0; 
        int transactionId = pdu[index++] & 0xFF; 
        int pduType = pdu[index++] & 0xFF; 
        int headerLength = 0;

        if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) && 
                (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) { 
            if (Config.DEBUG) Log.w(LOG_TAG, "Received non-PUSH WAP PDU. Type = " + pduType); 
            return Intents.RESULT_SMS_HANDLED; 
        }

        pduDecoder = new WspTypeDecoder(pdu);

        /** 
         * Parse HeaderLen(unsigned integer). 
         * From wap-230-wsp-20010705-a section 8.1.2 
         * The maximum size of a uintvar is 32 bits. 
         * So it will be encoded in no more than 5 octets. 
         */ 
        if (pduDecoder.decodeUintvarInteger(index) == false) { 
            if (Config.DEBUG) Log.w(LOG_TAG, "Received PDU. Header Length error."); 
            return Intents.RESULT_SMS_GENERIC_ERROR; 
        } 
        headerLength = (int)pduDecoder.getValue32(); 
        index += pduDecoder.getDecodedDataLength();

        int headerStartIndex = index;

        /** 
         * Parse Content-Type. 
         * From wap-230-wsp-20010705-a section 8.4.2.24 
         * 
         * Content-type-value = Constrained-media | Content-general-form 
         * Content-general-form = Value-length Media-type 
         * Media-type = (Well-known-media | Extension-Media) *(Parameter) 
         * Value-length = Short-length | (Length-quote Length) 
         * Short-length = <Any octet 0-30>   (octet <= WAP_PDU_SHORT_LENGTH_MAX) 
         * Length-quote = <Octet 31>         (WAP_PDU_LENGTH_QUOTE) 
         * Length = Uintvar-integer 
         */ 
        if (pduDecoder.decodeContentType(index) == false) { 
            if (Config.DEBUG) Log.w(LOG_TAG, "Received PDU. Header Content-Type error."); 
            return Intents.RESULT_SMS_GENERIC_ERROR; 
        }

        String mimeType = pduDecoder.getValueString(); 
        long binaryContentType = pduDecoder.getValue32(); 
        index += pduDecoder.getDecodedDataLength();

        byte[] header = new byte[headerLength]; 
        System.arraycopy(pdu, headerStartIndex, header, 0, header.length);

        byte[] intentData;

        if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO)) { 
            intentData = pdu; 
        } else { 
            int dataIndex = headerStartIndex + headerLength; 
            intentData = new byte[pdu.length - dataIndex]; 
            System.arraycopy(pdu, dataIndex, intentData, 0, intentData.length); 
        }

        /** 
         * Seek for application ID field in WSP header. 
         * If application ID is found, WapPushManager substitute the message 
         * processing. Since WapPushManager is optional module, if WapPushManager 
         * is not found, legacy message processing will be continued. 
         */ 
        if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) { 
            index = (int) pduDecoder.getValue32(); 
            pduDecoder.decodeXWapApplicationId(index); 
            String wapAppId = pduDecoder.getValueString(); 
            if (wapAppId == null) { 
                wapAppId = Integer.toString((int) pduDecoder.getValue32()); 
            } 
            String contentType = ((mimeType == null) ? 
                                  Long.toString(binaryContentType) : mimeType); 
            if (Config.DEBUG) Log.v(LOG_TAG, "appid found: " + wapAppId + ":" + contentType); 
            try { 
                boolean processFurther = true; 
                IWapPushManager wapPushMan = mWapConn.getWapPushManager();

                if (wapPushMan == null) { 
                    if (Config.DEBUG) Log.w(LOG_TAG, "wap push manager not found!"); 
                } else { 
                    Intent intent = new Intent(); 
                    intent.putExtra("transactionId", transactionId); 
                    intent.putExtra("pduType", pduType); 
                    intent.putExtra("header", header); 
                    intent.putExtra("data", intentData); 
                    intent.putExtra("contentTypeParameters", 
                            pduDecoder.getContentParameters()); 
                    int procRet = wapPushMan.processMessage(wapAppId, contentType, intent); 
                    if (Config.DEBUG) Log.v(LOG_TAG, "procRet:" + procRet); 
                    if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0 
                        && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) { 
                        processFurther = false; 
                    } 
                } 
                if (!processFurther) { 
                    return Intents.RESULT_SMS_HANDLED; 
                } 
            } catch (RemoteException e) { 
                if (Config.DEBUG) Log.w(LOG_TAG, "remote func failed..."); 
            } 
        } 
        if (Config.DEBUG) Log.v(LOG_TAG, "fall back to existing handler");

        if (mimeType == null) { 
            if (Config.DEBUG) Log.w(LOG_TAG, "Header Content-Type error."); 
            return Intents.RESULT_SMS_GENERIC_ERROR; 
        } 
        String permission; 
        if (mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_MMS)) { 
            permission = "android.permission.RECEIVE_MMS"; 
        } else { 
            permission = "android.permission.RECEIVE_WAP_PUSH"; 
        } 
        Intent intent = new Intent(Intents.WAP_PUSH_RECEIVED_ACTION); 
        intent.setType(mimeType); 
        intent.putExtra("transactionId", transactionId); 
        intent.putExtra("pduType", pduType); 
        intent.putExtra("header", header); 
        intent.putExtra("data", intentData); 
        intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters()); 
        mSmsDispatcher.dispatch(intent, permission); 
        return Activity.RESULT_OK; 
    }

該方法比較長,我們直接看最後的一步:呼叫父類的dispatch將資訊廣播出去。

Intent intent = new Intent(Intents.WAP_PUSH_RECEIVED_ACTION);

intent.setType(mimeType);

intent.putExtra("transactionId", transactionId);

intent.putExtra("pduType", pduType);

intent.putExtra("header", header);

intent.putExtra("data", intentData);

intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters());

mSmsDispatcher.dispatch(intent, permission);

應用層PushReceiver類的onReceive將被呼叫,讓螢幕亮5秒,然後建立一個ReceivePushTask並使用它的execute方法。ReceivePushTask是一個AsyncTask,實現了doInBackground方法。當傳入intent後,會在doInBackground中將其中的資料轉成GenericPdu,並根據其訊息型別做出不同的操作。 
如果是傳送報告或已讀報告,將其存入資料庫。 
如果是彩信通知,若已存在,則不處理。否則將其存入資料庫。啟動TransactionService進行處理。

 

case MESSAGE_TYPE_NOTIFICATION_IND: { 
    NotificationInd nInd = (NotificationInd) pdu;

    if (MmsConfig.getTransIdEnabled()) { 
        byte [] contentLocation = nInd.getContentLocation(); 
        if ('=' == contentLocation[contentLocation.length - 1]) { 
            byte [] transactionId = nInd.getTransactionId(); 
            byte [] contentLocationWithId = new byte [contentLocation.length 
                                                      + transactionId.length]; 
            System.arraycopy(contentLocation, 0, contentLocationWithId, 
                    0, contentLocation.length); 
            System.arraycopy(transactionId, 0, contentLocationWithId, 
                    contentLocation.length, transactionId.length); 
            nInd.setContentLocation(contentLocationWithId); 
        } 
    }

    if (!isDuplicateNotification(mContext, nInd)) { 
        Uri uri = p.persist(pdu, Inbox.CONTENT_URI); 
        // Start service to finish the notification transaction. 
        Intent svc = new Intent(mContext, TransactionService.class); 
        svc.putExtra(TransactionBundle.URI, uri.toString()); 
        svc.putExtra(TransactionBundle.TRANSACTION_TYPE, 
                Transaction.NOTIFICATION_TRANSACTION); 
        mContext.startService(svc); 
    } else if (LOCAL_LOGV) { 
        Log.v(TAG, "Skip downloading duplicate message: " 
                + new String(nInd.getContentLocation())); 
    } 
    break; 
}

啟動TransactionService服務,在onStartCommand中呼叫launchTransaction方法。

private void launchTransaction(int serviceId, TransactionBundle txnBundle, 
            boolean noNetwork) { 
        if (noNetwork) { 
            Log.w(TAG, "launchTransaction: no network error!"); 
            onNetworkUnavailable(serviceId, txnBundle.getTransactionType()); 
            return; 
        } 
        Message msg = mServiceHandler.obtainMessage(EVENT_TRANSACTION_REQUEST); 
        msg.arg1 = serviceId; 
        msg.obj = txnBundle;

        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 
            Log.v(TAG, "launchTransaction: sending message " + msg); 
        } 
        mServiceHandler.sendMessage(msg); 
    }

接著以what= EVENT_TRANSACTION_REQUEST的引數執行mServiceHandler,在mServiceHandler的處理中將建立NotificationTransaction類的物件,經一系列的判斷最後將呼叫processTransaction方法處理NotificationTransaction物件。

private boolean processTransaction(Transaction transaction) 
            throws IOException { 
        // Check if transaction already processing 
        synchronized (mProcessing) { 
            for (Transaction t : mPending) { 
                if (t.isEquivalent(transaction)) { 
                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 
                        Log.v(TAG, "Transaction already pending: " 
                                + transaction.getServiceId()); 
                    } 
                    return true; 
                } 
            } 
            for (Transaction t : mProcessing) { 
                if (t.isEquivalent(transaction)) { 
                    if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 
                        Log.v(TAG, 
                                "Duplicated transaction: " 
                                        + transaction.getServiceId()); 
                    } 
                    return true; 
                } 
            }

            /* 
             * Make sure that the network connectivity necessary for MMS traffic 
             * is enabled. If it is not, we need to defer processing the 
             * transaction until connectivity is established. 
             */ 
            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 
                Log.v(TAG, "processTransaction: call beginMmsConnectivity..."); 
            } 
            int connectivityResult = beginMmsConnectivity(); 
            if (connectivityResult == Phone.APN_REQUEST_STARTED) { 
                mPending.add(transaction); 
                if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 
                    Log.v(TAG, 
                            "processTransaction: connResult=APN_REQUEST_STARTED, " 
                                    + "defer transaction pending MMS connectivity"); 
                } 
                return true; 
            }

            if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 
                Log.v(TAG, "Adding transaction to 'mProcessing' list: " 
                        + transaction); 
            } 
            mProcessing.add(transaction); 
        }

        // Set a timer to keep renewing our "lease" on the MMS connection 
        sendMessageDelayed(obtainMessage(EVENT_CONTINUE_MMS_CONNECTIVITY), 
                APN_EXTENSION_WAIT);

        if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) { 
            Log.v(TAG, "processTransaction: starting transaction " 
                    + transaction); 
        }

        // Attach to transaction and process it 
        transaction.attach(TransactionService.this); 
        transaction.process(); 
        return true; 
    }

該方法首先會先判斷該Transaction物件是否存在mPending或mProcessing佇列中,如果沒有則將物件加入到mProcessing中,並將TransactionService本身加入到NotificationTransaction物件的觀察者列表(這樣做的目的是為了後面下載完成後通知該服務TransactionService的mProcessing移除掉NotificationTransaction物件併傳送完成下載的廣播)。最後將呼叫NotificationTransaction的process方法。

public void process() { 
        new Thread(this).start(); 
    }

    public void run() { 
        DownloadManager downloadManager = DownloadManager.getInstance(); 
        boolean autoDownload = downloadManager.isAuto(); 
        boolean dataSuspended = (TelephonyManager.getDefault().getDataState() == TelephonyManager.DATA_SUSPENDED); 
        try { 
            if (LOCAL_LOGV) { 
                Log.v(TAG, "Notification transaction launched: " + this); 
            }

            // By default, we set status to STATUS_DEFERRED because we 
            // should response MMSC with STATUS_DEFERRED when we cannot 
            // download a MM immediately. 
            int status = STATUS_DEFERRED; 
            // Don't try to download when data is suspended, as it will fail, so 
            // defer download 
            if (!autoDownload || dataSuspended) { 
                downloadManager 
                        .markState(mUri, DownloadManager.STATE_UNSTARTED); 
                sendNotifyRespInd(status); 
                return; 
            }

            downloadManager.markState(mUri, DownloadManager.STATE_DOWNLOADING);

            if (LOCAL_LOGV) { 
                Log.v(TAG, "Content-Location: " + mContentLocation); 
            }

            byte[] retrieveConfData = null; 
            // We should catch exceptions here to response MMSC 
            // with STATUS_DEFERRED. 
            try { 
                retrieveConfData = getPdu(mContentLocation); 
            } catch (IOException e) { 
                mTransactionState.setState(FAILED); 
            }

            if (retrieveConfData != null) { 
                GenericPdu pdu = new PduParser(retrieveConfData).parse(); 
                if ((pdu == null) 
                        || (pdu.getMessageType() != MESSAGE_TYPE_RETRIEVE_CONF)) { 
                    Log.e(TAG, "Invalid M-RETRIEVE.CONF PDU."); 
                    mTransactionState.setState(FAILED); 
                    status = STATUS_UNRECOGNIZED; 
                } else { 
                    // Save the received PDU (must be a M-RETRIEVE.CONF). 
                    PduPersister p = PduPersister.getPduPersister(mContext); 
                    Uri uri = p.persist(pdu, Inbox.CONTENT_URI); 
                    // We have successfully downloaded the new MM. Delete the 
                    // M-NotifyResp.ind from Inbox. 
                    SqliteWrapper.delete(mContext, 
                            mContext.getContentResolver(), mUri, null, null); 
                    // Notify observers with newly received MM. 
                    mUri = uri; 
                    status = STATUS_RETRIEVED; 
                } 
            }

            if (LOCAL_LOGV) { 
                Log.v(TAG, "status=0x" + Integer.toHexString(status)); 
            }

            // Check the status and update the result state of this Transaction. 
            switch (status) { 
            case STATUS_RETRIEVED: 
                mTransactionState.setState(SUCCESS); 
                break; 
            case STATUS_DEFERRED: 
                // STATUS_DEFERRED, may be a failed immediate retrieval. 
                if (mTransactionState.getState() == INITIALIZED) { 
                    mTransactionState.setState(SUCCESS); 
                } 
                break; 
            }

            sendNotifyRespInd(status);

            // Make sure this thread isn't over the limits in message count. 
            Recycler.getMmsRecycler().deleteOldMessagesInSameThreadAsMessage( 
                    mContext, mUri); 
        } catch (Throwable t) { 
            Log.e(TAG, Log.getStackTraceString(t)); 
        } finally { 
            mTransactionState.setContentUri(mUri); 
            if (!autoDownload || dataSuspended) { 
                // Always mark the transaction successful for deferred 
                // download since any error here doesn't make sense. 
                mTransactionState.setState(SUCCESS); 
            } 
            if (mTransactionState.getState() != SUCCESS) { 
                mTransactionState.setState(FAILED); 
                Log.e(TAG, "NotificationTransaction failed."); 
            } 
            notifyObservers(); 
        } 
    }

NotificationTransaction的process方法將下載相應彩信,首先刪除彩信通知,通知mmsc,刪除超過容量限制的彩信,彩信附件的獲取最終是通過getPdu(mContentLocation)來請求附件的流,返回byte[]型別。最後將notifyObservers()通知TransactionService處理其餘待傳送的彩信和傳送下載完成的廣播。

看圖比較容易理解:(網上竊的一副妙圖~~,不過裡面最後那裡好像有一個小錯誤,不過沒關係~~)

image

(彩信接收主要大概涉及到的類的類圖,這是原創滴,原創就是簡陋)

image

RIL到SMSDispatch中間其實涉及很多步驟,這裡就簡明一點,抽絲剝繭,讓大家對彩信接收涉及到的類的分佈和以及它們的作用有個大概的藍圖。簡訊來後發現自己屬於GsmSMSDispatch類,WapPushOverSms例項呼叫dispatchWapPdu傳送廣播,PushReceive接收到廣播後啟動TransactionService服務,TransactionService將自己attach到mObservers的觀察列表中,然後呼叫NotificationTransaction物件的process方法,該方法將通過getPdu來獲取附件內容,最後將呼叫notifyObservers通知所有新增到觀察列表的物件呼叫update方法實現更新。 (這個就是傳說中的觀察者模式,改網上的一個例子:郵局現在有少男少女和花花公子的雜誌(雜誌是被觀察者,裡面維護一條訂閱者的佇列),現在有一個女孩想訂閱少男少女(女孩是觀察者,訂閱就是將自己add註冊到被觀察者的佇列),一個男孩訂閱少男少女和花花公子,當郵局某本雜誌到來就會通知到訂閱者(通知就是迴圈佇列通知每個訂閱該雜誌的人,然後將這本雜誌作為變數傳過去,這樣訂閱者就知道是哪本雜誌到來)。 同理:TransactionService是觀察者(男孩或者女孩),NotificationTransaction是被觀察者(雜誌))

 

參考和引用部分文字和圖片的文章:

http://www.diybl.com/course/3_program/java/android/20111124/562797.html

http://www.2cto.com/kf/201111/109802.html

相關文章