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.ContentResolver;
20import android.content.Context;
21import android.database.Cursor;
22import android.database.SQLException;
23import android.net.Uri;
24import android.provider.Telephony;
25import android.telephony.Rlog;
26
27import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
28import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
29
30import java.util.HashMap;
31import 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 */
40public 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