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