Home | History | Annotate | Download | only in telephony
      1 /*
      2  * Copyright (C) 2013 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 android.content.BroadcastReceiver;
     20 import android.content.ContentResolver;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.IntentFilter;
     24 import android.database.Cursor;
     25 import android.database.SQLException;
     26 import android.os.PersistableBundle;
     27 import android.os.UserManager;
     28 import android.telephony.CarrierConfigManager;
     29 import android.telephony.Rlog;
     30 import android.telephony.SubscriptionManager;
     31 
     32 import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
     33 import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
     34 
     35 import java.util.HashMap;
     36 import java.util.HashSet;
     37 
     38 /**
     39  * Called when the credential-encrypted storage is unlocked, collecting all acknowledged messages
     40  * and deleting any partial message segments older than 30 days. Called from a worker thread to
     41  * avoid delaying phone app startup. The last step is to broadcast the first pending message from
     42  * the main thread, then the remaining pending messages will be broadcast after the previous
     43  * ordered broadcast completes.
     44  */
     45 public class SmsBroadcastUndelivered {
     46     private static final String TAG = "SmsBroadcastUndelivered";
     47     private static final boolean DBG = InboundSmsHandler.DBG;
     48 
     49     /** Delete any partial message segments older than 30 days. */
     50     static final long DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE = (long) (60 * 60 * 1000) * 24 * 30;
     51 
     52     /**
     53      * Query projection for dispatching pending messages at boot time.
     54      * Column order must match the {@code *_COLUMN} constants in {@link InboundSmsHandler}.
     55      */
     56     private static final String[] PDU_PENDING_MESSAGE_PROJECTION = {
     57             "pdu",
     58             "sequence",
     59             "destination_port",
     60             "date",
     61             "reference_number",
     62             "count",
     63             "address",
     64             "_id",
     65             "message_body",
     66             "display_originating_addr"
     67     };
     68 
     69     private static SmsBroadcastUndelivered instance;
     70 
     71     /** Content resolver to use to access raw table from SmsProvider. */
     72     private final ContentResolver mResolver;
     73 
     74     /** Handler for 3GPP-format messages (may be null). */
     75     private final GsmInboundSmsHandler mGsmInboundSmsHandler;
     76 
     77     /** Handler for 3GPP2-format messages (may be null). */
     78     private final CdmaInboundSmsHandler mCdmaInboundSmsHandler;
     79 
     80     /** Broadcast receiver that processes the raw table when the user unlocks the phone for the
     81      *  first time after reboot and the credential-encrypted storage is available.
     82      */
     83     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
     84         @Override
     85         public void onReceive(final Context context, Intent intent) {
     86             Rlog.d(TAG, "Received broadcast " + intent.getAction());
     87             if (Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
     88                 new ScanRawTableThread(context).start();
     89             }
     90         }
     91     };
     92 
     93     private class ScanRawTableThread extends Thread {
     94         private final Context context;
     95 
     96         private ScanRawTableThread(Context context) {
     97             this.context = context;
     98         }
     99 
    100         @Override
    101         public void run() {
    102             scanRawTable(context);
    103             InboundSmsHandler.cancelNewMessageNotification(context);
    104         }
    105     }
    106 
    107     public static void initialize(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
    108         CdmaInboundSmsHandler cdmaInboundSmsHandler) {
    109         if (instance == null) {
    110             instance = new SmsBroadcastUndelivered(
    111                 context, gsmInboundSmsHandler, cdmaInboundSmsHandler);
    112         }
    113 
    114         // Tell handlers to start processing new messages and transit from the startup state to the
    115         // idle state. This method may be called multiple times for multi-sim devices. We must make
    116         // sure the state transition happen to all inbound sms handlers.
    117         if (gsmInboundSmsHandler != null) {
    118             gsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
    119         }
    120         if (cdmaInboundSmsHandler != null) {
    121             cdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
    122         }
    123     }
    124 
    125     private SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
    126             CdmaInboundSmsHandler cdmaInboundSmsHandler) {
    127         mResolver = context.getContentResolver();
    128         mGsmInboundSmsHandler = gsmInboundSmsHandler;
    129         mCdmaInboundSmsHandler = cdmaInboundSmsHandler;
    130 
    131         UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
    132 
    133         if (userManager.isUserUnlocked()) {
    134             new ScanRawTableThread(context).start();
    135         } else {
    136             IntentFilter userFilter = new IntentFilter();
    137             userFilter.addAction(Intent.ACTION_USER_UNLOCKED);
    138             context.registerReceiver(mBroadcastReceiver, userFilter);
    139         }
    140     }
    141 
    142     /**
    143      * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete.
    144      */
    145     private void scanRawTable(Context context) {
    146         if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages");
    147         long startTime = System.nanoTime();
    148         HashMap<SmsReferenceKey, Integer> multiPartReceivedCount =
    149                 new HashMap<SmsReferenceKey, Integer>(4);
    150         HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(4);
    151         Cursor cursor = null;
    152         try {
    153             // query only non-deleted ones
    154             cursor = mResolver.query(InboundSmsHandler.sRawUri, PDU_PENDING_MESSAGE_PROJECTION,
    155                     "deleted = 0", null,
    156                     null);
    157             if (cursor == null) {
    158                 Rlog.e(TAG, "error getting pending message cursor");
    159                 return;
    160             }
    161 
    162             boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2();
    163             while (cursor.moveToNext()) {
    164                 InboundSmsTracker tracker;
    165                 try {
    166                     tracker = TelephonyComponentFactory.getInstance().makeInboundSmsTracker(cursor,
    167                             isCurrentFormat3gpp2);
    168                 } catch (IllegalArgumentException e) {
    169                     Rlog.e(TAG, "error loading SmsTracker: " + e);
    170                     continue;
    171                 }
    172 
    173                 if (tracker.getMessageCount() == 1) {
    174                     // deliver single-part message
    175                     broadcastSms(tracker);
    176                 } else {
    177                     SmsReferenceKey reference = new SmsReferenceKey(tracker);
    178                     Integer receivedCount = multiPartReceivedCount.get(reference);
    179                     if (receivedCount == null) {
    180                         multiPartReceivedCount.put(reference, 1);    // first segment seen
    181                         long expirationTime = getUndeliveredSmsExpirationTime(context);
    182                         if (tracker.getTimestamp() <
    183                                 (System.currentTimeMillis() - expirationTime)) {
    184                             // older than 30 days; delete if we don't find all the segments
    185                             oldMultiPartMessages.add(reference);
    186                         }
    187                     } else {
    188                         int newCount = receivedCount + 1;
    189                         if (newCount == tracker.getMessageCount()) {
    190                             // looks like we've got all the pieces; send a single tracker
    191                             // to state machine which will find the other pieces to broadcast
    192                             if (DBG) Rlog.d(TAG, "found complete multi-part message");
    193                             broadcastSms(tracker);
    194                             // don't delete this old message until after we broadcast it
    195                             oldMultiPartMessages.remove(reference);
    196                         } else {
    197                             multiPartReceivedCount.put(reference, newCount);
    198                         }
    199                     }
    200                 }
    201             }
    202             // Delete old incomplete message segments
    203             for (SmsReferenceKey message : oldMultiPartMessages) {
    204                 // delete permanently
    205                 int rows = mResolver.delete(InboundSmsHandler.sRawUriPermanentDelete,
    206                         message.getDeleteWhere(), message.getDeleteWhereArgs());
    207                 if (rows == 0) {
    208                     Rlog.e(TAG, "No rows were deleted from raw table!");
    209                 } else if (DBG) {
    210                     Rlog.d(TAG, "Deleted " + rows + " rows from raw table for incomplete "
    211                             + message.mMessageCount + " part message");
    212                 }
    213             }
    214         } catch (SQLException e) {
    215             Rlog.e(TAG, "error reading pending SMS messages", e);
    216         } finally {
    217             if (cursor != null) {
    218                 cursor.close();
    219             }
    220             if (DBG) Rlog.d(TAG, "finished scanning raw table in "
    221                     + ((System.nanoTime() - startTime) / 1000000) + " ms");
    222         }
    223     }
    224 
    225     /**
    226      * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast.
    227      */
    228     private void broadcastSms(InboundSmsTracker tracker) {
    229         InboundSmsHandler handler;
    230         if (tracker.is3gpp2()) {
    231             handler = mCdmaInboundSmsHandler;
    232         } else {
    233             handler = mGsmInboundSmsHandler;
    234         }
    235         if (handler != null) {
    236             handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker);
    237         } else {
    238             Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver.");
    239         }
    240     }
    241 
    242     private long getUndeliveredSmsExpirationTime(Context context) {
    243         int subId = SubscriptionManager.getDefaultSmsSubscriptionId();
    244         CarrierConfigManager configManager =
    245                 (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
    246         PersistableBundle bundle = configManager.getConfigForSubId(subId);
    247 
    248         if (bundle != null) {
    249             return bundle.getLong(CarrierConfigManager.KEY_UNDELIVERED_SMS_MESSAGE_EXPIRATION_TIME,
    250                     DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE);
    251         } else {
    252             return DEFAULT_PARTIAL_SEGMENT_EXPIRE_AGE;
    253         }
    254     }
    255 
    256     /**
    257      * Used as the HashMap key for matching concatenated message segments.
    258      */
    259     private static class SmsReferenceKey {
    260         final String mAddress;
    261         final int mReferenceNumber;
    262         final int mMessageCount;
    263         final String mQuery;
    264 
    265         SmsReferenceKey(InboundSmsTracker tracker) {
    266             mAddress = tracker.getAddress();
    267             mReferenceNumber = tracker.getReferenceNumber();
    268             mMessageCount = tracker.getMessageCount();
    269             mQuery = tracker.getQueryForSegments();
    270 
    271         }
    272 
    273         String[] getDeleteWhereArgs() {
    274             return new String[]{mAddress, Integer.toString(mReferenceNumber),
    275                     Integer.toString(mMessageCount)};
    276         }
    277 
    278         String getDeleteWhere() {
    279             return mQuery;
    280         }
    281 
    282         @Override
    283         public int hashCode() {
    284             return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode();
    285         }
    286 
    287         @Override
    288         public boolean equals(Object o) {
    289             if (o instanceof SmsReferenceKey) {
    290                 SmsReferenceKey other = (SmsReferenceKey) o;
    291                 return other.mAddress.equals(mAddress)
    292                         && (other.mReferenceNumber == mReferenceNumber)
    293                         && (other.mMessageCount == mMessageCount);
    294             }
    295             return false;
    296         }
    297     }
    298 }
    299