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