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.cellbroadcastservice;
18 
19 import android.annotation.NonNull;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.res.Resources;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.os.SystemClock;
30 import android.os.UserHandle;
31 import android.provider.Telephony.CellBroadcasts;
32 import android.telephony.CbGeoUtils.Geometry;
33 import android.telephony.CellBroadcastIntents;
34 import android.telephony.CellIdentity;
35 import android.telephony.CellIdentityGsm;
36 import android.telephony.CellInfo;
37 import android.telephony.SmsCbLocation;
38 import android.telephony.SmsCbMessage;
39 import android.telephony.SubscriptionManager;
40 import android.telephony.TelephonyManager;
41 import android.text.TextUtils;
42 import android.text.format.DateUtils;
43 import android.util.Pair;
44 import android.util.SparseArray;
45 
46 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage;
47 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
48 import com.android.internal.annotations.VisibleForTesting;
49 
50 import java.text.DateFormat;
51 import java.util.ArrayList;
52 import java.util.HashMap;
53 import java.util.Iterator;
54 import java.util.List;
55 import java.util.stream.IntStream;
56 
57 /**
58  * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts.
59  */
60 public class GsmCellBroadcastHandler extends CellBroadcastHandler {
61     private static final boolean VDBG = false;  // log CB PDU data
62 
63     /** Indicates that a message is not displayed. */
64     private static final String MESSAGE_NOT_DISPLAYED = "0";
65 
66     private final SparseArray<String> mAreaInfos = new SparseArray<>();
67 
68     /** This map holds incomplete concatenated messages waiting for assembly. */
69     private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
70             new HashMap<>(4);
71 
72     @VisibleForTesting
GsmCellBroadcastHandler(Context context, Looper looper)73     public GsmCellBroadcastHandler(Context context, Looper looper) {
74         super("GsmCellBroadcastHandler", context, looper);
75     }
76 
77     @Override
onQuitting()78     protected void onQuitting() {
79         super.onQuitting();     // release wakelock
80     }
81 
82     /**
83      * Handle a GSM cell broadcast message passed from the telephony framework.
84      * @param message
85      */
onGsmCellBroadcastSms(int slotIndex, byte[] message)86     public void onGsmCellBroadcastSms(int slotIndex, byte[] message) {
87         sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message);
88     }
89 
90     /**
91      * Get the area information
92      *
93      * @param slotIndex SIM slot index
94      * @return The area information
95      */
96     @NonNull
getCellBroadcastAreaInfo(int slotIndex)97     public String getCellBroadcastAreaInfo(int slotIndex) {
98         String info;
99         synchronized (mAreaInfos) {
100             info = mAreaInfos.get(slotIndex);
101         }
102         return info == null ? "" : info;
103     }
104 
105     /**
106      * Create a new CellBroadcastHandler.
107      * @param context the context to use for dispatching Intents
108      * @return the new handler
109      */
makeGsmCellBroadcastHandler(Context context)110     public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) {
111         GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper());
112         handler.start();
113         return handler;
114     }
115 
116     /**
117      * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a
118      * geo-fencing check for these messages.
119      * @param geoFencingTriggerMessage the trigger message
120      *
121      * @return {@code True} if geo-fencing is need for some cell broadcast message.
122      */
handleGeoFencingTriggerMessage( GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex)123     private boolean handleGeoFencingTriggerMessage(
124             GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) {
125         final List<SmsCbMessage> cbMessages = new ArrayList<>();
126         final List<Uri> cbMessageUris = new ArrayList<>();
127 
128         SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
129                 Context.TELEPHONY_SUBSCRIPTION_SERVICE);
130         int[] subIds = subMgr.getSubscriptionIds(slotIndex);
131         Resources res;
132         if (subIds != null) {
133             res = getResources(subIds[0]);
134         } else {
135             res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
136         }
137 
138         // Only consider the cell broadcast received within 24 hours.
139         long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
140 
141         // Some carriers require reset duplication detection after airplane mode or reboot.
142         if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) {
143             lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime);
144             lastReceivedTime = Long.max(lastReceivedTime,
145                     System.currentTimeMillis() - SystemClock.elapsedRealtime());
146         }
147 
148         // Find the cell broadcast message identify by the message identifier and serial number
149         // and was not displayed.
150         String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND "
151                 + CellBroadcasts.SERIAL_NUMBER + "=? AND "
152                 + CellBroadcasts.MESSAGE_DISPLAYED + "=? AND "
153                 + CellBroadcasts.RECEIVED_TIME + ">?";
154 
155         ContentResolver resolver = mContext.getContentResolver();
156         for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
157             try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
158                     CellBroadcastProvider.QUERY_COLUMNS,
159                     where,
160                     new String[] { Integer.toString(identity.messageIdentifier),
161                             Integer.toString(identity.serialNumber), MESSAGE_NOT_DISPLAYED,
162                             Long.toString(lastReceivedTime) },
163                     null /* sortOrder */)) {
164                 if (cursor != null) {
165                     while (cursor.moveToNext()) {
166                         cbMessages.add(SmsCbMessage.createFromCursor(cursor));
167                         cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI,
168                                 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
169                     }
170                 }
171             }
172         }
173 
174         log("Found " + cbMessages.size() + " not broadcasted messages since "
175                 + DateFormat.getDateTimeInstance().format(lastReceivedTime));
176 
177         List<Geometry> commonBroadcastArea = new ArrayList<>();
178         if (geoFencingTriggerMessage.shouldShareBroadcastArea()) {
179             for (SmsCbMessage msg : cbMessages) {
180                 if (msg.getGeometries() != null) {
181                     commonBroadcastArea.addAll(msg.getGeometries());
182                 }
183             }
184         }
185 
186         // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified
187         // in geo fencing trigger message. We will pick the largest maximum wait time among these
188         // cell broadcasts.
189         int maximumWaitTimeSec = 0;
190         for (SmsCbMessage msg : cbMessages) {
191             maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingDuration());
192         }
193 
194         if (DBG) {
195             logd("Geo-fencing trigger message = " + geoFencingTriggerMessage);
196             for (SmsCbMessage msg : cbMessages) {
197                 logd(msg.toString());
198             }
199         }
200 
201         if (cbMessages.isEmpty()) {
202             if (DBG) logd("No CellBroadcast message need to be broadcasted");
203             return false;
204         }
205 
206         requestLocationUpdate(location -> {
207             if (location == null) {
208                 // If the location is not available, broadcast the messages directly.
209                 for (int i = 0; i < cbMessages.size(); i++) {
210                     broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
211                 }
212             } else {
213                 for (int i = 0; i < cbMessages.size(); i++) {
214                     List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
215                             ? commonBroadcastArea : cbMessages.get(i).getGeometries();
216                     if (broadcastArea == null || broadcastArea.isEmpty()) {
217                         broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
218                     } else {
219                         performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea,
220                                 location, slotIndex);
221                     }
222                 }
223             }
224         }, maximumWaitTimeSec);
225         return true;
226     }
227 
228     /**
229      * Process area info message.
230      *
231      * @param slotIndex SIM slot index
232      * @param message Cell broadcast message
233      * @return {@code true} if the mssage is an area info message and got processed correctly,
234      * otherwise {@code false}.
235      */
handleAreaInfoMessage(int slotIndex, SmsCbMessage message)236     private boolean handleAreaInfoMessage(int slotIndex, SmsCbMessage message) {
237         SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
238                 Context.TELEPHONY_SUBSCRIPTION_SERVICE);
239 
240         // Check area info message
241         int[] subIds = subMgr.getSubscriptionIds(slotIndex);
242         Resources res;
243         if (subIds != null) {
244             res = getResources(subIds[0]);
245         } else {
246             res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
247         }
248         int[] areaInfoChannels = res.getIntArray(R.array.area_info_channels);
249 
250         if (IntStream.of(areaInfoChannels).anyMatch(
251                 x -> x == message.getServiceCategory())) {
252             synchronized (mAreaInfos) {
253                 String info = mAreaInfos.get(slotIndex);
254                 if (TextUtils.equals(info, message.getMessageBody())) {
255                     // Message is a duplicate
256                     return true;
257                 }
258                 mAreaInfos.put(slotIndex, message.getMessageBody());
259             }
260 
261             String[] pkgs = mContext.getResources().getStringArray(
262                     R.array.config_area_info_receiver_packages);
263             for (String pkg : pkgs) {
264                 Intent intent = new Intent(CellBroadcastIntents.ACTION_AREA_INFO_UPDATED);
265                 intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex);
266                 intent.setPackage(pkg);
267                 mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
268                         android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
269             }
270             return true;
271         }
272 
273         // This is not an area info message.
274         return false;
275     }
276 
277     /**
278      * Handle 3GPP-format Cell Broadcast messages sent from radio.
279      *
280      * @param message the message to process
281      * @return true if need to wait for geo-fencing or an ordered broadcast was sent.
282      */
283     @Override
handleSmsMessage(Message message)284     protected boolean handleSmsMessage(Message message) {
285         // For GSM, message.obj should be a byte[]
286         int slotIndex = message.arg1;
287         if (message.obj instanceof byte[]) {
288             byte[] pdu = (byte[]) message.obj;
289             SmsCbHeader header = createSmsCbHeader(pdu);
290             if (header == null) return false;
291 
292             log("header=" + header);
293             if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) {
294                 GeoFencingTriggerMessage triggerMessage =
295                         GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu);
296                 if (triggerMessage != null) {
297                     return handleGeoFencingTriggerMessage(triggerMessage, slotIndex);
298                 }
299             } else {
300                 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
301                 if (cbMessage != null) {
302                     if (isDuplicate(cbMessage)) {
303                         return false;
304                     }
305 
306                     if (handleAreaInfoMessage(slotIndex, cbMessage)) {
307                         log("Channel " + cbMessage.getServiceCategory() + " message processed");
308                         return false;
309                     }
310 
311                     handleBroadcastSms(cbMessage);
312                     return true;
313                 }
314                 if (VDBG) log("Not handled GSM broadcasts.");
315             }
316         }
317         return super.handleSmsMessage(message);
318     }
319 
320     // return the GSM cell location from the first GSM cell info
getGsmLacAndCid()321     private Pair<Integer, Integer> getGsmLacAndCid() {
322         TelephonyManager tm =
323                 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
324         List<CellInfo> infos = tm.getAllCellInfo();
325         for (CellInfo info : infos) {
326             CellIdentity ci = info.getCellIdentity();
327             if (ci instanceof CellIdentityGsm) {
328                 CellIdentityGsm ciGsm = (CellIdentityGsm) ci;
329                 int lac = ciGsm.getLac() != CellInfo.UNAVAILABLE ? ciGsm.getLac() : -1;
330                 int cid = ciGsm.getCid() != CellInfo.UNAVAILABLE ? ciGsm.getCid() : -1;
331                 return Pair.create(lac, cid);
332             }
333         }
334         return null;
335     }
336 
337 
338     /**
339      * Handle 3GPP format SMS-CB message.
340      * @param header the cellbroadcast header.
341      * @param receivedPdu the received PDUs as a byte[]
342      */
handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu, int slotIndex)343     private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu,
344             int slotIndex) {
345         try {
346             if (VDBG) {
347                 int pduLength = receivedPdu.length;
348                 for (int i = 0; i < pduLength; i += 8) {
349                     StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
350                     for (int j = i; j < i + 8 && j < pduLength; j++) {
351                         int b = receivedPdu[j] & 0xff;
352                         if (b < 0x10) {
353                             sb.append('0');
354                         }
355                         sb.append(Integer.toHexString(b)).append(' ');
356                     }
357                     log(sb.toString());
358                 }
359             }
360 
361             if (VDBG) log("header=" + header);
362             TelephonyManager tm =
363                     (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
364             tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex));
365             // TODO make a systemAPI for getNetworkOperatorForSlotIndex
366             String plmn = tm.getSimOperator();
367             int lac = -1;
368             int cid = -1;
369             Pair<Integer, Integer> lacAndCid = getGsmLacAndCid();
370             // Check if GSM lac and cid are available. This is required to support
371             // dual-mode devices such as CDMA/LTE devices that require support for
372             // both 3GPP and 3GPP2 format messages
373             if (lacAndCid != null) {
374                 lac = lacAndCid.first;
375                 cid = lacAndCid.second;
376             }
377 
378             SmsCbLocation location;
379             switch (header.getGeographicalScope()) {
380                 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE:
381                     location = new SmsCbLocation(plmn, lac, -1);
382                     break;
383 
384                 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
385                 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
386                     location = new SmsCbLocation(plmn, lac, cid);
387                     break;
388 
389                 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
390                 default:
391                     location = new SmsCbLocation(plmn, -1, -1);
392                     break;
393             }
394 
395             byte[][] pdus;
396             int pageCount = header.getNumberOfPages();
397             if (pageCount > 1) {
398                 // Multi-page message
399                 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
400 
401                 // Try to find other pages of the same message
402                 pdus = mSmsCbPageMap.get(concatInfo);
403 
404                 if (pdus == null) {
405                     // This is the first page of this message, make room for all
406                     // pages and keep until complete
407                     pdus = new byte[pageCount][];
408 
409                     mSmsCbPageMap.put(concatInfo, pdus);
410                 }
411 
412                 if (VDBG) log("pdus size=" + pdus.length);
413                 // Page parameter is one-based
414                 pdus[header.getPageIndex() - 1] = receivedPdu;
415 
416                 for (byte[] pdu : pdus) {
417                     if (pdu == null) {
418                         // Still missing pages, exit
419                         log("still missing pdu");
420                         return null;
421                     }
422                 }
423 
424                 // Message complete, remove and dispatch
425                 mSmsCbPageMap.remove(concatInfo);
426             } else {
427                 // Single page message
428                 pdus = new byte[1][];
429                 pdus[0] = receivedPdu;
430             }
431 
432             // Remove messages that are out of scope to prevent the map from
433             // growing indefinitely, containing incomplete messages that were
434             // never assembled
435             Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
436 
437             while (iter.hasNext()) {
438                 SmsCbConcatInfo info = iter.next();
439 
440                 if (!info.matchesLocation(plmn, lac, cid)) {
441                     iter.remove();
442                 }
443             }
444 
445             return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex);
446 
447         } catch (RuntimeException e) {
448             loge("Error in decoding SMS CB pdu", e);
449             return null;
450         }
451     }
452 
createSmsCbHeader(byte[] bytes)453     private SmsCbHeader createSmsCbHeader(byte[] bytes) {
454         try {
455             return new SmsCbHeader(bytes);
456         } catch (Exception ex) {
457             loge("Can't create SmsCbHeader, ex = " + ex.toString());
458             return null;
459         }
460     }
461 
462     /**
463      * Holds all info about a message page needed to assemble a complete concatenated message.
464      */
465     @VisibleForTesting
466     public static final class SmsCbConcatInfo {
467 
468         private final SmsCbHeader mHeader;
469         private final SmsCbLocation mLocation;
470 
471         @VisibleForTesting
SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location)472         public SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
473             mHeader = header;
474             mLocation = location;
475         }
476 
477         @Override
hashCode()478         public int hashCode() {
479             return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
480         }
481 
482         @Override
equals(Object obj)483         public boolean equals(Object obj) {
484             if (obj instanceof SmsCbConcatInfo) {
485                 SmsCbConcatInfo other = (SmsCbConcatInfo) obj;
486 
487                 // Two pages match if they have the same serial number (which includes the
488                 // geographical scope and update number), and both pages belong to the same
489                 // location (PLMN, plus LAC and CID if these are part of the geographical scope).
490                 return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
491                         && mLocation.equals(other.mLocation);
492             }
493 
494             return false;
495         }
496 
497         /**
498          * Compare the location code for this message to the current location code. The match is
499          * relative to the geographical scope of the message, which determines whether the LAC
500          * and Cell ID are saved in mLocation or set to -1 to match all values.
501          *
502          * @param plmn the current PLMN
503          * @param lac the current Location Area (GSM) or Service Area (UMTS)
504          * @param cid the current Cell ID
505          * @return true if this message is valid for the current location; false otherwise
506          */
matchesLocation(String plmn, int lac, int cid)507         public boolean matchesLocation(String plmn, int lac, int cid) {
508             return mLocation.isInLocationArea(plmn, lac, cid);
509         }
510     }
511 }
512