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.ContentResolver;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.database.SQLException;
     23 import android.net.Uri;
     24 import android.provider.Telephony;
     25 import android.telephony.Rlog;
     26 
     27 import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
     28 import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
     29 
     30 import java.util.HashMap;
     31 import java.util.HashSet;
     32 
     33 /**
     34  * Called at boot time to clean out the raw table, collecting all acknowledged messages and
     35  * deleting any partial message segments older than 30 days. Called from a worker thread to
     36  * avoid delaying phone app startup. The last step is to broadcast the first pending message
     37  * from the main thread, then the remaining pending messages will be broadcast after the
     38  * previous ordered broadcast completes.
     39  */
     40 public class SmsBroadcastUndelivered implements Runnable {
     41     private static final String TAG = "SmsBroadcastUndelivered";
     42     private static final boolean DBG = InboundSmsHandler.DBG;
     43 
     44     /** Delete any partial message segments older than 30 days. */
     45     static final long PARTIAL_SEGMENT_EXPIRE_AGE = (long) (60 * 60 * 1000) * 24 * 30;
     46 
     47     /**
     48      * Query projection for dispatching pending messages at boot time.
     49      * Column order must match the {@code *_COLUMN} constants in {@link InboundSmsHandler}.
     50      */
     51     private static final String[] PDU_PENDING_MESSAGE_PROJECTION = {
     52             "pdu",
     53             "sequence",
     54             "destination_port",
     55             "date",
     56             "reference_number",
     57             "count",
     58             "address",
     59             "_id"
     60     };
     61 
     62     /** URI for raw table from SmsProvider. */
     63     private static final Uri sRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw");
     64 
     65     /** Content resolver to use to access raw table from SmsProvider. */
     66     private final ContentResolver mResolver;
     67 
     68     /** Handler for 3GPP-format messages (may be null). */
     69     private final GsmInboundSmsHandler mGsmInboundSmsHandler;
     70 
     71     /** Handler for 3GPP2-format messages (may be null). */
     72     private final CdmaInboundSmsHandler mCdmaInboundSmsHandler;
     73 
     74     public SmsBroadcastUndelivered(Context context, GsmInboundSmsHandler gsmInboundSmsHandler,
     75             CdmaInboundSmsHandler cdmaInboundSmsHandler) {
     76         mResolver = context.getContentResolver();
     77         mGsmInboundSmsHandler = gsmInboundSmsHandler;
     78         mCdmaInboundSmsHandler = cdmaInboundSmsHandler;
     79     }
     80 
     81     @Override
     82     public void run() {
     83         if (DBG) Rlog.d(TAG, "scanning raw table for undelivered messages");
     84         scanRawTable();
     85         // tell handlers to start processing new messages
     86         if (mGsmInboundSmsHandler != null) {
     87             mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
     88         }
     89         if (mCdmaInboundSmsHandler != null) {
     90             mCdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_START_ACCEPTING_SMS);
     91         }
     92     }
     93 
     94     /**
     95      * Scan the raw table for complete SMS messages to broadcast, and old PDUs to delete.
     96      */
     97     private void scanRawTable() {
     98         long startTime = System.nanoTime();
     99         HashMap<SmsReferenceKey, Integer> multiPartReceivedCount =
    100                 new HashMap<SmsReferenceKey, Integer>(4);
    101         HashSet<SmsReferenceKey> oldMultiPartMessages = new HashSet<SmsReferenceKey>(4);
    102         Cursor cursor = null;
    103         try {
    104             cursor = mResolver.query(sRawUri, PDU_PENDING_MESSAGE_PROJECTION, null, null, null);
    105             if (cursor == null) {
    106                 Rlog.e(TAG, "error getting pending message cursor");
    107                 return;
    108             }
    109 
    110             boolean isCurrentFormat3gpp2 = InboundSmsHandler.isCurrentFormat3gpp2();
    111             while (cursor.moveToNext()) {
    112                 InboundSmsTracker tracker;
    113                 try {
    114                     tracker = new InboundSmsTracker(cursor, isCurrentFormat3gpp2);
    115                 } catch (IllegalArgumentException e) {
    116                     Rlog.e(TAG, "error loading SmsTracker: " + e);
    117                     continue;
    118                 }
    119 
    120                 if (tracker.getMessageCount() == 1) {
    121                     // deliver single-part message
    122                     broadcastSms(tracker);
    123                 } else {
    124                     SmsReferenceKey reference = new SmsReferenceKey(tracker);
    125                     Integer receivedCount = multiPartReceivedCount.get(reference);
    126                     if (receivedCount == null) {
    127                         multiPartReceivedCount.put(reference, 1);    // first segment seen
    128                         if (tracker.getTimestamp() <
    129                                 (System.currentTimeMillis() - PARTIAL_SEGMENT_EXPIRE_AGE)) {
    130                             // older than 30 days; delete if we don't find all the segments
    131                             oldMultiPartMessages.add(reference);
    132                         }
    133                     } else {
    134                         int newCount = receivedCount + 1;
    135                         if (newCount == tracker.getMessageCount()) {
    136                             // looks like we've got all the pieces; send a single tracker
    137                             // to state machine which will find the other pieces to broadcast
    138                             if (DBG) Rlog.d(TAG, "found complete multi-part message");
    139                             broadcastSms(tracker);
    140                             // don't delete this old message until after we broadcast it
    141                             oldMultiPartMessages.remove(reference);
    142                         } else {
    143                             multiPartReceivedCount.put(reference, newCount);
    144                         }
    145                     }
    146                 }
    147             }
    148             // Delete old incomplete message segments
    149             for (SmsReferenceKey message : oldMultiPartMessages) {
    150                 int rows = mResolver.delete(sRawUri, InboundSmsHandler.SELECT_BY_REFERENCE,
    151                         message.getDeleteWhereArgs());
    152                 if (rows == 0) {
    153                     Rlog.e(TAG, "No rows were deleted from raw table!");
    154                 } else if (DBG) {
    155                     Rlog.d(TAG, "Deleted " + rows + " rows from raw table for incomplete "
    156                             + message.mMessageCount + " part message");
    157                 }
    158             }
    159         } catch (SQLException e) {
    160             Rlog.e(TAG, "error reading pending SMS messages", e);
    161         } finally {
    162             if (cursor != null) {
    163                 cursor.close();
    164             }
    165             if (DBG) Rlog.d(TAG, "finished scanning raw table in "
    166                     + ((System.nanoTime() - startTime) / 1000000) + " ms");
    167         }
    168     }
    169 
    170     /**
    171      * Send tracker to appropriate (3GPP or 3GPP2) inbound SMS handler for broadcast.
    172      */
    173     private void broadcastSms(InboundSmsTracker tracker) {
    174         InboundSmsHandler handler;
    175         if (tracker.is3gpp2()) {
    176             handler = mCdmaInboundSmsHandler;
    177         } else {
    178             handler = mGsmInboundSmsHandler;
    179         }
    180         if (handler != null) {
    181             handler.sendMessage(InboundSmsHandler.EVENT_BROADCAST_SMS, tracker);
    182         } else {
    183             Rlog.e(TAG, "null handler for " + tracker.getFormat() + " format, can't deliver.");
    184         }
    185     }
    186 
    187     /**
    188      * Used as the HashMap key for matching concatenated message segments.
    189      */
    190     private static class SmsReferenceKey {
    191         final String mAddress;
    192         final int mReferenceNumber;
    193         final int mMessageCount;
    194 
    195         SmsReferenceKey(InboundSmsTracker tracker) {
    196             mAddress = tracker.getAddress();
    197             mReferenceNumber = tracker.getReferenceNumber();
    198             mMessageCount = tracker.getMessageCount();
    199         }
    200 
    201         String[] getDeleteWhereArgs() {
    202             return new String[]{mAddress, Integer.toString(mReferenceNumber),
    203                     Integer.toString(mMessageCount)};
    204         }
    205 
    206         @Override
    207         public int hashCode() {
    208             return ((mReferenceNumber * 31) + mMessageCount) * 31 + mAddress.hashCode();
    209         }
    210 
    211         @Override
    212         public boolean equals(Object o) {
    213             if (o instanceof SmsReferenceKey) {
    214                 SmsReferenceKey other = (SmsReferenceKey) o;
    215                 return other.mAddress.equals(mAddress)
    216                         && (other.mReferenceNumber == mReferenceNumber)
    217                         && (other.mMessageCount == mMessageCount);
    218             }
    219             return false;
    220         }
    221     }
    222 }
    223