Home | History | Annotate | Download | only in telephony
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.internal.telephony;
     18 
     19 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND;
     20 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
     21 import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND;
     22 import android.app.Activity;
     23 import android.app.AppOpsManager;
     24 import android.content.BroadcastReceiver;
     25 import android.content.ComponentName;
     26 import android.content.ContentValues;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.ServiceConnection;
     30 import android.database.Cursor;
     31 import android.database.DatabaseUtils;
     32 import android.database.sqlite.SQLiteException;
     33 import android.database.sqlite.SqliteWrapper;
     34 import android.net.Uri;
     35 import android.os.Bundle;
     36 import android.os.IBinder;
     37 import android.os.RemoteException;
     38 import android.os.UserHandle;
     39 import android.provider.Telephony;
     40 import android.provider.Telephony.Sms.Intents;
     41 import android.telephony.Rlog;
     42 import android.telephony.SmsManager;
     43 import android.telephony.SubscriptionManager;
     44 import android.util.Log;
     45 
     46 import com.android.internal.telephony.uicc.IccUtils;
     47 import com.google.android.mms.MmsException;
     48 import com.google.android.mms.pdu.DeliveryInd;
     49 import com.google.android.mms.pdu.GenericPdu;
     50 import com.google.android.mms.pdu.NotificationInd;
     51 import com.google.android.mms.pdu.PduHeaders;
     52 import com.google.android.mms.pdu.PduParser;
     53 import com.google.android.mms.pdu.PduPersister;
     54 import com.google.android.mms.pdu.ReadOrigInd;
     55 
     56 /**
     57  * WAP push handler class.
     58  *
     59  * @hide
     60  */
     61 public class WapPushOverSms implements ServiceConnection {
     62     private static final String TAG = "WAP PUSH";
     63     private static final boolean DBG = true;
     64 
     65     private final Context mContext;
     66 
     67     /** Assigned from ServiceConnection callback on main threaad. */
     68     private volatile IWapPushManager mWapPushManager;
     69 
     70     @Override
     71     public void onServiceConnected(ComponentName name, IBinder service) {
     72         mWapPushManager = IWapPushManager.Stub.asInterface(service);
     73         if (DBG) Rlog.v(TAG, "wappush manager connected to " + hashCode());
     74     }
     75 
     76     @Override
     77     public void onServiceDisconnected(ComponentName name) {
     78         mWapPushManager = null;
     79         if (DBG) Rlog.v(TAG, "wappush manager disconnected.");
     80     }
     81 
     82     public WapPushOverSms(Context context) {
     83         mContext = context;
     84         Intent intent = new Intent(IWapPushManager.class.getName());
     85         ComponentName comp = intent.resolveSystemService(context.getPackageManager(), 0);
     86         intent.setComponent(comp);
     87         if (comp == null || !context.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
     88             Rlog.e(TAG, "bindService() for wappush manager failed");
     89         } else {
     90             if (DBG) Rlog.v(TAG, "bindService() for wappush manager succeeded");
     91         }
     92     }
     93 
     94     void dispose() {
     95         if (mWapPushManager != null) {
     96             if (DBG) Rlog.v(TAG, "dispose: unbind wappush manager");
     97             mContext.unbindService(this);
     98         } else {
     99             Rlog.e(TAG, "dispose: not bound to a wappush manager");
    100         }
    101     }
    102 
    103     /**
    104      * Dispatches inbound messages that are in the WAP PDU format. See
    105      * wap-230-wsp-20010705-a section 8 for details on the WAP PDU format.
    106      *
    107      * @param pdu The WAP PDU, made up of one or more SMS PDUs
    108      * @return a result code from {@link android.provider.Telephony.Sms.Intents}, or
    109      *         {@link Activity#RESULT_OK} if the message has been broadcast
    110      *         to applications
    111      */
    112     public int dispatchWapPdu(byte[] pdu, BroadcastReceiver receiver, InboundSmsHandler handler) {
    113 
    114         if (DBG) Rlog.d(TAG, "Rx: " + IccUtils.bytesToHexString(pdu));
    115 
    116         try {
    117             int index = 0;
    118             int transactionId = pdu[index++] & 0xFF;
    119             int pduType = pdu[index++] & 0xFF;
    120 
    121             // Should we "abort" if no subId for now just no supplying extra param below
    122             int phoneId = handler.getPhone().getPhoneId();
    123 
    124             if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) &&
    125                     (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) {
    126                 index = mContext.getResources().getInteger(
    127                         com.android.internal.R.integer.config_valid_wappush_index);
    128                 if (index != -1) {
    129                     transactionId = pdu[index++] & 0xff;
    130                     pduType = pdu[index++] & 0xff;
    131                     if (DBG)
    132                         Rlog.d(TAG, "index = " + index + " PDU Type = " + pduType +
    133                                 " transactionID = " + transactionId);
    134 
    135                     // recheck wap push pduType
    136                     if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH)
    137                             && (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) {
    138                         if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType);
    139                         return Intents.RESULT_SMS_HANDLED;
    140                     }
    141                 } else {
    142                     if (DBG) Rlog.w(TAG, "Received non-PUSH WAP PDU. Type = " + pduType);
    143                     return Intents.RESULT_SMS_HANDLED;
    144                 }
    145             }
    146 
    147             WspTypeDecoder pduDecoder = new WspTypeDecoder(pdu);
    148 
    149             /**
    150              * Parse HeaderLen(unsigned integer).
    151              * From wap-230-wsp-20010705-a section 8.1.2
    152              * The maximum size of a uintvar is 32 bits.
    153              * So it will be encoded in no more than 5 octets.
    154              */
    155             if (pduDecoder.decodeUintvarInteger(index) == false) {
    156                 if (DBG) Rlog.w(TAG, "Received PDU. Header Length error.");
    157                 return Intents.RESULT_SMS_GENERIC_ERROR;
    158             }
    159             int headerLength = (int) pduDecoder.getValue32();
    160             index += pduDecoder.getDecodedDataLength();
    161 
    162             int headerStartIndex = index;
    163 
    164             /**
    165              * Parse Content-Type.
    166              * From wap-230-wsp-20010705-a section 8.4.2.24
    167              *
    168              * Content-type-value = Constrained-media | Content-general-form
    169              * Content-general-form = Value-length Media-type
    170              * Media-type = (Well-known-media | Extension-Media) *(Parameter)
    171              * Value-length = Short-length | (Length-quote Length)
    172              * Short-length = <Any octet 0-30>   (octet <= WAP_PDU_SHORT_LENGTH_MAX)
    173              * Length-quote = <Octet 31>         (WAP_PDU_LENGTH_QUOTE)
    174              * Length = Uintvar-integer
    175              */
    176             if (pduDecoder.decodeContentType(index) == false) {
    177                 if (DBG) Rlog.w(TAG, "Received PDU. Header Content-Type error.");
    178                 return Intents.RESULT_SMS_GENERIC_ERROR;
    179             }
    180 
    181             String mimeType = pduDecoder.getValueString();
    182             long binaryContentType = pduDecoder.getValue32();
    183             index += pduDecoder.getDecodedDataLength();
    184 
    185             byte[] header = new byte[headerLength];
    186             System.arraycopy(pdu, headerStartIndex, header, 0, header.length);
    187 
    188             byte[] intentData;
    189 
    190             if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO)) {
    191                 intentData = pdu;
    192             } else {
    193                 int dataIndex = headerStartIndex + headerLength;
    194                 intentData = new byte[pdu.length - dataIndex];
    195                 System.arraycopy(pdu, dataIndex, intentData, 0, intentData.length);
    196             }
    197 
    198             if (SmsManager.getDefault().getAutoPersisting()) {
    199                 // Store the wap push data in telephony
    200                 int [] subIds = SubscriptionManager.getSubId(phoneId);
    201                 // FIXME (tomtaylor) - when we've updated SubscriptionManager, change
    202                 // SubscriptionManager.DEFAULT_SUB_ID to SubscriptionManager.getDefaultSmsSubId()
    203                 int subId = (subIds != null) && (subIds.length > 0) ? subIds[0] :
    204                     SmsManager.getDefaultSmsSubscriptionId();
    205                 writeInboxMessage(subId, intentData);
    206             }
    207 
    208             /**
    209              * Seek for application ID field in WSP header.
    210              * If application ID is found, WapPushManager substitute the message
    211              * processing. Since WapPushManager is optional module, if WapPushManager
    212              * is not found, legacy message processing will be continued.
    213              */
    214             if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) {
    215                 index = (int) pduDecoder.getValue32();
    216                 pduDecoder.decodeXWapApplicationId(index);
    217                 String wapAppId = pduDecoder.getValueString();
    218                 if (wapAppId == null) {
    219                     wapAppId = Integer.toString((int) pduDecoder.getValue32());
    220                 }
    221 
    222                 String contentType = ((mimeType == null) ?
    223                         Long.toString(binaryContentType) : mimeType);
    224                 if (DBG) Rlog.v(TAG, "appid found: " + wapAppId + ":" + contentType);
    225 
    226                 try {
    227                     boolean processFurther = true;
    228                     IWapPushManager wapPushMan = mWapPushManager;
    229 
    230                     if (wapPushMan == null) {
    231                         if (DBG) Rlog.w(TAG, "wap push manager not found!");
    232                     } else {
    233                         Intent intent = new Intent();
    234                         intent.putExtra("transactionId", transactionId);
    235                         intent.putExtra("pduType", pduType);
    236                         intent.putExtra("header", header);
    237                         intent.putExtra("data", intentData);
    238                         intent.putExtra("contentTypeParameters",
    239                                 pduDecoder.getContentParameters());
    240                         SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId);
    241 
    242                         int procRet = wapPushMan.processMessage(wapAppId, contentType, intent);
    243                         if (DBG) Rlog.v(TAG, "procRet:" + procRet);
    244                         if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0
    245                                 && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) {
    246                             processFurther = false;
    247                         }
    248                     }
    249                     if (!processFurther) {
    250                         return Intents.RESULT_SMS_HANDLED;
    251                     }
    252                 } catch (RemoteException e) {
    253                     if (DBG) Rlog.w(TAG, "remote func failed...");
    254                 }
    255             }
    256             if (DBG) Rlog.v(TAG, "fall back to existing handler");
    257 
    258             if (mimeType == null) {
    259                 if (DBG) Rlog.w(TAG, "Header Content-Type error.");
    260                 return Intents.RESULT_SMS_GENERIC_ERROR;
    261             }
    262 
    263             String permission;
    264             int appOp;
    265 
    266             if (mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_MMS)) {
    267                 permission = android.Manifest.permission.RECEIVE_MMS;
    268                 appOp = AppOpsManager.OP_RECEIVE_MMS;
    269             } else {
    270                 permission = android.Manifest.permission.RECEIVE_WAP_PUSH;
    271                 appOp = AppOpsManager.OP_RECEIVE_WAP_PUSH;
    272             }
    273 
    274             Intent intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION);
    275             intent.setType(mimeType);
    276             intent.putExtra("transactionId", transactionId);
    277             intent.putExtra("pduType", pduType);
    278             intent.putExtra("header", header);
    279             intent.putExtra("data", intentData);
    280             intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters());
    281             SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId);
    282 
    283             // Direct the intent to only the default MMS app. If we can't find a default MMS app
    284             // then sent it to all broadcast receivers.
    285             ComponentName componentName = SmsApplication.getDefaultMmsApplication(mContext, true);
    286             if (componentName != null) {
    287                 // Deliver MMS message only to this receiver
    288                 intent.setComponent(componentName);
    289                 if (DBG) Rlog.v(TAG, "Delivering MMS to: " + componentName.getPackageName() +
    290                         " " + componentName.getClassName());
    291             }
    292 
    293             handler.dispatchIntent(intent, permission, appOp, receiver, UserHandle.OWNER);
    294             return Activity.RESULT_OK;
    295         } catch (ArrayIndexOutOfBoundsException aie) {
    296             // 0-byte WAP PDU or other unexpected WAP PDU contents can easily throw this;
    297             // log exception string without stack trace and return false.
    298             Rlog.e(TAG, "ignoring dispatchWapPdu() array index exception: " + aie);
    299             return Intents.RESULT_SMS_GENERIC_ERROR;
    300         }
    301     }
    302 
    303     private static boolean shouldParseContentDisposition(int subId) {
    304         return SmsManager
    305                 .getSmsManagerForSubscriptionId(subId)
    306                 .getCarrierConfigValues()
    307                 .getBoolean(SmsManager.MMS_CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION, true);
    308     }
    309 
    310     private void writeInboxMessage(int subId, byte[] pushData) {
    311         final GenericPdu pdu =
    312                 new PduParser(pushData, shouldParseContentDisposition(subId)).parse();
    313         if (pdu == null) {
    314             Rlog.e(TAG, "Invalid PUSH PDU");
    315         }
    316         final PduPersister persister = PduPersister.getPduPersister(mContext);
    317         final int type = pdu.getMessageType();
    318         try {
    319             switch (type) {
    320                 case MESSAGE_TYPE_DELIVERY_IND:
    321                 case MESSAGE_TYPE_READ_ORIG_IND: {
    322                     final long threadId = getDeliveryOrReadReportThreadId(mContext, pdu);
    323                     if (threadId == -1) {
    324                         // The associated SendReq isn't found, therefore skip
    325                         // processing this PDU.
    326                         Rlog.e(TAG, "Failed to find delivery or read report's thread id");
    327                         break;
    328                     }
    329                     final Uri uri = persister.persist(
    330                             pdu,
    331                             Telephony.Mms.Inbox.CONTENT_URI,
    332                             true/*createThreadId*/,
    333                             true/*groupMmsEnabled*/,
    334                             null/*preOpenedFiles*/);
    335                     if (uri == null) {
    336                         Rlog.e(TAG, "Failed to persist delivery or read report");
    337                         break;
    338                     }
    339                     // Update thread ID for ReadOrigInd & DeliveryInd.
    340                     final ContentValues values = new ContentValues(1);
    341                     values.put(Telephony.Mms.THREAD_ID, threadId);
    342                     if (SqliteWrapper.update(
    343                             mContext,
    344                             mContext.getContentResolver(),
    345                             uri,
    346                             values,
    347                             null/*where*/,
    348                             null/*selectionArgs*/) != 1) {
    349                         Rlog.e(TAG, "Failed to update delivery or read report thread id");
    350                     }
    351                     break;
    352                 }
    353                 case MESSAGE_TYPE_NOTIFICATION_IND: {
    354                     final NotificationInd nInd = (NotificationInd) pdu;
    355 
    356                     Bundle configs = SmsManager.getSmsManagerForSubscriptionId(subId)
    357                             .getCarrierConfigValues();
    358                     if (configs != null && configs.getBoolean(
    359                         SmsManager.MMS_CONFIG_APPEND_TRANSACTION_ID, false)) {
    360                         final byte [] contentLocation = nInd.getContentLocation();
    361                         if ('=' == contentLocation[contentLocation.length - 1]) {
    362                             byte [] transactionId = nInd.getTransactionId();
    363                             byte [] contentLocationWithId = new byte [contentLocation.length
    364                                     + transactionId.length];
    365                             System.arraycopy(contentLocation, 0, contentLocationWithId,
    366                                     0, contentLocation.length);
    367                             System.arraycopy(transactionId, 0, contentLocationWithId,
    368                                     contentLocation.length, transactionId.length);
    369                             nInd.setContentLocation(contentLocationWithId);
    370                         }
    371                     }
    372                     if (!isDuplicateNotification(mContext, nInd)) {
    373                         final Uri uri = persister.persist(
    374                                 pdu,
    375                                 Telephony.Mms.Inbox.CONTENT_URI,
    376                                 true/*createThreadId*/,
    377                                 true/*groupMmsEnabled*/,
    378                                 null/*preOpenedFiles*/);
    379                         if (uri == null) {
    380                             Rlog.e(TAG, "Failed to save MMS WAP push notification ind");
    381                         }
    382                     } else {
    383                         Rlog.d(TAG, "Skip storing duplicate MMS WAP push notification ind: "
    384                                 + new String(nInd.getContentLocation()));
    385                     }
    386                     break;
    387                 }
    388                 default:
    389                     Log.e(TAG, "Received unrecognized WAP Push PDU.");
    390             }
    391         } catch (MmsException e) {
    392             Log.e(TAG, "Failed to save MMS WAP push data: type=" + type, e);
    393         } catch (RuntimeException e) {
    394             Log.e(TAG, "Unexpected RuntimeException in persisting MMS WAP push data", e);
    395         }
    396 
    397     }
    398 
    399     private static final String THREAD_ID_SELECTION =
    400             Telephony.Mms.MESSAGE_ID + "=? AND " + Telephony.Mms.MESSAGE_TYPE + "=?";
    401 
    402     private static long getDeliveryOrReadReportThreadId(Context context, GenericPdu pdu) {
    403         String messageId;
    404         if (pdu instanceof DeliveryInd) {
    405             messageId = new String(((DeliveryInd) pdu).getMessageId());
    406         } else if (pdu instanceof ReadOrigInd) {
    407             messageId = new String(((ReadOrigInd) pdu).getMessageId());
    408         } else {
    409             Rlog.e(TAG, "WAP Push data is neither delivery or read report type: "
    410                     + pdu.getClass().getCanonicalName());
    411             return -1L;
    412         }
    413         Cursor cursor = null;
    414         try {
    415             cursor = SqliteWrapper.query(
    416                     context,
    417                     context.getContentResolver(),
    418                     Telephony.Mms.CONTENT_URI,
    419                     new String[]{ Telephony.Mms.THREAD_ID },
    420                     THREAD_ID_SELECTION,
    421                     new String[]{
    422                             DatabaseUtils.sqlEscapeString(messageId),
    423                             Integer.toString(PduHeaders.MESSAGE_TYPE_SEND_REQ)
    424                     },
    425                     null/*sortOrder*/);
    426             if (cursor != null && cursor.moveToFirst()) {
    427                 return cursor.getLong(0);
    428             }
    429         } catch (SQLiteException e) {
    430             Rlog.e(TAG, "Failed to query delivery or read report thread id", e);
    431         } finally {
    432             if (cursor != null) {
    433                 cursor.close();
    434             }
    435         }
    436         return -1L;
    437     }
    438 
    439     private static final String LOCATION_SELECTION =
    440             Telephony.Mms.MESSAGE_TYPE + "=? AND " + Telephony.Mms.CONTENT_LOCATION + " =?";
    441 
    442     private static boolean isDuplicateNotification(Context context, NotificationInd nInd) {
    443         final byte[] rawLocation = nInd.getContentLocation();
    444         if (rawLocation != null) {
    445             String location = new String(rawLocation);
    446             String[] selectionArgs = new String[] { location };
    447             Cursor cursor = null;
    448             try {
    449                 cursor = SqliteWrapper.query(
    450                         context,
    451                         context.getContentResolver(),
    452                         Telephony.Mms.CONTENT_URI,
    453                         new String[]{Telephony.Mms._ID},
    454                         LOCATION_SELECTION,
    455                         new String[]{
    456                                 Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND),
    457                                 new String(rawLocation)
    458                         },
    459                         null/*sortOrder*/);
    460                 if (cursor != null && cursor.getCount() > 0) {
    461                     // We already received the same notification before.
    462                     return true;
    463                 }
    464             } catch (SQLiteException e) {
    465                 Rlog.e(TAG, "failed to query existing notification ind", e);
    466             } finally {
    467                 if (cursor != null) {
    468                     cursor.close();
    469                 }
    470             }
    471         }
    472         return false;
    473     }
    474 }
    475